From 388c21e5849501bb0f6e06fd1100fe4d29fffef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Sat, 7 Feb 2026 07:02:40 -0800 Subject: [PATCH 1/8] Add an OpenTelemetry based plugin to publish operation metrics --- client/client-metrics-otel/README.md | 2 + client/client-metrics-otel/build.gradle.kts | 24 ++ .../client/metrics/otel/OperationMetrics.java | 140 +++++++++ .../otel/OperationMetricsInterceptor.java | 279 ++++++++++++++++++ .../metrics/otel/OperationMetricsPlugin.java | 179 +++++++++++ .../otel/OperationMetricsPluginTest.java | 263 +++++++++++++++++ .../smithy/java/core/schema/ApiService.java | 9 + gradle/libs.versions.toml | 6 + settings.gradle.kts | 1 + 9 files changed, 903 insertions(+) create mode 100644 client/client-metrics-otel/README.md create mode 100644 client/client-metrics-otel/build.gradle.kts create mode 100644 client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java create mode 100644 client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java create mode 100644 client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java create mode 100644 client/client-metrics-otel/src/test/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPluginTest.java diff --git a/client/client-metrics-otel/README.md b/client/client-metrics-otel/README.md new file mode 100644 index 000000000..09f916b7d --- /dev/null +++ b/client/client-metrics-otel/README.md @@ -0,0 +1,2 @@ +### client-metrics-otel +Provides a plugin to publish metrics using [OpenTelemetry](https://opentelemetry.io/). The OpenTelemetry should be configured with a provider. See the OpenTelemetry website for examples on how to configure providers. diff --git a/client/client-metrics-otel/build.gradle.kts b/client/client-metrics-otel/build.gradle.kts new file mode 100644 index 000000000..551f8f4e2 --- /dev/null +++ b/client/client-metrics-otel/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("smithy-java.module-conventions") +} + +description = "This module provides the client metrics plugin" + +extra["displayName"] = "Smithy :: Java :: Client :: Metrics" +extra["moduleName"] = "software.amazon.smithy.java.client.metrics.otel" + +dependencies { + api(project(":core")) + api(project(":client:client-core")) + api(project(":http:http-api")) + implementation(libs.opentelemetry.api) + + testImplementation(project(":client:dynamic-client")) + testImplementation(project(":aws:client:aws-client-restjson")) + testImplementation(project(":aws:client:aws-client-core")) + testImplementation(project(":aws:aws-sigv4")) + testImplementation(project(":client:client-mock-plugin")) + testImplementation(project(":aws:sdkv2:aws-sdkv2-auth")) + testImplementation(libs.aws.sdk.auth) + testImplementation(libs.opentelemetry.test.api) +} diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java new file mode 100644 index 000000000..4af28a489 --- /dev/null +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java @@ -0,0 +1,140 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.metrics.otel; + +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; + +/** + * Container for common operation/call metrics. + */ +final class OperationMetrics { + static final String DURATION = "smithy.client.call.duration"; + static final String ATTEMPTS = "smithy.client.call.attempts"; + static final String ERRORS = "smithy.client.call.errors"; + static final String ATTEMPT_DURATION = "smithy.client.call.attempt_duration"; + static final String SERIALIZATION_DURATION = "smithy.client.call.serialization_duration"; + static final String DESERIALIZATION_DURATION = "smithy.client.call.deserialization_duration"; + static final String RESOLVE_ENDPOINT_DURATION = "smithy.client.call.resolve_endpoint_duration"; + static final String REQUEST_PAYLOAD_SIZE = "smithy.client.call.request_payload_size"; + static final String RESPONSE_PAYLOAD_SIZE = "smithy.client.call.response_payload_size"; + static final String AUTH_RESOLVE_IDENTITY_DURATION = "smithy.client.call.auth.resolve_identity_duration"; + static final String AUTH_SIGNING_DURATION = "smithy.client.call.auth.signing_duration"; + + private final DoubleHistogram rpcCallDuration; + private final LongCounter rpcAttempts; + private final LongCounter rpcErrors; + private final DoubleHistogram rpcAttemptDuration; + private final DoubleHistogram serializationDuration; + private final DoubleHistogram deserializationDuration; + private final DoubleHistogram resolveEndpointDuration; + private final DoubleHistogram resolveIdentityDuration; + private final DoubleHistogram signingDuration; + private final DoubleHistogram requestPayloadSize; + private final DoubleHistogram responsePayloadSize; + + /** + * Creates a new operation metrics instance. + * + * @param meter the instruments provider used to record metrics + */ + OperationMetrics(Meter meter) { + this.rpcCallDuration = meter.histogramBuilder(DURATION) + .setUnit("s") + .setDescription("Overall call duration including retries") + .build(); + this.rpcAttempts = meter.counterBuilder(ATTEMPTS) + .setUnit("{attempt}") + .setDescription("The number of attempts for an operation") + .build(); + this.rpcErrors = meter.counterBuilder(ERRORS) + .setUnit("{error}") + .setDescription("The number of errors for an operation") + .build(); + this.rpcAttemptDuration = meter.histogramBuilder(ATTEMPT_DURATION) + .setUnit("s") + .setDescription( + "The time it takes to connect to complete an entire call attempt, including identity resolution, " + + + "endpoint resolution, signing, sending the request, and receiving the HTTP status code " + + + "and headers from the response for an operation") + .build(); + this.serializationDuration = meter.histogramBuilder(SERIALIZATION_DURATION) + .setUnit("s") + .setDescription("The time it takes to serialize a request message body") + .build(); + this.deserializationDuration = meter.histogramBuilder(DESERIALIZATION_DURATION) + .setUnit("s") + .setDescription("The time it takes to deserialize a response message body") + .build(); + this.resolveEndpointDuration = meter.histogramBuilder(RESOLVE_ENDPOINT_DURATION) + .setUnit("s") + .setDescription("The time it takes to resolve an endpoint for a request") + .build(); + this.resolveIdentityDuration = meter.histogramBuilder(AUTH_RESOLVE_IDENTITY_DURATION) + .setUnit("s") + .setDescription("The time it takes to resolve an identity for signing a request") + .build(); + this.signingDuration = meter.histogramBuilder(AUTH_SIGNING_DURATION) + .setUnit("s") + .setDescription("The time it takes to sign a request") + .build(); + this.requestPayloadSize = meter.histogramBuilder(REQUEST_PAYLOAD_SIZE) + .setUnit("bytes") + .setDescription("The payload size of a request") + .build(); + this.responsePayloadSize = meter.histogramBuilder(RESPONSE_PAYLOAD_SIZE) + .setUnit("bytes") + .setDescription("The payload size of a response") + .build(); + } + + DoubleHistogram rpcCallDuration() { + return rpcCallDuration; + } + + LongCounter rpcAttempts() { + return rpcAttempts; + } + + LongCounter rpcErrors() { + return rpcErrors; + } + + DoubleHistogram rpcAttemptDuration() { + return rpcAttemptDuration; + } + + DoubleHistogram serializationDuration() { + return serializationDuration; + } + + DoubleHistogram deserializationDuration() { + return deserializationDuration; + } + + DoubleHistogram resolveEndpointDuration() { + return resolveEndpointDuration; + } + + DoubleHistogram resolveIdentityDuration() { + return resolveIdentityDuration; + } + + DoubleHistogram signingDuration() { + return signingDuration; + } + + DoubleHistogram requestPayloadSize() { + return requestPayloadSize; + } + + DoubleHistogram responsePayloadSize() { + return responsePayloadSize; + } +} diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java new file mode 100644 index 000000000..45f887f5c --- /dev/null +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java @@ -0,0 +1,279 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.metrics.otel; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.ArrayList; +import software.amazon.smithy.java.auth.api.identity.Identity; +import software.amazon.smithy.java.auth.api.identity.IdentityResolver; +import software.amazon.smithy.java.auth.api.identity.IdentityResult; +import software.amazon.smithy.java.client.core.ClientConfig; +import software.amazon.smithy.java.client.core.endpoint.Endpoint; +import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; +import software.amazon.smithy.java.client.core.endpoint.EndpointResolverParams; +import software.amazon.smithy.java.client.core.interceptors.CallHook; +import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor; +import software.amazon.smithy.java.client.core.interceptors.InputHook; +import software.amazon.smithy.java.client.core.interceptors.OutputHook; +import software.amazon.smithy.java.client.core.interceptors.RequestHook; +import software.amazon.smithy.java.client.core.interceptors.ResponseHook; +import software.amazon.smithy.java.context.Context; +import software.amazon.smithy.java.http.api.HttpRequest; +import software.amazon.smithy.java.http.api.HttpResponse; + +/** + * Interceptor to instrument calls to emit metrics using OpenTelemetry. + */ +class OperationMetricsInterceptor implements ClientInterceptor { + static final Context.Key STATE = Context.key("smithy.operation.metrics.state"); + static final AttributeKey RPC_SERVICE = AttributeKey.stringKey("rpc.service"); + static final AttributeKey RPC_METHOD = AttributeKey.stringKey("rpc.method"); + static final AttributeKey EXCEPTION_TYPE = AttributeKey.stringKey("error"); + private final OperationMetrics metrics; + + /** + * Creates a new interceptor using the given metrics containing the needed meters to emit metrics. + * + * @param metrics The container of meters for the metrics to emit. + */ + OperationMetricsInterceptor(OperationMetrics metrics) { + this.metrics = metrics; + } + + @Override + public ClientConfig modifyBeforeCall(CallHook hook) { + var operationName = hook.operation().name(); + var serviceName = hook.operation().service().name(); + var attributes = Attributes.builder() + .put(RPC_SERVICE, serviceName) + .put(RPC_METHOD, operationName) + .build(); + // Instrument endpoint resolver + var endpointResolverDelegate = hook.config().endpointResolver(); + var endpointResolver = new MetricsCollectingEndpointResolver(endpointResolverDelegate, metrics, attributes); + var state = new MetricsState(metrics, attributes); + // Instrument identity resolvers + var identityResolvers = hook.config().identityResolvers(); + var newIdentityResolvers = new ArrayList>(identityResolvers.size()); + for (IdentityResolver identityResolver : identityResolvers) { + newIdentityResolvers.add(new MetricsCollectingIdentityResolver<>(identityResolver, metrics, attributes)); + } + return hook.config() + .toBuilder() + .putConfig(STATE, state) + .endpointResolver(endpointResolver) + .identityResolvers(newIdentityResolvers) + .build(); + } + + @Override + public void readBeforeExecution(InputHook hook) { + var state = hook.context().get(STATE); + state.startCall(); + } + + @Override + public void readAfterExecution(OutputHook hook, RuntimeException error) { + var state = hook.context().get(STATE); + state.endCall(); + } + + @Override + public void readBeforeAttempt(RequestHook hook) { + var state = hook.context().get(STATE); + state.startAttempt(); + } + + @Override + public void readAfterAttempt(OutputHook hook, RuntimeException error) { + var state = hook.context().get(STATE); + state.endAttempt(error); + } + + @Override + public void readBeforeSerialization(InputHook hook) { + var state = hook.context().get(STATE); + state.startSerialize(); + } + + @Override + public void readAfterSerialization(RequestHook hook) { + var state = hook.context().get(STATE); + state.endSerialize(); + } + + @Override + public void readBeforeDeserialization(ResponseHook hook) { + var state = hook.context().get(STATE); + state.startDeserialize(); + } + + @Override + public void readAfterDeserialization(OutputHook hook, RuntimeException error) { + var state = hook.context().get(STATE); + state.endDeserialize(); + } + + @Override + public void readBeforeSigning(RequestHook hook) { + var state = hook.context().get(STATE); + state.startSigning(); + } + + @Override + public void readAfterSigning(RequestHook hook) { + var state = hook.context().get(STATE); + state.endSigning(); + } + + @Override + public void readBeforeTransmit(RequestHook hook) { + if (hook.request() instanceof HttpRequest request) { + var contentLength = request.contentLength(); + if (contentLength != null) { + var state = hook.context().get(STATE); + state.requestPayloadSize(contentLength); + } + } + } + + @Override + public void readAfterTransmit(ResponseHook hook) { + if (hook.response() instanceof HttpResponse response) { + var contentLength = response.contentLength(); + if (contentLength != null) { + var state = hook.context().get(STATE); + state.responsePayloadSize(contentLength); + } + } + } + + static class MetricsCollectingEndpointResolver implements EndpointResolver { + private final EndpointResolver delegate; + private final OperationMetrics metrics; + private final Attributes attributes; + + MetricsCollectingEndpointResolver(EndpointResolver delegate, OperationMetrics metrics, Attributes attributes) { + this.delegate = delegate; + this.metrics = metrics; + this.attributes = attributes; + } + + @Override + public Endpoint resolveEndpoint(EndpointResolverParams params) { + var startNs = System.nanoTime(); + var result = delegate.resolveEndpoint(params); + metrics.resolveEndpointDuration().record(elapsedSecondsSinceNs(startNs), attributes); + return result; + } + } + + static class MetricsCollectingIdentityResolver implements IdentityResolver { + private final IdentityResolver delegate; + private final OperationMetrics metrics; + private final Attributes attributes; + + @SuppressWarnings("unchecked") + MetricsCollectingIdentityResolver( + IdentityResolver delegate, + OperationMetrics metrics, + Attributes attributes + ) { + this.delegate = (IdentityResolver) delegate; + this.metrics = metrics; + this.attributes = attributes; + } + + @Override + public IdentityResult resolveIdentity(Context requestProperties) { + var startNs = System.nanoTime(); + var result = delegate.resolveIdentity(requestProperties); + metrics.resolveIdentityDuration().record(elapsedSecondsSinceNs(startNs), attributes); + return result; + } + + @Override + public Class identityType() { + return delegate.identityType(); + } + } + + static double elapsedSecondsSinceNs(long startNs) { + var elapsedNs = System.nanoTime() - startNs; + return elapsedNs / 1_000_000_000.0; + } + + static class MetricsState { + private final OperationMetrics metrics; + private final Attributes attributes; + private long callStartNs; + private long attemptStartNs; + private long serializeStartNs; + private long deserializeStartNs; + private long signingStartNs; + + MetricsState(OperationMetrics metrics, Attributes attributes) { + this.metrics = metrics; + this.attributes = attributes; + } + + void startCall() { + this.callStartNs = System.nanoTime(); + } + + void endCall() { + metrics.rpcCallDuration().record(elapsedSecondsSinceNs(callStartNs), attributes); + } + + void startAttempt() { + this.attemptStartNs = System.nanoTime(); + } + + void endAttempt(RuntimeException error) { + metrics.rpcAttemptDuration().record(elapsedSecondsSinceNs(attemptStartNs), attributes); + metrics.rpcAttempts().add(1L, attributes); + if (error != null) { + var attributesWithError = attributes.toBuilder() + .put(EXCEPTION_TYPE, error.getClass().getName()) + .build(); + metrics.rpcErrors().add(1L, attributesWithError); + } + } + + void startSerialize() { + serializeStartNs = System.nanoTime(); + } + + void endSerialize() { + metrics.serializationDuration().record(elapsedSecondsSinceNs(serializeStartNs), attributes); + } + + void startDeserialize() { + deserializeStartNs = System.nanoTime(); + } + + void endDeserialize() { + metrics.deserializationDuration().record(elapsedSecondsSinceNs(deserializeStartNs), attributes); + } + + void startSigning() { + signingStartNs = System.nanoTime(); + } + + void endSigning() { + metrics.signingDuration().record(elapsedSecondsSinceNs(signingStartNs), attributes); + } + + void requestPayloadSize(long size) { + metrics.requestPayloadSize().record(size, attributes); + } + + void responsePayloadSize(long size) { + metrics.responsePayloadSize().record(size, attributes); + } + } +} diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java new file mode 100644 index 000000000..1cdbbe4fb --- /dev/null +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java @@ -0,0 +1,179 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.metrics.otel; + +import io.opentelemetry.api.OpenTelemetry; +import software.amazon.smithy.java.client.core.Client; +import software.amazon.smithy.java.client.core.ClientConfig; +import software.amazon.smithy.java.client.core.ClientPlugin; + +/** + * A plugin that instruments the client to emit request/response metrics using + * OpenTelemetry. + * + *

