Skip to content

Tech debt: Create abstraction for Metrics and do not expose aws-embedded-metrics-java #1848

Open
@phipag

Description

@phipag

Why is this needed?

Today, we are exposing the whole interface of the https://github.com/awslabs/aws-embedded-metrics-java library that we use in the Metrics utility for logging CloudWatch EMF metrics. Example:

import software.amazon.lambda.powertools.metrics.Metrics;
// We expose the MetricsLogger interface from the EMF library
import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger;

public class MetricsEnabledHandler implements RequestHandler<Object, Object> {

    MetricsLogger metricsLogger = MetricsUtils.metricsLogger();

    @Override
    @Metrics(namespace = "ExampleApplication", service = "booking")
    public Object handleRequest(Object input, Context context) {
        // User calls methods on software.amazon.cloudwatchlogs.emf.logger.MetricsLogger transitive dependency
        metricsLogger.putDimensions(DimensionSet.of("environment", "prod"));
        metricsLogger.putMetric("SuccessfulBooking", 1, Unit.COUNT);
    }
}

This design is not optimal because it exposes the Metrics utility directly to breaking changes in this transitive dependency. This impacts the extensibility and interface design flexibility of the Metrics utility negatively:

  • Breaking changes in the EMF dependency will directly impact customers and require major version bumps of the Metrics utility in Powertools
  • Direct coupling with the EMF dependency only allows exposing metrics in EMF and hinders adoption of new Metrics backends

Which area does this relate to?

Metrics Utility tech debt

Suggestion

This issue suggests to add a Metrics interface similar to the other Powertools for AWS runtimes. For example, the same as .NET is doing: https://github.com/aws-powertools/powertools-lambda-dotnet/blob/develop/libraries/src/AWS.Lambda.Powertools.Metrics/IMetrics.cs.

Such an interface will abstract away the direct dependency on the EMF library and will allow us to add new backends or to replace the EMF backend with our own implementation in the future.

Example interface in Java:

package com.amazonaws.lambda.powertools.metrics;

import java.util.Map;
import com.amazonaws.services.lambda.runtime.Context;

public interface Metrics {

    // MetricUnit, MetricResolution and other data classes need to be implement manually
    void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution);

    default void addMetric(String key, double value, MetricUnit unit) {
        addMetric(key, value, unit, MetricResolution.DEFAULT);
    }

    default void addMetric(String key, double value) {
        addMetric(key, value, MetricUnit.NONE, MetricResolution.DEFAULT);
    }

    void addDimension(String key, String value);

    void addMetadata(String key, Object value);

    void setDefaultDimensions(Map<String, String> defaultDimensions);

    void setNamespace(String namespace);

    void setService(String service);

    void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics);

    void setCaptureColdStart(boolean captureColdStart);

    void setFunctionName(String functionName);

    void clearDefaultDimensions();

    void flush();

    void captureColdStartMetric(Context context);
}

Acknowledgment

  • Should this be considered in other Powertools for AWS Lambda languages? i.e. Python, TypeScript, and .NET

Activity

self-assigned this
on May 19, 2025
moved this from Triage to Backlog in Powertools for AWS Lambda (Java)on May 19, 2025
moved this from Backlog to Working on it in Powertools for AWS Lambda (Java)on Jun 2, 2025
phipag

phipag commented on Jun 2, 2025

@phipag
ContributorAuthor

Started work in phipag/issue1848 branch. Created initial implementation.

Next steps:

  • Re-write unit tests
  • Update documentation
  • Make sure that all functionality is available via functional and annotation-based programming paradigm
phipag

phipag commented on Jun 4, 2025

@phipag
ContributorAuthor

Added unit tests and updated documentation. All functionality is working with @Metrics annotation as a nice to have extra of this implementation.

phipag

phipag commented on Jun 4, 2025

@phipag
ContributorAuthor

We need to re-generate GraalVM metadata files now using the re-written unit tests.

phipag

phipag commented on Jun 4, 2025

@phipag
ContributorAuthor

Updated the GRM files and tested successfully in my AWS account. I had to refactor some unit tests because mocking an Interface is not supported when running unit tests against a GraalVM native image.

moved this from Working on it to Pending review in Powertools for AWS Lambda (Java)on Jun 4, 2025
phipag

phipag commented on Jun 4, 2025

@phipag
ContributorAuthor

Created PR #1863

Next steps after review of PR:

  • If implementation is approved -> Create upgrade guide for Metrics module
phipag

phipag commented on Jun 5, 2025

@phipag
ContributorAuthor

Final interface:

package software.amazon.lambda.powertools.metrics;

import com.amazonaws.services.lambda.runtime.Context;
import java.time.Instant;

import software.amazon.lambda.powertools.metrics.model.DimensionSet;
import software.amazon.lambda.powertools.metrics.model.MetricResolution;
import software.amazon.lambda.powertools.metrics.model.MetricUnit;

