diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java index 5d3fa8c5f..028d81131 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java @@ -101,6 +101,7 @@ private void init() { putContext("fmt.Errorf", SmithyGoDependency.FMT.func("Errorf")); putContext("errors.As", SmithyGoDependency.ERRORS.func("As")); putContext("context.Context", SmithyGoDependency.CONTEXT.func("Context")); + putContext("time.Now", SmithyGoDependency.TIME.func("Now")); if (!innerWriter) { packageDocs = new GoWriter(this.fullPackageName, true); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java index 8c2104293..b228f4cfe 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java @@ -26,7 +26,6 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; -import software.amazon.smithy.aws.traits.ServiceTrait; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.go.codegen.auth.AuthSchemeResolverGenerator; import software.amazon.smithy.go.codegen.auth.GetIdentityMiddlewareGenerator; @@ -38,6 +37,7 @@ import software.amazon.smithy.go.codegen.integration.ClientMemberResolver; import software.amazon.smithy.go.codegen.integration.ConfigFieldResolver; import software.amazon.smithy.go.codegen.integration.GoIntegration; +import software.amazon.smithy.go.codegen.integration.OperationMetricsStruct; import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.ServiceIndex; @@ -95,7 +95,7 @@ public void run() { private GoWriter.Writable generate() { return GoWriter.ChainWritable.of( generateMetadata(), - generateObservabilityHelpers(), + generateObservabilityComponents(), generateClient(), generateNew(), generateGetOptions(), @@ -105,8 +105,10 @@ private GoWriter.Writable generate() { ).compose(); } - private GoWriter.Writable generateObservabilityHelpers() { + private GoWriter.Writable generateObservabilityComponents() { return goTemplate(""" + $operationMetrics:W + func operationTracer(p $tracerProvider:T) $tracer:T { return p.Tracer($scope:S) } @@ -114,7 +116,8 @@ func operationTracer(p $tracerProvider:T) $tracer:T { Map.of( "tracerProvider", SmithyGoDependency.SMITHY_TRACING.interfaceSymbol("TracerProvider"), "tracer", SmithyGoDependency.SMITHY_TRACING.interfaceSymbol("Tracer"), - "scope", settings.getModuleName() + "scope", settings.getModuleName(), + "operationMetrics", new OperationMetricsStruct(settings.getModuleName()) )); } @@ -339,8 +342,16 @@ func resolveAuthSchemes(options *Options) { @SuppressWarnings("checkstyle:LineLength") private GoWriter.Writable generateInvokeOperation() { return goTemplate(""" - func (c *Client) invokeOperation(ctx $context.Context:T, opID string, params interface{}, optFns []func(*Options), stackFns ...func($stack:P, Options) error) (result interface{}, metadata $metadata:T, err error) { - ctx = $clearStackValues:T(ctx) + $middleware:D $tracing:D + func (c *Client) invokeOperation( + ctx context.Context, opID string, params interface{}, optFns []func(*Options), stackFns ...func(*middleware.Stack, Options) error, + ) ( + result interface{}, metadata middleware.Metadata, err error, + ) { + ctx = middleware.ClearStackValues(ctx) + ctx = middleware.WithServiceID(ctx, ServiceID) + ctx = middleware.WithOperationName(ctx, opID) + $newStack:W options := c.options.Copy() $resolvers:W @@ -363,24 +374,30 @@ private GoWriter.Writable generateInvokeOperation() { } } + ctx, err = withOperationMetrics(ctx, options.MeterProvider) + if err != nil { + return nil, metadata, err + } + tracer := operationTracer(options.TracerProvider) - spanName := $fmt.Sprintf:T("$tracingServiceId:L.%s", opID) + spanName := fmt.Sprintf("%s.%s", ServiceID, opID) - ctx = $withOperationTracer:T(ctx, tracer) + ctx = tracing.WithOperationTracer(ctx, tracer) - ctx, span := tracer.StartSpan(ctx, spanName, func (o $spanOptions:P) { - o.Kind = $spanKindClient:T + ctx, span := tracer.StartSpan(ctx, spanName, func (o *tracing.SpanOptions) { + o.Kind = tracing.SpanKindClient o.Properties.Set("rpc.system", "aws-api") o.Properties.Set("rpc.method", opID) - o.Properties.Set("rpc.service", $tracingServiceId:S) + o.Properties.Set("rpc.service", ServiceID) }) + defer startMetricTimer(ctx, "client.call.duration")() defer span.End() handler := $newClientHandler:T(options.HTTPClient) - decorated := $decorateHandler:T(handler, stack) + decorated := middleware.DecorateHandler(handler, stack) result, metadata, err = decorated.Handle(ctx, params) if err != nil { - span.SetProperty("error.go.type", $fmt.Sprintf:T("%T", err)) + span.SetProperty("error.go.type", fmt.Sprintf("%T", err)) span.SetProperty("error.go.error", err.Error()) var aerr smithy.APIError @@ -399,18 +416,17 @@ private GoWriter.Writable generateInvokeOperation() { span.SetProperty("error", err != nil) if err == nil { - span.SetStatus($spanStatusOK:T) + span.SetStatus(tracing.SpanStatusOK) } else { - span.SetStatus($spanStatusError:T) + span.SetStatus(tracing.SpanStatusError) } return result, metadata, err } """, MapUtils.of( - "stack", SmithyGoTypes.Middleware.Stack, - "metadata", SmithyGoTypes.Middleware.Metadata, - "clearStackValues", SmithyGoTypes.Middleware.ClearStackValues, + "middleware", SmithyGoDependency.SMITHY_MIDDLEWARE, + "tracing", SmithyGoDependency.SMITHY_TRACING, "newStack", generateNewStack(), "operationError", SmithyGoTypes.Smithy.OperationError, "resolvers", GoWriter.ChainWritable.of( @@ -425,16 +441,7 @@ private GoWriter.Writable generateInvokeOperation() { ConfigFieldResolver.Target.FINALIZATION ).map(this::generateConfigFieldResolver).toList() ).compose(), - "newClientHandler", SmithyGoDependency.SMITHY_HTTP_TRANSPORT.func("NewClientHandler"), - "decorateHandler", SmithyGoDependency.SMITHY_MIDDLEWARE.func("DecorateHandler") - ), - Map.of( - "tracingServiceId", getTracingServiceId(), - "spanOptions", SmithyGoDependency.SMITHY_TRACING.struct("SpanOptions"), - "spanKindClient", SmithyGoDependency.SMITHY_TRACING.constSymbol("SpanKindClient"), - "withOperationTracer", SmithyGoDependency.SMITHY_TRACING.constSymbol("WithOperationTracer"), - "spanStatusOK", SmithyGoDependency.SMITHY_TRACING.constSymbol("SpanStatusOK"), - "spanStatusError", SmithyGoDependency.SMITHY_TRACING.constSymbol("SpanStatusError") + "newClientHandler", SmithyGoDependency.SMITHY_HTTP_TRANSPORT.func("NewClientHandler") )); } @@ -497,10 +504,4 @@ func addProtocolFinalizerMiddlewares(stack $P, options $L, operation string) err SignRequestMiddlewareGenerator.generateAddToProtocolFinalizers() ).compose(false)); } - - private String getTracingServiceId() { - return service.hasTrait(ServiceTrait.class) - ? service.expectTrait(ServiceTrait.class).getSdkId().replaceAll("\\s", "") - : service.getId().getName(); - } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoDependency.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoDependency.java index 4477cbd93..8f873575f 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoDependency.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SmithyGoDependency.java @@ -77,6 +77,7 @@ public final class SmithyGoDependency { public static final GoDependency SMITHY_ENDPOINTS = smithy("endpoints", "smithyendpoints"); public static final GoDependency SMITHY_ENDPOINT_RULESFN = smithy("endpoints/private/rulesfn"); public static final GoDependency SMITHY_TRACING = smithy("tracing"); + public static final GoDependency SMITHY_METRICS = smithy("metrics"); public static final GoDependency GO_JMESPATH = goJmespath(null); public static final GoDependency MATH = stdlib("math"); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/GetIdentityMiddlewareGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/GetIdentityMiddlewareGenerator.java index 19b9d8828..8dca07542 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/GetIdentityMiddlewareGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/GetIdentityMiddlewareGenerator.java @@ -78,7 +78,13 @@ private GoWriter.Writable generateBody() { return out, metadata, $fmt.Errorf:T("no identity resolver") } - identity, err := resolver.GetIdentity(innerCtx, rscheme.IdentityProperties) + identity, err := timeOperationMetric(ctx, "client.call.resolve_identity_duration", + func() ($identity:T, error) { + return resolver.GetIdentity(innerCtx, rscheme.IdentityProperties) + }, + func (o $recordMetricOptions:P) { + o.Properties.Set("auth.scheme_id", rscheme.Scheme.SchemeID()) + }) if err != nil { return out, metadata, $fmt.Errorf:T("get identity: %w", err) } @@ -89,7 +95,9 @@ private GoWriter.Writable generateBody() { return next.HandleFinalize(ctx, in) """, MapUtils.of( - "startSpan", SmithyGoDependency.SMITHY_TRACING.func("StartSpan") + "startSpan", SmithyGoDependency.SMITHY_TRACING.func("StartSpan"), + "identity", SmithyGoDependency.SMITHY_AUTH.interfaceSymbol("Identity"), + "recordMetricOptions", SmithyGoDependency.SMITHY_METRICS.struct("RecordMetricOptions") )); } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/SignRequestMiddlewareGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/SignRequestMiddlewareGenerator.java index e6f44a427..9378ccffa 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/SignRequestMiddlewareGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/auth/SignRequestMiddlewareGenerator.java @@ -86,7 +86,12 @@ private GoWriter.Writable generateBody() { return out, metadata, $fmt.Errorf:T("no signer") } - if err := signer.SignRequest(ctx, req, identity, rscheme.SignerProperties); err != nil { + _, err = timeOperationMetric(ctx, "client.call.signing_duration", func() (any, error) { + return nil, signer.SignRequest(ctx, req, identity, rscheme.SignerProperties) + }, func(o $recordMetricOptions:P) { + o.Properties.Set("auth.scheme_id", rscheme.Scheme.SchemeID()) + }) + if err != nil { return out, metadata, $fmt.Errorf:T("sign request: %w", err) } @@ -96,7 +101,8 @@ private GoWriter.Writable generateBody() { MapUtils.of( // FUTURE(#458) protocol generator should specify the transport type "request", SmithyGoTypes.Transport.Http.Request, - "startSpan", SmithyGoDependency.SMITHY_TRACING.func("StartSpan") + "startSpan", SmithyGoDependency.SMITHY_TRACING.func("StartSpan"), + "recordMetricOptions", SmithyGoDependency.SMITHY_METRICS.struct("RecordMetricOptions") )); } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointMiddlewareGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointMiddlewareGenerator.java index 68fc06d40..bc54b06d1 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointMiddlewareGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/endpoints/EndpointMiddlewareGenerator.java @@ -133,9 +133,12 @@ private GoWriter.Writable generateAssertResolver() { private GoWriter.Writable generateResolveEndpoint() { return goTemplate(""" params := bindEndpointParams(ctx, getOperationInput(ctx), m.options) - endpt, err := m.options.EndpointResolverV2.ResolveEndpoint(ctx, *params) + endpt, err := timeOperationMetric(ctx, "client.call.resolve_endpoint_duration", + func() (smithyendpoints.Endpoint, error) { + return m.options.EndpointResolverV2.ResolveEndpoint(ctx, *params) + }) if err != nil { - return out, metadata, $1T("failed to resolve service endpoint, %w", err) + return out, metadata, $fmt.Errorf:T("failed to resolve service endpoint, %w", err) } span.SetProperty("operation.resolved_endpoint", endpt.URI.String()) @@ -145,13 +148,12 @@ private GoWriter.Writable generateResolveEndpoint() { } req.URL.Scheme = endpt.URI.Scheme req.URL.Host = endpt.URI.Host - req.URL.Path = $2T(endpt.URI.Path, req.URL.Path) - req.URL.RawPath = $2T(endpt.URI.RawPath, req.URL.RawPath) + req.URL.Path = $1T(endpt.URI.Path, req.URL.Path) + req.URL.RawPath = $1T(endpt.URI.RawPath, req.URL.RawPath) for k := range endpt.Headers { req.Header.Set(k, endpt.Headers.Get(k)) } """, - GoStdlibTypes.Fmt.Errorf, SmithyGoTypes.Transport.Http.JoinPath); } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java index ca2025716..ecbe5e150 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpBindingProtocolGenerator.java @@ -254,6 +254,8 @@ private void generateOperationSerializerMiddleware(GenerationContext context, Op writer.write(goTemplate(""" _, span := $T(ctx, "OperationSerializer") + endTimer := startMetricTimer(ctx, "client.call.serialization_duration") + defer endTimer() defer span.End() """, SMITHY_TRACING.func("StartSpan"))); @@ -358,6 +360,7 @@ private void generateOperationSerializerMiddleware(GenerationContext context, Op writer.write("in.Request = request"); writer.write(""); + writer.write("endTimer()"); writer.write("span.End()"); writer.write("return next.$L(ctx, in)", generator.getHandleMethodName()); }); @@ -395,6 +398,7 @@ private void generateOperationDeserializerMiddleware(GenerationContext context, writer.write(goTemplate(""" _, span := $T(ctx, "OperationDeserializer") + defer startMetricTimer(ctx, "client.call.deserialization_duration")() defer span.End() """, SMITHY_TRACING.func("StartSpan"))); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpRpcProtocolGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpRpcProtocolGenerator.java index 44b36e8b7..c8413feb0 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpRpcProtocolGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/HttpRpcProtocolGenerator.java @@ -149,6 +149,8 @@ private void generateOperationSerializer(GenerationContext context, OperationSha writer.write(goTemplate(""" _, span := $T(ctx, "OperationSerializer") + endTimer := startMetricTimer(ctx, "client.call.serialization_duration") + defer endTimer() defer span.End() """, SMITHY_TRACING.func("StartSpan"))); @@ -220,6 +222,7 @@ private void generateOperationSerializer(GenerationContext context, OperationSha writer.write("in.Request = request"); writer.write(""); + writer.write("endTimer()"); writer.write("span.End()"); writer.write("return next.$L(ctx, in)", generator.getHandleMethodName()); }); @@ -341,6 +344,7 @@ private void generateOperationDeserializer(GenerationContext context, OperationS writer.write(goTemplate(""" _, span := $T(ctx, "OperationDeserializer") + defer startMetricTimer(ctx, "client.call.deserialization_duration")() defer span.End() """, SMITHY_TRACING.func("StartSpan"))); @@ -378,7 +382,6 @@ private void generateOperationDeserializer(GenerationContext context, OperationS } writer.write(""); - writer.write("span.End()"); writer.write("return out, metadata, err"); }); writer.write(""); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ObservabilityOptions.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ObservabilityOptions.java index 0bc055584..99166c62e 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ObservabilityOptions.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ObservabilityOptions.java @@ -32,25 +32,32 @@ public class ObservabilityOptions implements GoIntegration { .documentation("The client tracer provider.") .build(); + private static final ConfigField METER_PROVIDER = ConfigField.builder() + .name("MeterProvider") + .type(SmithyGoDependency.SMITHY_METRICS.interfaceSymbol("MeterProvider")) + .documentation("The client meter provider.") + .build(); + private static final ConfigFieldResolver RESOLVE_TRACER_PROVIDER = ConfigFieldResolver.builder() .resolver(buildPackageSymbol("resolveTracerProvider")) .location(ConfigFieldResolver.Location.CLIENT) .target(ConfigFieldResolver.Target.INITIALIZATION) .build(); - // TODO - // private static final ConfigField METER_PROVIDER = ConfigField.builder() - // .name("MeterProvider") - // .type(SmithyGoDependency.SMITHY_METRICS.interfaceSymbol("MeterProvider")) - // .documentation("The client meter provider.") - // .build(); + private static final ConfigFieldResolver RESOLVE_METER_PROVIDER = ConfigFieldResolver.builder() + .resolver(buildPackageSymbol("resolveMeterProvider")) + .location(ConfigFieldResolver.Location.CLIENT) + .target(ConfigFieldResolver.Target.INITIALIZATION) + .build(); @Override public List getClientPlugins() { return List.of( RuntimeClientPlugin.builder() .addConfigField(TRACER_PROVIDER) + .addConfigField(METER_PROVIDER) .addConfigFieldResolver(RESOLVE_TRACER_PROVIDER) + .addConfigFieldResolver(RESOLVE_METER_PROVIDER) .build() ); } @@ -63,6 +70,15 @@ func resolveTracerProvider(options *Options) { options.TracerProvider = &$T{} } } - """, SmithyGoDependency.SMITHY_TRACING.struct("NopTracerProvider"))); + + func resolveMeterProvider(options *Options) { + if options.MeterProvider == nil { + options.MeterProvider = $T{} + } + } + """, + SmithyGoDependency.SMITHY_TRACING.struct("NopTracerProvider"), + SmithyGoDependency.SMITHY_METRICS.struct("NopMeterProvider") + )); } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/OperationMetricsStruct.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/OperationMetricsStruct.java new file mode 100644 index 000000000..6690ba80f --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/OperationMetricsStruct.java @@ -0,0 +1,213 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package software.amazon.smithy.go.codegen.integration; + +import static software.amazon.smithy.go.codegen.GoWriter.goTemplate; +import static software.amazon.smithy.go.codegen.SmithyGoDependency.CONTEXT; +import static software.amazon.smithy.go.codegen.SmithyGoDependency.SMITHY_METRICS; +import static software.amazon.smithy.go.codegen.SmithyGoDependency.SMITHY_MIDDLEWARE; +import static software.amazon.smithy.go.codegen.SmithyGoDependency.TIME; + +import software.amazon.smithy.go.codegen.GoWriter; + +/** + * Writable operationMetrics structure that records operation-specific metrics. + */ +public class OperationMetricsStruct implements GoWriter.Writable { + private final String scope; + + public OperationMetricsStruct(String scope) { + this.scope = scope; + } + + @Override + public void accept(GoWriter writer) { + writer.write(GoWriter.ChainWritable.of( + useDependencies(), generateStruct(), generateHelpers(), generateContextApis() + ).compose()); + } + + private GoWriter.Writable useDependencies() { + return writer -> writer + .addUseImports(CONTEXT) + .addUseImports(TIME) + .addUseImports(SMITHY_METRICS) + .addUseImports(SMITHY_MIDDLEWARE); + } + + private GoWriter.Writable generateStruct() { + return goTemplate(""" + type operationMetrics struct { + Attempts metrics.Int64Counter + Errors metrics.Int64Counter + + Duration metrics.Float64Histogram + AttemptDuration metrics.Float64Histogram + SerializeDuration metrics.Float64Histogram + GetIdentityDuration metrics.Float64Histogram + ResolveEndpointDuration metrics.Float64Histogram + SignRequestDuration metrics.Float64Histogram + DeserializeDuration metrics.Float64Histogram + } + """); + } + + @SuppressWarnings({"checkstyle:LineLength"}) + private GoWriter.Writable generateContextApis() { + return goTemplate(""" + type operationMetricsKey struct{} + + func withOperationMetrics(parent context.Context, mp metrics.MeterProvider) (context.Context, error) { + meter := mp.Meter($S) + om := &operationMetrics{} + + var err error + + om.Attempts, err = meter.Int64Counter("client.call.attempts", func(o *metrics.InstrumentOptions) { + o.UnitLabel = "{attempt}" + o.Description = "The number of attempts for an individual operation" + }) + if err != nil { + return nil, err + } + om.Errors, err = meter.Int64Counter("client.call.errors", func(o *metrics.InstrumentOptions) { + o.UnitLabel = "{error}" + o.Description = "The number of errors for an operation" + }) + if err != nil { + return nil, err + } + + om.Duration, err = operationMetricTimer(meter, "client.call.duration", + "Overall call duration (including retries and time to send or receive request and response body)") + if err != nil { + return nil, err + } + om.AttemptDuration, err = operationMetricTimer(meter, "client.call.attempt_duration", + "The time it takes to connect to the service, send the request, and get back HTTP status code and headers (including time queued waiting to be sent)") + if err != nil { + return nil, err + } + om.SerializeDuration, err = operationMetricTimer(meter, "client.call.serialization_duration", + "The time it takes to serialize a message body") + if err != nil { + return nil, err + } + om.GetIdentityDuration, err = operationMetricTimer(meter, "client.call.auth.resolve_identity_duration", + "The time taken to acquire an identity (AWS credentials, bearer token, etc) from an Identity Provider") + if err != nil { + return nil, err + } + om.ResolveEndpointDuration, err = operationMetricTimer(meter, "client.call.resolve_endpoint_duration", + "The time it takes to resolve an endpoint (endpoint resolver, not DNS) for the request") + if err != nil { + return nil, err + } + om.SignRequestDuration, err = operationMetricTimer(meter, "client.call.auth.signing_duration", + "The time it takes to sign a request") + if err != nil { + return nil, err + } + om.DeserializeDuration, err = operationMetricTimer(meter, "client.call.deserialization_duration", + "The time it takes to deserialize a message body") + if err != nil { + return nil, err + } + + return context.WithValue(parent, operationMetricsKey{}, om), nil + } + + func operationMetricTimer(m metrics.Meter, name, desc string) (metrics.Float64Histogram, error) { + return m.Float64Histogram(name, func(o *metrics.InstrumentOptions) { + o.UnitLabel = "s" + o.Description = desc + }) + } + + func getOperationMetrics(ctx context.Context) *operationMetrics { + return ctx.Value(operationMetricsKey{}).(*operationMetrics) + } + """, scope); + } + + private GoWriter.Writable generateHelpers() { + return goTemplate(""" + func (m *operationMetrics) histogramFor(name string) metrics.Float64Histogram { + switch name { + case "client.call.duration": + return m.Duration + case "client.call.attempt_duration": + return m.AttemptDuration + case "client.call.serialization_duration": + return m.SerializeDuration + case "client.call.resolve_identity_duration": + return m.GetIdentityDuration + case "client.call.resolve_endpoint_duration": + return m.ResolveEndpointDuration + case "client.call.signing_duration": + return m.SignRequestDuration + case "client.call.deserialization_duration": + return m.DeserializeDuration + default: + panic("unrecognized operation metric") + } + } + + func timeOperationMetric[T any]( + ctx context.Context, metric string, fn func() (T, error), + opts ...metrics.RecordMetricOption, + ) (T, error) { + instr := getOperationMetrics(ctx).histogramFor(metric) + opts = append([]metrics.RecordMetricOption{withOperationMetadata(ctx)}, opts...) + + start := time.Now() + v, err := fn() + end := time.Now() + + elapsed := end.Sub(start) + instr.Record(ctx, float64(elapsed)/1e9, opts...) + return v, err + } + + func startMetricTimer(ctx context.Context, metric string, opts ...metrics.RecordMetricOption) func() { + instr := getOperationMetrics(ctx).histogramFor(metric) + opts = append([]metrics.RecordMetricOption{withOperationMetadata(ctx)}, opts...) + + var ended bool + start := time.Now() + return func() { + if ended { + return + } + ended = true + + end := time.Now() + + elapsed := end.Sub(start) + instr.Record(ctx, float64(elapsed)/1e9, opts...) + } + } + + func withOperationMetadata(ctx context.Context) metrics.RecordMetricOption { + return func(o *metrics.RecordMetricOptions) { + o.Properties.Set("rpc.service", middleware.GetServiceID(ctx)) + o.Properties.Set("rpc.method", middleware.GetOperationName(ctx)) + } + } + """); + } +} diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 000000000..25cae5262 --- /dev/null +++ b/metrics/metrics.go @@ -0,0 +1,136 @@ +// Package tracing defines the metrics APIs used by Smithy clients. +package metrics + +import ( + "context" + + "github.com/aws/smithy-go" +) + +// MeterProvider is the entry point for creating a Meter. +type MeterProvider interface { + Meter(scope string, opts ...MeterOption) Meter +} + +// MeterOption applies configuration to a Meter. +type MeterOption func(o *MeterOptions) + +// MeterOptions represents configuration for a Meter. +type MeterOptions struct { + Properties smithy.Properties +} + +// Meter is the entry point for creation of measurement instruments. +type Meter interface { + // integer/synchronous + Int64Counter(name string, opts ...InstrumentOption) (Int64Counter, error) + Int64UpDownCounter(name string, opts ...InstrumentOption) (Int64UpDownCounter, error) + Int64Gauge(name string, opts ...InstrumentOption) (Int64Gauge, error) + Int64Histogram(name string, opts ...InstrumentOption) (Int64Histogram, error) + + // integer/asynchronous + Int64AsyncCounter(name string, callback Int64Callback, opts ...InstrumentOption) (AsyncInstrument, error) + Int64AsyncUpDownCounter(name string, callback Int64Callback, opts ...InstrumentOption) (AsyncInstrument, error) + Int64AsyncGauge(name string, callback Int64Callback, opts ...InstrumentOption) (AsyncInstrument, error) + + // floating-point/synchronous + Float64Counter(name string, opts ...InstrumentOption) (Float64Counter, error) + Float64UpDownCounter(name string, opts ...InstrumentOption) (Float64UpDownCounter, error) + Float64Gauge(name string, opts ...InstrumentOption) (Float64Gauge, error) + Float64Histogram(name string, opts ...InstrumentOption) (Float64Histogram, error) + + // floating-point/asynchronous + Float64AsyncCounter(name string, callback Float64Callback, opts ...InstrumentOption) (AsyncInstrument, error) + Float64AsyncUpDownCounter(name string, callback Float64Callback, opts ...InstrumentOption) (AsyncInstrument, error) + Float64AsyncGauge(name string, callback Float64Callback, opts ...InstrumentOption) (AsyncInstrument, error) +} + +// InstrumentOption applies configuration to an instrument. +type InstrumentOption func(o *InstrumentOptions) + +// InstrumentOptions represents configuration for an instrument. +type InstrumentOptions struct { + UnitLabel string + Description string +} + +// Int64Counter measures a monotonically increasing int64 value. +type Int64Counter interface { + Add(context.Context, int64, ...RecordMetricOption) +} + +// Int64UpDownCounter measures a fluctuating int64 value. +type Int64UpDownCounter interface { + Add(context.Context, int64, ...RecordMetricOption) +} + +// Int64Gauge samples a discrete int64 value. +type Int64Gauge interface { + Sample(context.Context, int64, ...RecordMetricOption) +} + +// Int64Histogram records multiple data points for an int64 value. +type Int64Histogram interface { + Record(context.Context, int64, ...RecordMetricOption) +} + +// Float64Counter measures a monotonically increasing float64 value. +type Float64Counter interface { + Add(context.Context, float64, ...RecordMetricOption) +} + +// Float64UpDownCounter measures a fluctuating float64 value. +type Float64UpDownCounter interface { + Add(context.Context, float64, ...RecordMetricOption) +} + +// Float64Gauge samples a discrete float64 value. +type Float64Gauge interface { + Sample(context.Context, float64, ...RecordMetricOption) +} + +// Float64Histogram records multiple data points for an float64 value. +type Float64Histogram interface { + Record(context.Context, float64, ...RecordMetricOption) +} + +// AsyncInstrument is the universal handle returned for creation of all async +// instruments. +// +// Callers use the Stop() API to unregister the callback passed at instrument +// creation. +type AsyncInstrument interface { + Stop() +} + +// Int64Callback describes a function invoked when an async int64 instrument is +// read. +type Int64Callback func(context.Context, Int64Observer) + +// Int64Observer is the interface passed to async int64 instruments. +// +// Callers use the Observe() API of this interface to report metrics to the +// underlying collector. +type Int64Observer interface { + Observe(context.Context, int64, ...RecordMetricOption) +} + +// Float64Callback describes a function invoked when an async float64 +// instrument is read. +type Float64Callback func(context.Context, Float64Observer) + +// Float64Observer is the interface passed to async int64 instruments. +// +// Callers use the Observe() API of this interface to report metrics to the +// underlying collector. +type Float64Observer interface { + Observe(context.Context, float64, ...RecordMetricOption) +} + +// RecordMetricOption applies configuration to a recorded metric. +type RecordMetricOption func(o *RecordMetricOptions) + +// RecordOption represents configuration for a recorded metric. +type RecordMetricOptions struct { + Properties smithy.Properties +} diff --git a/metrics/nop.go b/metrics/nop.go new file mode 100644 index 000000000..fb374e1fb --- /dev/null +++ b/metrics/nop.go @@ -0,0 +1,67 @@ +package metrics + +import "context" + +// NopMeterProvider is a no-op metrics implementation. +type NopMeterProvider struct{} + +var _ MeterProvider = (*NopMeterProvider)(nil) + +// Meter returns a meter which creates no-op instruments. +func (NopMeterProvider) Meter(string, ...MeterOption) Meter { + return nopMeter{} +} + +type nopMeter struct{} + +var _ Meter = (*nopMeter)(nil) + +func (nopMeter) Int64Counter(string, ...InstrumentOption) (Int64Counter, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Int64UpDownCounter(string, ...InstrumentOption) (Int64UpDownCounter, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Int64Gauge(string, ...InstrumentOption) (Int64Gauge, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Int64Histogram(string, ...InstrumentOption) (Int64Histogram, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Int64AsyncCounter(string, Int64Callback, ...InstrumentOption) (AsyncInstrument, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Int64AsyncUpDownCounter(string, Int64Callback, ...InstrumentOption) (AsyncInstrument, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Int64AsyncGauge(string, Int64Callback, ...InstrumentOption) (AsyncInstrument, error) { + return nopInstrument[int64]{}, nil +} +func (nopMeter) Float64Counter(string, ...InstrumentOption) (Float64Counter, error) { + return nopInstrument[float64]{}, nil +} +func (nopMeter) Float64UpDownCounter(string, ...InstrumentOption) (Float64UpDownCounter, error) { + return nopInstrument[float64]{}, nil +} +func (nopMeter) Float64Gauge(string, ...InstrumentOption) (Float64Gauge, error) { + return nopInstrument[float64]{}, nil +} +func (nopMeter) Float64Histogram(string, ...InstrumentOption) (Float64Histogram, error) { + return nopInstrument[float64]{}, nil +} +func (nopMeter) Float64AsyncCounter(string, Float64Callback, ...InstrumentOption) (AsyncInstrument, error) { + return nopInstrument[float64]{}, nil +} +func (nopMeter) Float64AsyncUpDownCounter(string, Float64Callback, ...InstrumentOption) (AsyncInstrument, error) { + return nopInstrument[float64]{}, nil +} +func (nopMeter) Float64AsyncGauge(string, Float64Callback, ...InstrumentOption) (AsyncInstrument, error) { + return nopInstrument[float64]{}, nil +} + +type nopInstrument[N any] struct{} + +func (nopInstrument[N]) Add(context.Context, N, ...RecordMetricOption) {} +func (nopInstrument[N]) Sample(context.Context, N, ...RecordMetricOption) {} +func (nopInstrument[N]) Record(context.Context, N, ...RecordMetricOption) {} +func (nopInstrument[_]) Stop() {} diff --git a/metrics/smithy-otel-metrics/async.go b/metrics/smithy-otel-metrics/async.go new file mode 100644 index 000000000..4b15a51d4 --- /dev/null +++ b/metrics/smithy-otel-metrics/async.go @@ -0,0 +1,62 @@ +package smithyotelmetrics + +import ( + "context" + + "github.com/aws/smithy-go/metrics" + otelmetric "go.opentelemetry.io/otel/metric" +) + +type asyncInstrument struct { + otel otelmetric.Registration +} + +var _ metrics.AsyncInstrument = (*asyncInstrument)(nil) + +func (i *asyncInstrument) Stop() { + i.otel.Unregister() +} + +// int64Observer wraps an untyped, multi-instrument OTEL Observer to Observe() +// against a single int64 instrument. +type int64Observer struct { + observer otelmetric.Observer + instrument otelmetric.Int64Observable +} + +var _ metrics.Int64Observer = (*int64Observer)(nil) + +func (o *int64Observer) Observe(ctx context.Context, v int64, opts ...metrics.RecordMetricOption) { + o.observer.ObserveInt64(o.instrument, v, withMetricProps(opts...)) +} + +// adaptInt64CB wraps an OTEL async instrument callback, binding it to a single +// int64 instrument. +func adaptInt64CB(io otelmetric.Int64Observable, cb metrics.Int64Callback) otelmetric.Callback { + return func(ctx context.Context, o otelmetric.Observer) error { + cb(ctx, &int64Observer{o, io}) + return nil + } +} + +// float64Observer wraps an untyped, multi-instrument OTEL Observer to Observe() +// against a single float64 instrument. +type float64Observer struct { + observer otelmetric.Observer + instrument otelmetric.Float64Observable +} + +var _ metrics.Float64Observer = (*float64Observer)(nil) + +func (o *float64Observer) Observe(ctx context.Context, v float64, opts ...metrics.RecordMetricOption) { + o.observer.ObserveFloat64(o.instrument, v, withMetricProps(opts...)) +} + +// adaptFloat64CB wraps an OTEL async instrument callback, binding it to a single +// float64 instrument. +func adaptFloat64CB(io otelmetric.Float64Observable, cb metrics.Float64Callback) otelmetric.Callback { + return func(ctx context.Context, o otelmetric.Observer) error { + cb(ctx, &float64Observer{o, io}) + return nil + } +} diff --git a/metrics/smithy-otel-metrics/attribute.go b/metrics/smithy-otel-metrics/attribute.go new file mode 100644 index 000000000..abacc40e6 --- /dev/null +++ b/metrics/smithy-otel-metrics/attribute.go @@ -0,0 +1,67 @@ +package smithyotelmetrics + +import ( + "fmt" + + "github.com/aws/smithy-go" + otelattribute "go.opentelemetry.io/otel/attribute" +) + +// IMPORTANT: The contents of this file are mirrored in +// smithyoteltracing/attribute.go. Any changes made here must be replicated in +// that module's copy of the file, although that will probably never happen, as +// the set of attribute types supported by the OTEL API cannot reasonably +// expand to include anything else that would be useful. +// +// This is done in order to avoid the one-way door of exposing an internal-only +// module for what is effectively a simple value mapper (that will likely never +// change). +// +// While the contents of the file are mirrored, the tests are only present +// in the other version. + +func toOTELKeyValue(k, v any) otelattribute.KeyValue { + kk := str(k) + + switch vv := v.(type) { + case bool: + return otelattribute.Bool(kk, vv) + case []bool: + return otelattribute.BoolSlice(kk, vv) + case int: + return otelattribute.Int(kk, vv) + case []int: + return otelattribute.IntSlice(kk, vv) + case int64: + return otelattribute.Int64(kk, vv) + case []int64: + return otelattribute.Int64Slice(kk, vv) + case float64: + return otelattribute.Float64(kk, vv) + case []float64: + return otelattribute.Float64Slice(kk, vv) + case string: + return otelattribute.String(kk, vv) + case []string: + return otelattribute.StringSlice(kk, vv) + default: + return otelattribute.String(kk, str(v)) + } +} + +func toOTELKeyValues(props smithy.Properties) []otelattribute.KeyValue { + var kvs []otelattribute.KeyValue + for k, v := range props.Values() { + kvs = append(kvs, toOTELKeyValue(k, v)) + } + return kvs +} + +func str(v any) string { + if s, ok := v.(string); ok { + return s + } else if s, ok := v.(fmt.Stringer); ok { + return s.String() + } + return fmt.Sprintf("%#v", v) +} diff --git a/metrics/smithy-otel-metrics/float64.go b/metrics/smithy-otel-metrics/float64.go new file mode 100644 index 000000000..696d69674 --- /dev/null +++ b/metrics/smithy-otel-metrics/float64.go @@ -0,0 +1,43 @@ +package smithyotelmetrics + +import ( + "context" + + "github.com/aws/smithy-go/metrics" + otelmetric "go.opentelemetry.io/otel/metric" +) + +type otelFloat64Add interface { + Add(context.Context, float64, ...otelmetric.AddOption) +} + +type float64Counter struct { + otel otelFloat64Add +} + +var _ metrics.Float64Counter = (*float64Counter)(nil) +var _ metrics.Float64UpDownCounter = (*float64Counter)(nil) + +func (i *float64Counter) Add(ctx context.Context, v float64, opts ...metrics.RecordMetricOption) { + i.otel.Add(ctx, v, withMetricProps(opts...)) +} + +type float64Gauge struct { + otel otelmetric.Float64Gauge +} + +var _ metrics.Float64Gauge = (*float64Gauge)(nil) + +func (i *float64Gauge) Sample(ctx context.Context, v float64, opts ...metrics.RecordMetricOption) { + i.otel.Record(ctx, v, withMetricProps(opts...)) +} + +type float64Histogram struct { + otel otelmetric.Float64Histogram +} + +var _ metrics.Float64Histogram = (*float64Histogram)(nil) + +func (i *float64Histogram) Record(ctx context.Context, v float64, opts ...metrics.RecordMetricOption) { + i.otel.Record(ctx, v, withMetricProps(opts...)) +} diff --git a/metrics/smithy-otel-metrics/go.mod b/metrics/smithy-otel-metrics/go.mod new file mode 100644 index 000000000..6fedbf02d --- /dev/null +++ b/metrics/smithy-otel-metrics/go.mod @@ -0,0 +1,11 @@ +module github.com/aws/smithy-go/metrics/smithy-otel-metrics + +go 1.22 + +require ( + github.com/aws/smithy-go v1.20.4 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/metric v1.29.0 +) + +replace github.com/aws/smithy-go => ../../ diff --git a/metrics/smithy-otel-metrics/go.sum b/metrics/smithy-otel-metrics/go.sum new file mode 100644 index 000000000..6296a714c --- /dev/null +++ b/metrics/smithy-otel-metrics/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/metrics/smithy-otel-metrics/int64.go b/metrics/smithy-otel-metrics/int64.go new file mode 100644 index 000000000..ca0783126 --- /dev/null +++ b/metrics/smithy-otel-metrics/int64.go @@ -0,0 +1,41 @@ +package smithyotelmetrics + +import ( + "context" + + "github.com/aws/smithy-go/metrics" + otelmetric "go.opentelemetry.io/otel/metric" +) + +type int64Counter struct { + otel interface { + Add(context.Context, int64, ...otelmetric.AddOption) + } +} + +var _ metrics.Int64Counter = (*int64Counter)(nil) +var _ metrics.Int64UpDownCounter = (*int64Counter)(nil) + +func (i *int64Counter) Add(ctx context.Context, v int64, opts ...metrics.RecordMetricOption) { + i.otel.Add(ctx, v, withMetricProps(opts...)) +} + +type int64Gauge struct { + otel otelmetric.Int64Gauge +} + +var _ metrics.Int64Gauge = (*int64Gauge)(nil) + +func (i *int64Gauge) Sample(ctx context.Context, v int64, opts ...metrics.RecordMetricOption) { + i.otel.Record(ctx, v, withMetricProps(opts...)) +} + +type int64Histogram struct { + otel otelmetric.Int64Histogram +} + +var _ metrics.Int64Histogram = (*int64Histogram)(nil) + +func (i *int64Histogram) Record(ctx context.Context, v int64, opts ...metrics.RecordMetricOption) { + i.otel.Record(ctx, v, withMetricProps(opts...)) +} diff --git a/metrics/smithy-otel-metrics/metrics.go b/metrics/smithy-otel-metrics/metrics.go new file mode 100644 index 000000000..650bc5a2f --- /dev/null +++ b/metrics/smithy-otel-metrics/metrics.go @@ -0,0 +1,188 @@ +package smithyotelmetrics + +import ( + "github.com/aws/smithy-go/metrics" + otelmetric "go.opentelemetry.io/otel/metric" +) + +// Adapt wraps a concrete OpenTelemetry SDK MeterProvider for use with Smithy +// SDK clients. +// +// Adapt can be called multiple times on a single MeterProvider. +func Adapt(mp otelmetric.MeterProvider) metrics.MeterProvider { + return &meterProvider{mp} +} + +type meterProvider struct { + otel otelmetric.MeterProvider +} + +var _ metrics.MeterProvider = (*meterProvider)(nil) + +func (p *meterProvider) Meter(scope string, opts ...metrics.MeterOption) metrics.Meter { + var options metrics.MeterOptions + for _, opt := range opts { + opt(&options) + } + + m := p.otel.Meter(scope, otelmetric.WithInstrumentationAttributes( + toOTELKeyValues(options.Properties)..., + )) + return &meter{m} +} + +type meter struct { + otel otelmetric.Meter +} + +var _ metrics.Meter = (*meter)(nil) + +func (m *meter) Int64Counter(name string, opts ...metrics.InstrumentOption) (metrics.Int64Counter, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64Counter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &int64Counter{i}, nil +} + +func (m *meter) Int64UpDownCounter(name string, opts ...metrics.InstrumentOption) (metrics.Int64UpDownCounter, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64UpDownCounter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &int64Counter{i}, nil +} + +func (m *meter) Int64Gauge(name string, opts ...metrics.InstrumentOption) (metrics.Int64Gauge, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64Gauge(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &int64Gauge{i}, nil +} + +func (m *meter) Int64Histogram(name string, opts ...metrics.InstrumentOption) (metrics.Int64Histogram, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64Histogram(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &int64Histogram{i}, nil +} + +func (m *meter) Int64AsyncCounter(name string, callback metrics.Int64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64ObservableCounter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + + return m.registerAsyncInt64(i, callback) +} + +func (m *meter) Int64AsyncUpDownCounter(name string, callback metrics.Int64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64ObservableUpDownCounter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + + return m.registerAsyncInt64(i, callback) +} + +func (m *meter) Int64AsyncGauge(name string, callback metrics.Int64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Int64ObservableGauge(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + + return m.registerAsyncInt64(i, callback) +} + +func (m *meter) Float64Counter(name string, opts ...metrics.InstrumentOption) (metrics.Float64Counter, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64Counter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &float64Counter{i}, nil +} + +func (m *meter) Float64UpDownCounter(name string, opts ...metrics.InstrumentOption) (metrics.Float64UpDownCounter, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64UpDownCounter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &float64Counter{i}, nil +} + +func (m *meter) Float64Gauge(name string, opts ...metrics.InstrumentOption) (metrics.Float64Gauge, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64Gauge(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &float64Gauge{i}, nil +} + +func (m *meter) Float64Histogram(name string, opts ...metrics.InstrumentOption) (metrics.Float64Histogram, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64Histogram(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + return &float64Histogram{i}, nil +} + +func (m *meter) Float64AsyncCounter(name string, callback metrics.Float64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64ObservableCounter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + + return m.registerAsyncFloat64(i, callback) +} + +func (m *meter) Float64AsyncUpDownCounter(name string, callback metrics.Float64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64ObservableUpDownCounter(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + + return m.registerAsyncFloat64(i, callback) +} + +func (m *meter) Float64AsyncGauge(name string, callback metrics.Float64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) { + unit, desc := toInstrumentOpts(opts...) + i, err := m.otel.Float64ObservableGauge(name, otelmetric.WithUnit(unit), otelmetric.WithDescription(desc)) + if err != nil { + return nil, err + } + + return m.registerAsyncFloat64(i, callback) +} + +func (m *meter) registerAsyncInt64(i otelmetric.Int64Observable, cb metrics.Int64Callback) (metrics.AsyncInstrument, error) { + r, err := m.otel.RegisterCallback(adaptInt64CB(i, cb), i) + if err != nil { + return nil, err + } + + return &asyncInstrument{r}, nil +} + +func (m *meter) registerAsyncFloat64(i otelmetric.Float64Observable, cb metrics.Float64Callback) (metrics.AsyncInstrument, error) { + r, err := m.otel.RegisterCallback(adaptFloat64CB(i, cb), i) + if err != nil { + return nil, err + } + + return &asyncInstrument{r}, nil +} diff --git a/metrics/smithy-otel-metrics/option.go b/metrics/smithy-otel-metrics/option.go new file mode 100644 index 000000000..2ec977dde --- /dev/null +++ b/metrics/smithy-otel-metrics/option.go @@ -0,0 +1,23 @@ +package smithyotelmetrics + +import ( + "github.com/aws/smithy-go/metrics" + otelmetric "go.opentelemetry.io/otel/metric" +) + +func toInstrumentOpts(opts ...metrics.InstrumentOption) (unit, desc string) { + var o metrics.InstrumentOptions + for _, opt := range opts { + opt(&o) + } + return o.UnitLabel, o.Description +} + +func withMetricProps(opts ...metrics.RecordMetricOption) otelmetric.MeasurementOption { + var o metrics.RecordMetricOptions + for _, opt := range opts { + opt(&o) + } + return otelmetric.WithAttributes(toOTELKeyValues(o.Properties)...) + +} diff --git a/middleware/context.go b/middleware/context.go new file mode 100644 index 000000000..f51aa4f04 --- /dev/null +++ b/middleware/context.go @@ -0,0 +1,41 @@ +package middleware + +import "context" + +type ( + serviceIDKey struct{} + operationNameKey struct{} +) + +// WithServiceID adds a service ID to the context, scoped to middleware stack +// values. +// +// This API is called in the client runtime when bootstrapping an operation and +// should not typically be used directly. +func WithServiceID(parent context.Context, id string) context.Context { + return WithStackValue(parent, serviceIDKey{}, id) +} + +// GetServiceID retrieves the service ID from the context. This is typically +// the service shape's name from its Smithy model. Service clients for specific +// systems (e.g. AWS SDK) may use an alternate designated value. +func GetServiceID(ctx context.Context) string { + id, _ := GetStackValue(ctx, serviceIDKey{}).(string) + return id +} + +// WithOperationName adds the operation name to the context, scoped to +// middleware stack values. +// +// This API is called in the client runtime when bootstrapping an operation and +// should not typically be used directly. +func WithOperationName(parent context.Context, id string) context.Context { + return WithStackValue(parent, operationNameKey{}, id) +} + +// GetOperationName retrieves the operation name from the context. This is +// typically the operation shape's name from its Smithy model. +func GetOperationName(ctx context.Context) string { + name, _ := GetStackValue(ctx, operationNameKey{}).(string) + return name +} diff --git a/tracing/smithy-otel-tracing/adapt.go b/tracing/smithy-otel-tracing/adapt.go index cc26598e0..9058f0167 100644 --- a/tracing/smithy-otel-tracing/adapt.go +++ b/tracing/smithy-otel-tracing/adapt.go @@ -2,11 +2,8 @@ package smithyoteltracing import ( "context" - "fmt" - "github.com/aws/smithy-go" "github.com/aws/smithy-go/tracing" - otelattribute "go.opentelemetry.io/otel/attribute" otelcodes "go.opentelemetry.io/otel/codes" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -141,49 +138,3 @@ func toOTELSpanStatus(v tracing.SpanStatus) otelcodes.Code { return otelcodes.Unset } } - -func toOTELKeyValue(k, v any) otelattribute.KeyValue { - kk := str(k) - - switch vv := v.(type) { - case bool: - return otelattribute.Bool(kk, vv) - case []bool: - return otelattribute.BoolSlice(kk, vv) - case int: - return otelattribute.Int(kk, vv) - case []int: - return otelattribute.IntSlice(kk, vv) - case int64: - return otelattribute.Int64(kk, vv) - case []int64: - return otelattribute.Int64Slice(kk, vv) - case float64: - return otelattribute.Float64(kk, vv) - case []float64: - return otelattribute.Float64Slice(kk, vv) - case string: - return otelattribute.String(kk, vv) - case []string: - return otelattribute.StringSlice(kk, vv) - default: - return otelattribute.String(kk, str(v)) - } -} - -func toOTELKeyValues(props smithy.Properties) []otelattribute.KeyValue { - var kvs []otelattribute.KeyValue - for k, v := range props.Values() { - kvs = append(kvs, toOTELKeyValue(k, v)) - } - return kvs -} - -func str(v any) string { - if s, ok := v.(string); ok { - return s - } else if s, ok := v.(fmt.Stringer); ok { - return s.String() - } - return fmt.Sprintf("%#v", v) -} diff --git a/tracing/smithy-otel-tracing/adapt_test.go b/tracing/smithy-otel-tracing/adapt_test.go index 5ac24dacc..293806d1e 100644 --- a/tracing/smithy-otel-tracing/adapt_test.go +++ b/tracing/smithy-otel-tracing/adapt_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/aws/smithy-go/tracing" - otelattribute "go.opentelemetry.io/otel/attribute" otelcodes "go.opentelemetry.io/otel/codes" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -51,41 +50,3 @@ func TestToOTELSpanStatus(t *testing.T) { }) } } - -type stringer struct{} - -func (s stringer) String() string { - return "stringer" -} - -type notstringer struct{} - -func TestToOTELKeyValue(t *testing.T) { - for _, tt := range []struct { - K, V any - Expect otelattribute.KeyValue - }{ - {1, "asdf", otelattribute.String("1", "asdf")}, // non-string key - {"key", stringer{}, otelattribute.String("key", "stringer")}, // stringer - // unsupported value type - {"key", notstringer{}, otelattribute.String("key", "smithyoteltracing.notstringer{}")}, - {"key", true, otelattribute.Bool("key", true)}, - {"key", []bool{true, false}, otelattribute.BoolSlice("key", []bool{true, false})}, - {"key", int(1), otelattribute.Int("key", 1)}, - {"key", []int{1, 2}, otelattribute.IntSlice("key", []int{1, 2})}, - {"key", int64(1), otelattribute.Int64("key", 1)}, - {"key", []int64{1, 2}, otelattribute.Int64Slice("key", []int64{1, 2})}, - {"key", float64(1), otelattribute.Float64("key", 1)}, - {"key", []float64{1, 2}, otelattribute.Float64Slice("key", []float64{1, 2})}, - {"key", "value", otelattribute.String("key", "value")}, - {"key", []string{"v1", "v2"}, otelattribute.StringSlice("key", []string{"v1", "v2"})}, - } { - name := fmt.Sprintf("(%v, %v) -> %v", tt.K, tt.V, tt.Expect) - t.Run(name, func(t *testing.T) { - actual := toOTELKeyValue(tt.K, tt.V) - if tt.Expect != actual { - t.Errorf("%v != %v", tt.Expect, actual) - } - }) - } -} diff --git a/tracing/smithy-otel-tracing/attribute.go b/tracing/smithy-otel-tracing/attribute.go new file mode 100644 index 000000000..b51b81a56 --- /dev/null +++ b/tracing/smithy-otel-tracing/attribute.go @@ -0,0 +1,67 @@ +package smithyoteltracing + +import ( + "fmt" + + "github.com/aws/smithy-go" + otelattribute "go.opentelemetry.io/otel/attribute" +) + +// IMPORTANT: The contents of this file are mirrored in +// smithyotelmetrics/attribute.go. Any changes made here must be replicated in +// that module's copy of the file, although that will probably never happen, as +// the set of attribute types supported by the OTEL API cannot reasonably +// expand to include anything else that would be useful. +// +// This is done in order to avoid the one-way door of exposing an internal-only +// module for what is effectively a simple value mapper (that will likely never +// change). +// +// While the contents of the file are mirrored, the tests are only present +// here. + +func toOTELKeyValue(k, v any) otelattribute.KeyValue { + kk := str(k) + + switch vv := v.(type) { + case bool: + return otelattribute.Bool(kk, vv) + case []bool: + return otelattribute.BoolSlice(kk, vv) + case int: + return otelattribute.Int(kk, vv) + case []int: + return otelattribute.IntSlice(kk, vv) + case int64: + return otelattribute.Int64(kk, vv) + case []int64: + return otelattribute.Int64Slice(kk, vv) + case float64: + return otelattribute.Float64(kk, vv) + case []float64: + return otelattribute.Float64Slice(kk, vv) + case string: + return otelattribute.String(kk, vv) + case []string: + return otelattribute.StringSlice(kk, vv) + default: + return otelattribute.String(kk, str(v)) + } +} + +func toOTELKeyValues(props smithy.Properties) []otelattribute.KeyValue { + var kvs []otelattribute.KeyValue + for k, v := range props.Values() { + kvs = append(kvs, toOTELKeyValue(k, v)) + } + return kvs +} + +func str(v any) string { + if s, ok := v.(string); ok { + return s + } else if s, ok := v.(fmt.Stringer); ok { + return s.String() + } + return fmt.Sprintf("%#v", v) +} diff --git a/tracing/smithy-otel-tracing/attribute_test.go b/tracing/smithy-otel-tracing/attribute_test.go new file mode 100644 index 000000000..0b36abbc3 --- /dev/null +++ b/tracing/smithy-otel-tracing/attribute_test.go @@ -0,0 +1,46 @@ +package smithyoteltracing + +import ( + "fmt" + "testing" + + otelattribute "go.opentelemetry.io/otel/attribute" +) + +type stringer struct{} + +func (s stringer) String() string { + return "stringer" +} + +type notstringer struct{} + +func TestToOTELKeyValue(t *testing.T) { + for _, tt := range []struct { + K, V any + Expect otelattribute.KeyValue + }{ + {1, "asdf", otelattribute.String("1", "asdf")}, // non-string key + {"key", stringer{}, otelattribute.String("key", "stringer")}, // stringer + // unsupported value type + {"key", notstringer{}, otelattribute.String("key", "smithyoteltracing.notstringer{}")}, + {"key", true, otelattribute.Bool("key", true)}, + {"key", []bool{true, false}, otelattribute.BoolSlice("key", []bool{true, false})}, + {"key", int(1), otelattribute.Int("key", 1)}, + {"key", []int{1, 2}, otelattribute.IntSlice("key", []int{1, 2})}, + {"key", int64(1), otelattribute.Int64("key", 1)}, + {"key", []int64{1, 2}, otelattribute.Int64Slice("key", []int64{1, 2})}, + {"key", float64(1), otelattribute.Float64("key", 1)}, + {"key", []float64{1, 2}, otelattribute.Float64Slice("key", []float64{1, 2})}, + {"key", "value", otelattribute.String("key", "value")}, + {"key", []string{"v1", "v2"}, otelattribute.StringSlice("key", []string{"v1", "v2"})}, + } { + name := fmt.Sprintf("(%v, %v) -> %v", tt.K, tt.V, tt.Expect) + t.Run(name, func(t *testing.T) { + actual := toOTELKeyValue(tt.K, tt.V) + if tt.Expect != actual { + t.Errorf("%v != %v", tt.Expect, actual) + } + }) + } +} diff --git a/tracing/smithy-otel-tracing/go.mod b/tracing/smithy-otel-tracing/go.mod index f5e6aa527..dd24ffc7e 100644 --- a/tracing/smithy-otel-tracing/go.mod +++ b/tracing/smithy-otel-tracing/go.mod @@ -4,8 +4,8 @@ go 1.22 require ( github.com/aws/smithy-go v1.20.4 - go.opentelemetry.io/otel v1.28.0 - go.opentelemetry.io/otel/trace v1.28.0 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/trace v1.29.0 ) replace github.com/aws/smithy-go => ../../ diff --git a/tracing/smithy-otel-tracing/go.sum b/tracing/smithy-otel-tracing/go.sum index 390d4f389..117dd580e 100644 --- a/tracing/smithy-otel-tracing/go.sum +++ b/tracing/smithy-otel-tracing/go.sum @@ -6,9 +6,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=