Usage example: OTLP → CloudWatch Agent

+ * + *

The following example shows how to configure OpenTelemetry to publish the metrics to + * + * AWS CloudWatch using the EC2 CloudWatch client.

+ * + *

This is the most common pattern. Your Java app sends metrics via OTLP to a locally running CloudWatch agent.

+ *

+ * {@snippet lang = "java": + * public static OperationMetricsPlugin createPlugin() { + * // Configure the SDK meter provider to use the local EC2 CloudWatch agent + * // Define resource attributes + * Resource resource = Resource.getDefault().toBuilder() + * .put(ResourceAttributes.SERVICE_NAME, "my-java-service") + * .put(ResourceAttributes.SERVICE_VERSION, "1.0.0") + * .put(ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production") + * .build(); + * + * // Create OTLP HTTP exporter pointing to CloudWatch agent + * OtlpHttpMetricExporter metricExporter = OtlpHttpMetricExporter.builder() + * .setEndpoint("http://localhost:4318/v1/metrics") // CloudWatch agent endpoint + * .build(); + * + * // Or use gRPC (port 4317) + * // OtlpGrpcMetricExporter metricExporter = OtlpGrpcMetricExporter.builder() + * // .setEndpoint("http://localhost:4317") + * // .build(); + * + * // Create metric reader with export interval + * PeriodicMetricReader metricReader = PeriodicMetricReader.builder(metricExporter) + * .setInterval(Duration.ofSeconds(60)) + * .build(); + * + * // Create meter provider + * SdkMeterProvider meterProvider = SdkMeterProvider.builder() + * .setResource(resource) + * .registerMetricReader(metricReader) + * .build(); + * + * // Build and register global OpenTelemetry + * OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + * .setMeterProvider(meterProvider) + * .buildAndRegisterGlobal(); + * + * // Create the plugin with the OpenTelemetry instance + * return new OperationMetricsPlugin(openTelemetry); + * } + * + * public static DynamicClient createClient() { + * // Using the plugin with a client. + * return DynamicClient.builder() + * .addPlugin(createPlugin()) + * .build(); + * } + *} + * + *

Metrics published

+ *

This plugin publishes the following metrics for each request

+ * + *
+ *
smithy.client.call.duration (unit: s)
+ *
+ * Overall call duration including retries. + *
+ * + *
smithy.client.call.attempts
+ *
+ * The number of attempts for an operation. + *
+ * + *
smithy.client.call.errors
+ *
+ * The number of errors for an operation. + *
+ * + *
smithy.client.call.attempt_duration (unit: s)
+ *
+ * The time it takes to connect to complete an entire call attempt, including identity resolution, endpoint resolution, signing, sending the request, and receiving the HTTP status code and headers from the response for an operation. + *
+ * + *
smithy.client.call.serialization_duration (unit: s)
+ *
+ * The time it takes to serialize a request message body. + *
+ * + *
smithy.client.call.deserialization_duration (unit: s)
+ *
+ * The time it takes to deserialize a response message body. + *
+ * + *
smithy.client.call.resolve_endpoint_duration (unit: s)
+ *
+ * The time it takes to resolve an endpoint for a request. + *
+ * + *
smithy.client.call.auth.resolve_identity_duration (unit: s)
+ *
+ * The time it takes to resolve an identity for signing a request. + *
+ * + *
smithy.client.call.auth.signing_duration (unit: s)
+ *
+ * The time it takes to sign a request. + *
+ * + *
smithy.client.call.request_payload_size (unit: bytes)
+ *
+ * The payload size of a request. + *
+ * + *
smithy.client.call.response_payload_size (unit: bytes)
+ *
+ * The payload size of a response. + *
+ *
+ * + *

The following attributes are attached to each metric

+ * + *
+ *
rpc.service
+ *
The name of the service
+ *
rpc.method
+ *
The name of the operation being called
+ *
+ * + * Additionally the following attribute is attached to the smithy.client.call.errors metric + * + *
+ *
excepton.type
+ *
The name of the class exception
+ *
+ * + * @see ClientConfig.Builder#addPlugin(ClientPlugin) + * @see Client.Builder#addPlugin(ClientPlugin) + */ +public final class OperationMetricsPlugin implements ClientPlugin { + + private final OperationMetrics operationMetrics; + + /** + * Creates a new operation metrics plugin. + * + * @param openTelemetry The OpenTelemetry instance used to create metrics + * @param scope The scope used to publish metrics. + */ + public OperationMetricsPlugin(OpenTelemetry openTelemetry, String scope) { + this.operationMetrics = new OperationMetrics(openTelemetry.getMeter(scope)); + } + + /** + * Creates a new operation metrics plugin using the default scope
"software.amazon.smithy.java.client"
. + * + * @param openTelemetry The OpenTelemetry instance used to create metrics + */ + public OperationMetricsPlugin(OpenTelemetry openTelemetry) { + this(openTelemetry, "software.amazon.smithy.java.client"); + } + + @Override + public void configureClient(ClientConfig.Builder config) { + config.addInterceptor(new OperationMetricsInterceptor(operationMetrics)); + } +} diff --git a/client/client-metrics-otel/src/test/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPluginTest.java b/client/client-metrics-otel/src/test/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPluginTest.java new file mode 100644 index 000000000..d8076459f --- /dev/null +++ b/client/client-metrics-otel/src/test/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPluginTest.java @@ -0,0 +1,263 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.client.metrics.otel; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static software.amazon.smithy.java.client.metrics.otel.OperationMetricsInterceptor.EXCEPTION_TYPE; +import static software.amazon.smithy.java.client.metrics.otel.OperationMetricsInterceptor.RPC_METHOD; +import static software.amazon.smithy.java.client.metrics.otel.OperationMetricsInterceptor.RPC_SERVICE; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.MetricDataType; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.smithy.java.aws.client.auth.scheme.sigv4.SigV4AuthScheme; +import software.amazon.smithy.java.aws.client.core.settings.RegionSetting; +import software.amazon.smithy.java.aws.client.restjson.RestJsonClientProtocol; +import software.amazon.smithy.java.aws.sdkv2.auth.SdkCredentialsResolver; +import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver; +import software.amazon.smithy.java.client.core.endpoint.EndpointResolver; +import software.amazon.smithy.java.client.http.mock.MockPlugin; +import software.amazon.smithy.java.client.http.mock.MockQueue; +import software.amazon.smithy.java.dynamicclient.DynamicClient; +import software.amazon.smithy.java.http.api.HttpResponse; +import software.amazon.smithy.java.io.datastream.DataStream; +import software.amazon.smithy.java.retries.api.AcquireInitialTokenRequest; +import software.amazon.smithy.java.retries.api.AcquireInitialTokenResponse; +import software.amazon.smithy.java.retries.api.RecordSuccessRequest; +import software.amazon.smithy.java.retries.api.RecordSuccessResponse; +import software.amazon.smithy.java.retries.api.RefreshRetryTokenRequest; +import software.amazon.smithy.java.retries.api.RefreshRetryTokenResponse; +import software.amazon.smithy.java.retries.api.RetryStrategy; +import software.amazon.smithy.java.retries.api.RetryToken; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ShapeId; + +class OperationMetricsPluginTest { + private static final Model MODEL = Model.assembler() + .addUnparsedModel("test.smithy", """ + $version: "2" + namespace smithy.example + + @aws.protocols#restJson1 + @aws.auth#sigv4(name:"sprockets") + service Sprockets { + operations: [GetSprocket] + } + + @http(method: "POST", uri: "/s") + operation GetSprocket { + input := { + id: String + } + output := { + id: String + } + } + """) + .discoverModels() + .assemble() + .unwrap(); + + private static final ShapeId SERVICE = ShapeId.from("smithy.example#Sprockets"); + + private InMemoryMetricReader metricReader; + private OpenTelemetry openTelemetry; + private OperationMetricsPlugin metricsPlugin; + + @BeforeEach + void setUp() { + // Create an in-memory metric reader + metricReader = InMemoryMetricReader.create(); + + // Build the SDK with the in-memory reader + var meterProvider = SdkMeterProvider.builder() + .registerMetricReader(metricReader) + .build(); + + // Create the open telemetry instance with the configured SDK + openTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(meterProvider) + .build(); + + // Create the plugin + metricsPlugin = new OperationMetricsPlugin(openTelemetry); + } + + @Test + public void recordsTheExpectedMetrics() throws URISyntaxException { + // Arrange + var queue = new MockQueue(); + queue.enqueue(HttpResponse.builder() + .body(DataStream.ofString("{\"id\":\"10\"}")) + .statusCode(200) + .build()); + var client = createClient(queue); + + // Act + client.call("GetSprocket", Map.of("id", "10")); + + // Assert + var metrics = metricReader.collectAllMetrics(); + var name2data = metrics.stream() + .collect(Collectors.toMap(MetricData::getName, Function.identity())); + var expectedMetrics = mapOfExpectedMetrics(); + for (var kvp : name2data.entrySet()) { + var expectation = expectedMetrics.get(kvp.getKey()); + assertNotNull(expectation); + assertEquals(expectation.dataType, kvp.getValue().getType()); + assertEquals(expectation.unit, kvp.getValue().getUnit()); + assertAttributes(kvp.getValue()); + } + } + + @Test + public void recordsRetryAttempts() throws URISyntaxException { + // Arrange + var queue = new MockQueue(); + queue.enqueue(HttpResponse.builder() + .body(DataStream.ofString("{\"__type\":\"InvalidSprocketId\"}")) + .statusCode(429) + .build()); + queue.enqueue(HttpResponse.builder() + .body(DataStream.ofString("{\"id\":\"10\"}")) + .statusCode(200) + .build()); + var client = createClient(queue); + + // Act + client.call("GetSprocket", Map.of("id", "10")); + + // Assert + var metrics = metricReader.collectAllMetrics(); + var name2data = metrics.stream() + .collect(Collectors.toMap(MetricData::getName, Function.identity())); + var expectedMetrics = mapOfExpectedMetrics(); + for (var kvp : name2data.entrySet()) { + var expectation = expectedMetrics.get(kvp.getKey()); + assertNotNull(expectation); + assertEquals(expectation.dataType, kvp.getValue().getType(), kvp.getKey()); + assertEquals(expectation.unit, kvp.getValue().getUnit(), kvp.getKey()); + assertAttributes(kvp.getValue()); + } + // assert attempts + var attempts = name2data.get(OperationMetrics.ATTEMPTS); + var attemptsPoints = attempts.getData().getPoints().stream().toList(); + assertEquals(1, attemptsPoints.size()); + var attemptPoint = attemptsPoints.get(0); + assertInstanceOf(LongPointData.class, attemptPoint); + var attemptLongValue = ((LongPointData) attemptPoint).getValue(); + assertEquals(2, attemptLongValue); + // assert errors + var errors = name2data.get(OperationMetrics.ERRORS); + var errorsPoints = errors.getData().getPoints().stream().toList(); + assertEquals(1, errorsPoints.size()); + var errorPoint = errorsPoints.get(0); + assertInstanceOf(LongPointData.class, errorPoint); + var errorLongValue = ((LongPointData) errorPoint).getValue(); + assertEquals(1, errorLongValue); + String value = errorPoint.getAttributes().get(EXCEPTION_TYPE); + assertEquals("software.amazon.smithy.java.core.error.CallException", value); + } + + private void assertAttributes(MetricData data) { + for (var point : data.getData().getPoints()) { + var attributes = point.getAttributes(); + assertEquals("Sprockets", attributes.get(RPC_SERVICE)); + assertEquals("GetSprocket", attributes.get(RPC_METHOD)); + } + } + + DynamicClient createClient(MockQueue queue) throws URISyntaxException { + var credentials = AwsBasicCredentials.create("access_key", "secret_key"); + return DynamicClient.builder() + .model(MODEL) + .serviceId(SERVICE) + .protocol(new RestJsonClientProtocol(SERVICE)) + .retryStrategy(createRetryStrategy()) + .addPlugin(MockPlugin.builder().addQueue(queue).build()) + .addPlugin(metricsPlugin) + .endpointResolver(EndpointResolver.staticEndpoint(new URI("http://localhost"))) + .putConfig(RegionSetting.REGION, "us-west-2") + .putSupportedAuthSchemes(new SigV4AuthScheme("sprockets")) + .authSchemeResolver(AuthSchemeResolver.DEFAULT) + .addIdentityResolver(new SdkCredentialsResolver(StaticCredentialsProvider.create(credentials))) + .build(); + } + + static RetryStrategy createRetryStrategy() { + return new RetryStrategy() { + @Override + public AcquireInitialTokenResponse acquireInitialToken(AcquireInitialTokenRequest request) { + return new AcquireInitialTokenResponse(new Token(), Duration.ZERO); + } + + @Override + public RefreshRetryTokenResponse refreshRetryToken(RefreshRetryTokenRequest request) { + return new RefreshRetryTokenResponse(new Token(), Duration.ZERO); + } + + @Override + public RecordSuccessResponse recordSuccess(RecordSuccessRequest request) { + return new RecordSuccessResponse(request.token()); + } + + @Override + public int maxAttempts() { + return 3; + } + + @Override + public Builder toBuilder() { + throw new UnsupportedOperationException(); + } + }; + } + + static Map mapOfExpectedMetrics() { + return Map.ofEntries( + entry(OperationMetrics.DURATION, new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.ATTEMPTS, new MetricExpectation(MetricDataType.LONG_SUM, "{attempt}")), + entry(OperationMetrics.ATTEMPT_DURATION, new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.SERIALIZATION_DURATION, new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.AUTH_RESOLVE_IDENTITY_DURATION, + new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.DESERIALIZATION_DURATION, new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.RESOLVE_ENDPOINT_DURATION, new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.REQUEST_PAYLOAD_SIZE, new MetricExpectation(MetricDataType.HISTOGRAM, "bytes")), + entry(OperationMetrics.RESPONSE_PAYLOAD_SIZE, new MetricExpectation(MetricDataType.HISTOGRAM, "bytes")), + entry(OperationMetrics.AUTH_SIGNING_DURATION, new MetricExpectation(MetricDataType.HISTOGRAM, "s")), + entry(OperationMetrics.ERRORS, new MetricExpectation(MetricDataType.LONG_SUM, "{error}"))); + } + + static class MetricExpectation { + private final MetricDataType dataType; + private final String unit; + + public MetricExpectation(MetricDataType dataType, String units) { + this.dataType = dataType; + this.unit = units; + } + } + + private static final class Token implements RetryToken {} +} diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java b/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java index 0c5f0b5fb..7fced58ef 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java @@ -15,4 +15,13 @@ public interface ApiService { * @return Returns the service schema, including the shape ID and relevant traits. */ Schema schema(); + + /** + * Name of the Service. + * + * @return Returns the name of the operation. + */ + default String name() { + return schema().id().getName(); + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cf304c6eb..fee39a459 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ graalvm-native = "0.11.3" shadow = "8.3.9" jazzer = "0.29.1" json-schema-validator = "3.0.0" +opentelemetry = "1.58.0" [libraries] smithy-model = { module = "software.amazon.smithy:smithy-model", version.ref = "smithy" } @@ -44,6 +45,11 @@ aws-sdk-retries = {module = "software.amazon.awssdk:retries", version.ref = "aws aws-sdk-core = {module = "software.amazon.awssdk:sdk-core", version.ref = "aws-sdk"} aws-sdk-auth = {module = "software.amazon.awssdk:auth", version.ref = "aws-sdk"} +# OpenTelemetry +opentelemetry-bom = {module = "io.opentelemetry:opentelemetry-bom", version.ref = "opentelemetry"} +opentelemetry-api = {module = "io.opentelemetry:opentelemetry-api", version.ref = "opentelemetry"} +opentelemetry-test-api = {module = "io.opentelemetry:opentelemetry-sdk-testing", version.ref = "opentelemetry"} + jackson-core = {module = "tools.jackson.core:jackson-core", version.ref = "jackson"} netty-all = {module = "io.netty:netty-all", version.ref = "netty"} diff --git a/settings.gradle.kts b/settings.gradle.kts index 562f62f7e..f70659dab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,6 +46,7 @@ include(":client:dynamic-client") include(":client:client-mock-plugin") include(":client:client-waiters") include(":client:client-rulesengine") +include(":client:client-metrics-otel") // Server include(":server:server-api") From f38520a390831d41c3fd96f1258494cc76abf79c Mon Sep 17 00:00:00 2001 From: Manuel Sugawara Date: Tue, 10 Feb 2026 13:15:25 -0800 Subject: [PATCH 2/8] Update client/client-metrics-otel/build.gradle.kts Co-authored-by: Michael Dowling --- client/client-metrics-otel/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client-metrics-otel/build.gradle.kts b/client/client-metrics-otel/build.gradle.kts index 551f8f4e2..3e03491b8 100644 --- a/client/client-metrics-otel/build.gradle.kts +++ b/client/client-metrics-otel/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("smithy-java.module-conventions") } -description = "This module provides the client metrics plugin" +description = "This module provides a client metrics plugin for OpenTelemetry" extra["displayName"] = "Smithy :: Java :: Client :: Metrics" extra["moduleName"] = "software.amazon.smithy.java.client.metrics.otel" From 8111ba8ee423127327faacac369ec606c380655e Mon Sep 17 00:00:00 2001 From: Manuel Sugawara Date: Tue, 10 Feb 2026 13:15:39 -0800 Subject: [PATCH 3/8] Update client/client-metrics-otel/build.gradle.kts Co-authored-by: Michael Dowling --- client/client-metrics-otel/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client-metrics-otel/build.gradle.kts b/client/client-metrics-otel/build.gradle.kts index 3e03491b8..7c249e614 100644 --- a/client/client-metrics-otel/build.gradle.kts +++ b/client/client-metrics-otel/build.gradle.kts @@ -4,7 +4,7 @@ plugins { description = "This module provides a client metrics plugin for OpenTelemetry" -extra["displayName"] = "Smithy :: Java :: Client :: Metrics" +extra["displayName"] = "Smithy :: Java :: Client :: Metrics :: OTel" extra["moduleName"] = "software.amazon.smithy.java.client.metrics.otel" dependencies { From 992a67b1727fb11855ec68596ff4024e705ef4e3 Mon Sep 17 00:00:00 2001 From: Manuel Sugawara Date: Tue, 10 Feb 2026 13:16:19 -0800 Subject: [PATCH 4/8] Update client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java Co-authored-by: Michael Dowling --- .../java/client/metrics/otel/OperationMetricsInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java index 45f887f5c..7e43bb4da 100644 --- a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java @@ -152,7 +152,7 @@ public void readAfterTransmit(ResponseHook hook) { } } - static class MetricsCollectingEndpointResolver implements EndpointResolver { + static final class MetricsCollectingEndpointResolver implements EndpointResolver { private final EndpointResolver delegate; private final OperationMetrics metrics; private final Attributes attributes; From 49b3c9f1156c5b1b7294571e18c88e31b9f7d10f Mon Sep 17 00:00:00 2001 From: Manuel Sugawara Date: Tue, 10 Feb 2026 13:16:29 -0800 Subject: [PATCH 5/8] Update client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java Co-authored-by: Michael Dowling --- .../java/client/metrics/otel/OperationMetricsInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java index 7e43bb4da..3e844caf8 100644 --- a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java @@ -207,7 +207,7 @@ static double elapsedSecondsSinceNs(long startNs) { return elapsedNs / 1_000_000_000.0; } - static class MetricsState { + static final class MetricsState { private final OperationMetrics metrics; private final Attributes attributes; private long callStartNs; From 230ed3821ac47b3fbb95261169451f4e8a92b103 Mon Sep 17 00:00:00 2001 From: Manuel Sugawara Date: Tue, 10 Feb 2026 13:16:48 -0800 Subject: [PATCH 6/8] Update client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java Co-authored-by: Michael Dowling --- .../java/client/metrics/otel/OperationMetricsInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java index 3e844caf8..48d95d845 100644 --- a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsInterceptor.java @@ -172,7 +172,7 @@ public Endpoint resolveEndpoint(EndpointResolverParams params) { } } - static class MetricsCollectingIdentityResolver implements IdentityResolver { + static final class MetricsCollectingIdentityResolver implements IdentityResolver { private final IdentityResolver delegate; private final OperationMetrics metrics; private final Attributes attributes; From 8a0056390f33446672a8dd2b86c2823e83d3c8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Tue, 10 Feb 2026 13:35:28 -0800 Subject: [PATCH 7/8] Address comments --- .../smithy/java/client/metrics/otel/OperationMetrics.java | 4 ++-- .../java/client/metrics/otel/OperationMetricsPlugin.java | 4 ++-- .../software/amazon/smithy/java/core/schema/ApiService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java index 4af28a489..65bbf7750 100644 --- a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java @@ -66,11 +66,11 @@ final class OperationMetrics { .build(); this.serializationDuration = meter.histogramBuilder(SERIALIZATION_DURATION) .setUnit("s") - .setDescription("The time it takes to serialize a request message body") + .setDescription("The time it takes to serialize a requestg") .build(); this.deserializationDuration = meter.histogramBuilder(DESERIALIZATION_DURATION) .setUnit("s") - .setDescription("The time it takes to deserialize a response message body") + .setDescription("The time it takes to deserialize a response") .build(); this.resolveEndpointDuration = meter.histogramBuilder(RESOLVE_ENDPOINT_DURATION) .setUnit("s") diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java index 1cdbbe4fb..f3dee8a74 100644 --- a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetricsPlugin.java @@ -96,12 +96,12 @@ * *
smithy.client.call.serialization_duration (unit: s)
*
- * The time it takes to serialize a request message body. + * The time it takes to serialize a request. *
* *
smithy.client.call.deserialization_duration (unit: s)
*
- * The time it takes to deserialize a response message body. + * The time it takes to deserialize a response. *
* *
smithy.client.call.resolve_endpoint_duration (unit: s)
diff --git a/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java b/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java index 7fced58ef..d2e51eb04 100644 --- a/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java +++ b/core/src/main/java/software/amazon/smithy/java/core/schema/ApiService.java @@ -19,7 +19,7 @@ public interface ApiService { /** * Name of the Service. * - * @return Returns the name of the operation. + * @return Returns the name of the service. */ default String name() { return schema().id().getName(); From e080b7668544f1aefd16066638054650b6adbe66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Tue, 10 Feb 2026 13:40:59 -0800 Subject: [PATCH 8/8] Fix a typo --- .../smithy/java/client/metrics/otel/OperationMetrics.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java index 65bbf7750..8b4234464 100644 --- a/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java +++ b/client/client-metrics-otel/src/main/java/software/amazon/smithy/java/client/metrics/otel/OperationMetrics.java @@ -66,7 +66,7 @@ final class OperationMetrics { .build(); this.serializationDuration = meter.histogramBuilder(SERIALIZATION_DURATION) .setUnit("s") - .setDescription("The time it takes to serialize a requestg") + .setDescription("The time it takes to serialize a request") .build(); this.deserializationDuration = meter.histogramBuilder(DESERIALIZATION_DURATION) .setUnit("s")