/**
 * Interface for metrics implementations.
 * This interface is used to collect metrics in the Lambda function.
 * It provides methods to add metrics, dimensions, and metadata.
 */
public interface Metrics {

    /**
     * Add a metric
     *
     * @param key        the name of the metric
     * @param value      the value of the metric
     * @param unit       the unit of the metric
     * @param resolution the resolution of the metric
     */
    void addMetric(String key, double value, MetricUnit unit, MetricResolution resolution);

    /**
     * Add a metric with default resolution
     *
     * @param key   the name of the metric
     * @param value the value of the metric
     * @param unit  the unit of the metric
     */
    default void addMetric(String key, double value, MetricUnit unit) {
        addMetric(key, value, unit, MetricResolution.STANDARD);
    }

    /**
     * Add a metric with default unit and resolution
     *
     * @param key   the name of the metric
     * @param value the value of the metric
     */
    default void addMetric(String key, double value) {
        addMetric(key, value, MetricUnit.NONE, MetricResolution.STANDARD);
    }

    /**
     * Add a dimension
     * This is equivalent to calling {@code addDimension(DimensionSet.of(key, value))}
     *
     * @param key   the name of the dimension
     * @param value the value of the dimension
     */
    default void addDimension(String key, String value) {
        addDimension(DimensionSet.of(key, value));
    }

    /**
     * Add a dimension set
     *
     * @param dimensionSet the dimension set to add
     */
    void addDimension(DimensionSet dimensionSet);

    /**
     * Set a custom timestamp for the metrics
     *
     * @param timestamp the timestamp to use for the metrics
     */
    void setTimestamp(Instant timestamp);

    /**
     * Add metadata
     *
     * @param key   the name of the metadata
     * @param value the value of the metadata
     */
    void addMetadata(String key, Object value);

    /**
     * Set default dimensions
     *
     * @param dimensionSet the dimension set to use as default dimensions
     */
    void setDefaultDimensions(DimensionSet dimensionSet);

    /**
     * Get the default dimensions
     *
     * @return the default dimensions as a DimensionSet
     */
    DimensionSet getDefaultDimensions();

    /**
     * Set the namespace
     *
     * @param namespace the namespace
     */
    void setNamespace(String namespace);

    /**
     * Set whether to raise an exception if no metrics are emitted
     *
     * @param raiseOnEmptyMetrics true to raise an exception, false otherwise
     */
    void setRaiseOnEmptyMetrics(boolean raiseOnEmptyMetrics);

    /**
     * Clear default dimensions
     */
    void clearDefaultDimensions();

    /**
     * Flush metrics to the configured sink
     */
    void flush();

    /**
     * Capture cold start metric and flush immediately
     *
     * @param context Lambda context
     * @param dimensions custom dimensions for this metric (optional)
     */
    void captureColdStartMetric(Context context, DimensionSet dimensions);

    /**
     * Capture cold start metric and flush immediately
     *
     * @param context Lambda context
     */
    default void captureColdStartMetric(Context context) {
        captureColdStartMetric(context, null);
    }

    /**
     * Capture cold start metric without Lambda context and flush immediately
     *
     * @param dimensions custom dimensions for this metric (optional)
     */
    void captureColdStartMetric(DimensionSet dimensions);

    /**
     * Capture cold start metric without Lambda context and flush immediately
     */
    default void captureColdStartMetric() {
        captureColdStartMetric((DimensionSet) null);
    }

    /**
     * Flush a single metric with custom dimensions. This creates a separate metrics context
     * that doesn't affect the default metrics context.
     *
     * @param name       the name of the metric
     * @param value      the value of the metric
     * @param unit       the unit of the metric
     * @param namespace  the namespace for the metric
     * @param dimensions custom dimensions for this metric (optional)
     */
    void flushSingleMetric(String name, double value, MetricUnit unit, String namespace, DimensionSet dimensions);

    /**
     * Flush a single metric with custom dimensions. This creates a separate metrics context
     * that doesn't affect the default metrics context.
     *
     * @param name       the name of the metric
     * @param value      the value of the metric
     * @param unit       the unit of the metric
     * @param namespace  the namespace for the metric
     */
    default void flushSingleMetric(String name, double value, MetricUnit unit, String namespace) {
        flushSingleMetric(name, value, unit, namespace, null);
    }
}
phipag

phipag commented on Jun 6, 2025

@phipag
ContributorAuthor

Merged PR. Now I need to update the upgrade guide documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Pending review

Milestone

No milestone

Relationships

None yet

    Participants

    @phipag@dreamorosi

    Issue actions

      Tech debt: Create abstraction for Metrics and do not expose aws-embedded-metrics-java · Issue #1848 · aws-powertools/powertools-lambda-java