From 406cf3707315815cbbf6a2b01196a495da7fd8c0 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Tue, 21 May 2024 14:29:56 -0700 Subject: [PATCH 1/6] added init --- ...S.OpenTelemetry.AutoInstrumentation.csproj | 1 + .../AwsSpanMetricsProcessorBuilder.cs | 2 +- .../Plugin.cs | 147 +++++++++++++++++- 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj index f0d68402..c552aa1b 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj @@ -14,6 +14,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs index 0ce7b3fe..b7de5334 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs @@ -70,7 +70,7 @@ public AwsSpanMetricsProcessorBuilder SetScopeName(string scopeName) { if (scopeName == null) { - throw new ArgumentNullException("generator must not be null", nameof(scopeName)); + throw new ArgumentNullException("scopeName must not be null", nameof(scopeName)); } this.scopeName = scopeName; diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs index f9402211..aa57f83b 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -1,6 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Contrib.Extensions.AWSXRay.Resources; using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -13,6 +17,17 @@ namespace AWS.OpenTelemetry.AutoInstrumentation; /// public class Plugin { + private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddConsole()); + private static readonly ILogger Logger = Factory.CreateLogger(); + private static readonly string APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"; + private static readonly string ApplicationSignalsEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"; + private static readonly string APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"; + private static readonly string ApplicationSignalsExporterEndpointConfig = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"; + private static readonly string MetricExportIntervalConfig = "OTEL_METRIC_EXPORT_INTERVAL"; + private static readonly int DefaultMetricExportInternal = 60000; + private static readonly string OtelTracesSampler = "OTEL_TRACES_SAMPLER"; + private static readonly string OtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"; + /// /// To configure plugin, before OTel SDK configuration is called. /// public void Initializing() @@ -27,7 +42,51 @@ public void Initializing() /// Provider to configure public void TracerProviderInitialized(TracerProvider tracerProvider) { - // My custom logic here + if (this.IsApplicationSignalsEnabled()) + { + tracerProvider.AddProcessor(AttributePropagatingSpanProcessorBuilder.Create().Build()); + + string? intervalConfigString = System.Environment.GetEnvironmentVariable(MetricExportIntervalConfig); + int exportInterval = DefaultMetricExportInternal; + try + { + exportInterval = Convert.ToInt32(intervalConfigString); + } + catch (Exception) + { + Logger.Log(LogLevel.Trace, "Could not convert OTEL_METRIC_EXPORT_INTERVAL to integer. Using default value 60000."); + } + + if (exportInterval.CompareTo(DefaultMetricExportInternal) > 0) + { + exportInterval = DefaultMetricExportInternal; + Logger.Log(LogLevel.Information, "AWS Application Signals metrics export interval capped to {0}", exportInterval); + } + + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md#enable-metric-exporter + // for setting the temporatityPref. + var metricReader = new PeriodicExportingMetricReader(this.ApplicationSignalsExporterProvider(), exportInterval); + MeterProvider provider = Sdk.CreateMeterProviderBuilder() + .AddReader(metricReader) + .ConfigureResource(builder => builder + .AddDetector(new AWSEC2ResourceDetector()) + .AddDetector(new AWSECSResourceDetector()) + .AddDetector(new AWSEKSResourceDetector())) + .AddMeter("AwsSpanMetricsProcessor") + .Build(); + + Resource resource = provider.GetResource(); + BaseProcessor spanMetricsProcessor = AwsSpanMetricsProcessorBuilder.Create(resource).Build(); + tracerProvider.AddProcessor(spanMetricsProcessor); + } + } + + private OtlpMetricExporter ApplicationSignalsExporterProvider() + { + // https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#configuring-the-aggregation-of-a-histogram + // use the above to check about adding aggregation type. + var options = new OtlpExporterOptions(); + return new OtlpMetricExporter(options); } /// @@ -57,10 +116,63 @@ public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder /// Returns configured builder public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder builder) { - // My custom logic here + if (this.IsApplicationSignalsEnabled()) + { + Logger.Log(LogLevel.Information, "AWS Application Signals enabled"); + Sampler alwaysRecordSampler = AlwaysRecordSampler.Create(this.GetSampler()); + builder.SetSampler(alwaysRecordSampler); + } + + builder.AddXRayTraceId(); return builder; } + // This function is based on an internal function in Otel: + // https://github.com/open-telemetry/opentelemetry-dotnet/blob/1bbafaa7b7bed6470ff52fc76b6e881cd19692a5/src/OpenTelemetry/Trace/TracerProviderSdk.cs#L408 + // Unfortunately, that function is private. + private Sampler GetSampler() + { + string? tracesSampler = System.Environment.GetEnvironmentVariable(OtelTracesSampler); + string? tracesSamplerArg = System.Environment.GetEnvironmentVariable(OtelTracesSamplerArg); + double samplerProbability = 1.0; + if (tracesSampler != null) + { + try + { + samplerProbability = Convert.ToDouble(tracesSamplerArg); + } + catch (Exception) + { + Logger.Log(LogLevel.Trace, "Could not convert OTEL_TRACES_SAMPLER_ARG to double. Using default value 1.0."); + } + } + + // based on the list of available samplers: + // https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/77256e3a9666ee0f1f72fec5f4ca1a6d8500f229/docs/config.md#samplers + // Need to also add XRay Sampler here. + // Currently, this is the only way to get the sampler as there is no factory and we can't get the sampler + // that was already set in the TracerProviderBuilder + switch (tracesSampler) + { + case "always_on": + return new AlwaysOnSampler(); + case "always_off": + return new AlwaysOffSampler(); + case "traceidratio": + return new TraceIdRatioBasedSampler(samplerProbability); + case "parentbased_always_off": + Sampler alwaysOffSampler = new AlwaysOffSampler(); + return new ParentBasedSampler(alwaysOffSampler); + case "parentbased_traceidratio": + Sampler traceIdRatioSampler = new TraceIdRatioBasedSampler(samplerProbability); + return new ParentBasedSampler(traceIdRatioSampler); + case "parentbased_always_on": + default: + Sampler alwaysOnSampler = new AlwaysOnSampler(); + return new ParentBasedSampler(alwaysOnSampler); + } + } + /// /// To configure any traces options used by OpenTelemetry .NET Automatic Instrumentation /// We can set the OTLP endpoint configs to point to the cloudwatch agent here as default so that we don't need @@ -68,6 +180,32 @@ public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder /// /// options to configure public void ConfigureTracesOptions(OtlpExporterOptions options) + { + if (this.IsApplicationSignalsEnabled()) + { + Logger.Log( + LogLevel.Debug, "AWS Application Signals export protocol: %{0}", options.Protocol); + + string? applicationSignalsEndpoint = System.Environment.GetEnvironmentVariable(ApplicationSignalsExporterEndpointConfig); + if (options.Protocol == OtlpExportProtocol.HttpProtobuf) + { + applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4316/v1/metrics"; + } + else if (options.Protocol == OtlpExportProtocol.Grpc) + { + applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4315"; + } + else + { + throw new NotSupportedException("Unsupported AWS Application Signals export protocol: " + options.Protocol); + } + + options.Endpoint = new Uri(applicationSignalsEndpoint); + } + } + + // To configure any metrics options used by OpenTelemetry .NET Automatic Instrumentation + public void ConfigureMetricsOptions(MetricReaderOptions options) { // My custom logic here // Find supported options below @@ -120,4 +258,9 @@ public ResourceBuilder ConfigureResource(ResourceBuilder builder) // by calling SetResourceBuilder with new object. return builder; } + + private bool IsApplicationSignalsEnabled() + { + return System.Environment.GetEnvironmentVariable(ApplicationSignalsEnabledConfig) == "true"; + } } From 4ddb206c256ecfd679642a4305ad94f5978bfcd2 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Wed, 22 May 2024 09:30:34 -0700 Subject: [PATCH 2/6] added notes --- .../Plugin.cs | 77 ++++++++----------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs index aa57f83b..14e18930 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Reflection; using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Contrib.Extensions.AWSXRay.Resources; @@ -27,6 +28,7 @@ public class Plugin private static readonly int DefaultMetricExportInternal = 60000; private static readonly string OtelTracesSampler = "OTEL_TRACES_SAMPLER"; private static readonly string OtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"; + private static readonly string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// /// To configure plugin, before OTel SDK configuration is called. @@ -44,6 +46,13 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) { if (this.IsApplicationSignalsEnabled()) { + // add new export processor here. + // https://stackoverflow.com/questions/12993962/set-value-of-private-field + // use reflection to get the internal exporter and set the new modified exporter. + // I need to get the composite processor after SDK init and replace the exporter. + var processor = tracerProvider.GetType().GetProperty("Processor", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(tracerProvider); + var exporter = processor.GetType().GetField("exporter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(processor); + tracerProvider.AddProcessor(AttributePropagatingSpanProcessorBuilder.Create().Build()); string? intervalConfigString = System.Environment.GetEnvironmentVariable(MetricExportIntervalConfig); @@ -86,6 +95,31 @@ private OtlpMetricExporter ApplicationSignalsExporterProvider() // https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#configuring-the-aggregation-of-a-histogram // use the above to check about adding aggregation type. var options = new OtlpExporterOptions(); + + Logger.Log( + LogLevel.Debug, "AWS Application Signals export protocol: %{0}", options.Protocol); + + string? applicationSignalsEndpoint = System.Environment.GetEnvironmentVariable(ApplicationSignalsExporterEndpointConfig); + string? protocolString = System.Environment.GetEnvironmentVariable(DefaultProtocolEnvVarName); + OtlpExportProtocol protocol = OtlpExportProtocol.HttpProtobuf; + if (protocolString == "http/protobuf") + { + applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4316/v1/metrics"; + protocol = OtlpExportProtocol.HttpProtobuf; + } + else if (protocolString == "grpc") + { + applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4315"; + protocol = OtlpExportProtocol.Grpc; + } + else + { + throw new NotSupportedException("Unsupported AWS Application Signals export protocol: " + options.Protocol); + } + + options.Endpoint = new Uri(applicationSignalsEndpoint); + options.Protocol = protocol; + return new OtlpMetricExporter(options); } @@ -173,37 +207,6 @@ private Sampler GetSampler() } } - /// - /// To configure any traces options used by OpenTelemetry .NET Automatic Instrumentation - /// We can set the OTLP endpoint configs to point to the cloudwatch agent here as default so that we don't need - /// to use env vars. - /// - /// options to configure - public void ConfigureTracesOptions(OtlpExporterOptions options) - { - if (this.IsApplicationSignalsEnabled()) - { - Logger.Log( - LogLevel.Debug, "AWS Application Signals export protocol: %{0}", options.Protocol); - - string? applicationSignalsEndpoint = System.Environment.GetEnvironmentVariable(ApplicationSignalsExporterEndpointConfig); - if (options.Protocol == OtlpExportProtocol.HttpProtobuf) - { - applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4316/v1/metrics"; - } - else if (options.Protocol == OtlpExportProtocol.Grpc) - { - applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4315"; - } - else - { - throw new NotSupportedException("Unsupported AWS Application Signals export protocol: " + options.Protocol); - } - - options.Endpoint = new Uri(applicationSignalsEndpoint); - } - } - // To configure any metrics options used by OpenTelemetry .NET Automatic Instrumentation public void ConfigureMetricsOptions(MetricReaderOptions options) { @@ -233,18 +236,6 @@ public MeterProviderBuilder AfterConfigureMeterProvider(MeterProviderBuilder bui return builder; } - /// - /// To configure any metrics options used by OpenTelemetry .NET Automatic Instrumentation - /// We can set the OTLP endpoint configs to point to the cloudwatch agent here as default so that we don't need - /// to use env vars. - /// - /// options to configure - public void ConfigureMetricsOptions(OtlpExporterOptions options) - { - // My custom logic here - // Find supported options below - } - /// /// To configure Resource /// From 9dfc922c6f01c81c68487e5875232f3aea751183 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Fri, 24 May 2024 10:11:52 -0700 Subject: [PATCH 3/6] Tested instrumentation is working as expected --- build/Build.cs | 25 ++- .../integration-test-app/Dockerfile | 5 +- ...S.OpenTelemetry.AutoInstrumentation.csproj | 5 +- .../AwsAttributeKeys.cs | 1 - .../AwsMetricAttributeGenerator.cs | 12 +- .../AwsMetricAttributesSpanProcessor.cs | 125 ++++++++++++ ...AwsMetricAttributesSpanProcessorBuilder.cs | 63 +++++++ .../AwsSpanMetricsProcessor.cs | 8 +- .../AwsSpanProcessingUtil.cs | 36 +++- .../Plugin.cs | 178 +++++++----------- .../AwsSpanProcessingUtilTest.cs | 95 +++++----- 11 files changed, 375 insertions(+), 178 deletions(-) create mode 100644 src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs create mode 100644 src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs diff --git a/build/Build.cs b/build/Build.cs index 6bf3d949..929124d6 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -85,6 +85,7 @@ private static string GetOTelAutoInstrumentationFileName() fileName = "opentelemetry-dotnet-instrumentation-windows.zip"; break; case PlatformFamily.Linux: + case PlatformFamily.OSX: var architecture = RuntimeInformation.ProcessArchitecture; string architectureSuffix; switch (architecture) @@ -103,9 +104,6 @@ private static string GetOTelAutoInstrumentationFileName() ? $"opentelemetry-dotnet-instrumentation-linux-musl-{architectureSuffix}.zip" : $"opentelemetry-dotnet-instrumentation-linux-glibc-{architectureSuffix}.zip"; break; - case PlatformFamily.OSX: - fileName = "opentelemetry-dotnet-instrumentation-macos.zip"; - break; case PlatformFamily.Unknown: throw new NotSupportedException(); default: @@ -124,6 +122,27 @@ private static string GetOTelAutoInstrumentationFileName() "net8.0" / "AWS.OpenTelemetry.AutoInstrumentation.dll", this.openTelemetryDistributionFolder / "net"); + //TODO: fix build script to copy dependencies without manually setting them here. + FileSystemTasks.CopyFileToDirectory( + RootDirectory / "src" / "AWS.OpenTelemetry.AutoInstrumentation" / "bin" / this.configuration / + "net8.0" / "Newtonsoft.Json.dll", + this.openTelemetryDistributionFolder / "net"); + + FileSystemTasks.CopyFileToDirectory( + RootDirectory / "src" / "AWS.OpenTelemetry.AutoInstrumentation" / "bin" / this.configuration / + "net8.0" / "OpenTelemetry.Extensions.AWS.dll", + this.openTelemetryDistributionFolder / "net"); + + FileSystemTasks.CopyFileToDirectory( + RootDirectory / "src" / "AWS.OpenTelemetry.AutoInstrumentation" / "bin" / this.configuration / + "net8.0" / "OpenTelemetry.ResourceDetectors.AWS.dll", + this.openTelemetryDistributionFolder / "net"); + + FileSystemTasks.CopyFileToDirectory( + RootDirectory / "src" / "AWS.OpenTelemetry.AutoInstrumentation" / "bin" / this.configuration / + "net8.0" / "OpenTelemetry.SemanticConventions.dll", + this.openTelemetryDistributionFolder / "net"); + if (EnvironmentInfo.IsWin) { FileSystemTasks.CopyFileToDirectory( diff --git a/sample-applications/integration-test-app/Dockerfile b/sample-applications/integration-test-app/Dockerfile index 2d6223e0..8ec1e7eb 100644 --- a/sample-applications/integration-test-app/Dockerfile +++ b/sample-applications/integration-test-app/Dockerfile @@ -22,4 +22,7 @@ ENV DOTNET_ADDITIONAL_DEPS=${INSTALL_DIR}/AdditionalDeps ENV DOTNET_SHARED_STORE=${INSTALL_DIR}/store ENV DOTNET_STARTUP_HOOKS=${INSTALL_DIR}/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll ENV OTEL_DOTNET_AUTO_HOME=${INSTALL_DIR} -ENV OTEL_DOTNET_AUTO_PLUGINS="AWS.OpenTelemetry.AutoInstrumentation.Plugin, AWS.OpenTelemetry.AutoInstrumentation" \ No newline at end of file +ENV OTEL_DOTNET_AUTO_PLUGINS="AWS.OpenTelemetry.AutoInstrumentation.Plugin, AWS.OpenTelemetry.AutoInstrumentation" +ENV OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true" +ENV OTEL_TRACES_SAMPLER="always_on" +ENV OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj index 857bb09e..5b53171c 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj @@ -10,10 +10,11 @@ + - - + + diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs index 983ebf77..9f270001 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsAttributeKeys.cs @@ -47,7 +47,6 @@ internal sealed class AwsAttributeKeys internal static readonly string AttributeAWSS3Bucket = "aws.s3.bucket"; - internal static readonly string AttributeHttpStatusCode = "http.status_code"; internal static readonly string AttributeHttpResponseContentLength = "http.response_content_length"; internal static readonly string AttributeValueDynamoDb = "dynamodb"; diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs index 4c9a15cb..a705530c 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs @@ -241,9 +241,9 @@ private static void SetRemoteServiceAndOperation(Activity span, ActivityTagsColl private static string GenerateRemoteOperation(Activity span) { string remoteOperation = UnknownRemoteOperation; - if (IsKeyPresent(span, AttributeHttpUrl)) + if (IsKeyPresent(span, AttributeUrlFull)) { - string? httpUrl = (string?)span.GetTagItem(AttributeHttpUrl); + string? httpUrl = (string?)span.GetTagItem(AttributeUrlFull); try { Uri url; @@ -259,9 +259,9 @@ private static string GenerateRemoteOperation(Activity span) } } - if (IsKeyPresent(span, AttributeHttpMethod)) + if (IsKeyPresent(span, AttributeHttpRequestMethod)) { - string? httpMethod = (string?)span.GetTagItem(AttributeHttpMethod); + string? httpMethod = (string?)span.GetTagItem(AttributeHttpRequestMethod); remoteOperation = httpMethod + " " + remoteOperation; } @@ -294,9 +294,9 @@ private static string GenerateRemoteService(Activity span) remoteService += ":" + port; } } - else if (IsKeyPresent(span, AttributeHttpUrl)) + else if (IsKeyPresent(span, AttributeUrlFull)) { - string? httpUrl = (string?)span.GetTagItem(AttributeHttpUrl); + string? httpUrl = (string?)span.GetTagItem(AttributeUrlFull); try { if (httpUrl != null) diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs new file mode 100644 index 00000000..664cb988 --- /dev/null +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry; +using OpenTelemetry.Resources; +using static AWS.OpenTelemetry.AutoInstrumentation.AwsAttributeKeys; + +namespace AWS.OpenTelemetry.AutoInstrumentation; + +/// +/// AwsMetricAttributesSpanProcessor is SpanProcessor that generates metrics from spans +/// This processor will generate metrics based on span data. It depends on a MetricAttributeGenerator being provided on +/// instantiation, which will provide a means to determine attributes which should be used to create metrics. A Resource +/// must also be provided, which is used to generate metrics.Finally, three Histogram must be provided, which will be +/// used to actually create desired metrics (see below) +/// +/// AwsMetricAttributesSpanProcessor produces metrics for errors (e.g.HTTP 4XX status codes), faults(e.g.HTTP 5XX status +/// codes), and latency(in Milliseconds). Errors and faults are counted, while latency is measured with a histogram. +/// Metrics are emitted with attributes derived from span attributes. +/// +/// For highest fidelity metrics, this processor should be coupled with the AlwaysRecordSampler, which will result in +/// 100% of spans being sent to the processor. +/// +public class AwsMetricAttributesSpanProcessor : BaseProcessor +{ + private IMetricAttributeGenerator generator; + private Resource resource; + + private AwsMetricAttributesSpanProcessor( + IMetricAttributeGenerator generator, + Resource resource) + { + this.generator = generator; + this.resource = resource; + } + + /// + /// Configure Resource Builder for Logs, Metrics and Traces + /// + /// to configure + public override void OnStart(Activity activity) + { + } + + /// + /// Configure Resource Builder for Logs, Metrics and Traces + /// + /// to configure + public override void OnEnd(Activity activity) + { + this.AddMetricAttributes(activity); + } + + /// + /// Use to construct this processor + /// + /// Configured AwsMetricAttributesSpanProcessor + internal static AwsMetricAttributesSpanProcessor Create( + IMetricAttributeGenerator generator, + Resource resource) + { + return new AwsMetricAttributesSpanProcessor(generator, resource); + } + + private static Activity WrapSpanWithAttributes(Activity span, ActivityTagsCollection attributes) + { + foreach (KeyValuePair attribute in attributes) + { + span.SetTag(attribute.Key, attribute.Value); + } + + return span; + } + + private Activity AddMetricAttributes(Activity span) + { + /// If the map has no items, no modifications are required. If there is one item, it means the + /// span either produces Service or Dependency metric attributes, and in either case we want to + /// modify the span with them. If there are two items, the span produces both Service and + /// Dependency metric attributes indicating the span is a local dependency root. The Service + /// Attributes must be a subset of the Dependency, with the exception of AttributeAWSSpanKind. The + /// knowledge that the span is a local root is more important that knowing that it is a + /// Dependency metric, so we take all the Dependency metrics but replace AttributeAWSSpanKind with + /// . + Dictionary attributeMap = + this.generator.GenerateMetricAttributeMapFromSpan(span, this.resource); + ActivityTagsCollection attributes = new ActivityTagsCollection(); + + bool generatesServiceMetrics = AwsSpanProcessingUtil.ShouldGenerateServiceMetricAttributes(span); + bool generatesDependencyMetrics = AwsSpanProcessingUtil.ShouldGenerateDependencyMetricAttributes(span); + + if (generatesServiceMetrics && generatesDependencyMetrics) + { + attributes = this.CopyAttributesWithLocalRoot(attributeMap[IMetricAttributeGenerator.DependencyMetric]); + } + else if (generatesServiceMetrics) + { + attributes = attributeMap[IMetricAttributeGenerator.ServiceMetric]; + } + else if (generatesDependencyMetrics) + { + attributes = attributeMap[IMetricAttributeGenerator.DependencyMetric]; + } + + if (attributes.Count != 0) + { + Activity modifiedSpan = WrapSpanWithAttributes(span, attributes); + return modifiedSpan; + } + else + { + return span; + } + } + + private ActivityTagsCollection CopyAttributesWithLocalRoot(ActivityTagsCollection attributes) + { + ActivityTagsCollection attributeCollection = new ActivityTagsCollection(); + attributeCollection.Concat(attributes); + attributeCollection.Remove(AttributeAWSSpanKind); + attributeCollection.Add(AttributeAWSSpanKind, AwsSpanProcessingUtil.LocalRoot); + return attributeCollection; + } +} diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs new file mode 100644 index 00000000..b8cf82a6 --- /dev/null +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs @@ -0,0 +1,63 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Metrics; +using OpenTelemetry.Resources; + +namespace AWS.OpenTelemetry.AutoInstrumentation; + +/// +/// A builder for +/// +public class AwsMetricAttributesSpanProcessorBuilder +{ + // Defaults + private static readonly IMetricAttributeGenerator DefaultGenerator = new AwsMetricAttributeGenerator(); + + private readonly Resource resource; + + // Optional builder elements + private IMetricAttributeGenerator generator = DefaultGenerator; + + private AwsMetricAttributesSpanProcessorBuilder(Resource resource) + { + this.resource = resource; + } + + /// + /// Configure new AwsMetricAttributesSpanProcessorBuilder + /// + /// Resource to store + /// New AwsMetricAttributesSpanProcessorBuilder + public static AwsMetricAttributesSpanProcessorBuilder Create(Resource resource) + { + return new AwsMetricAttributesSpanProcessorBuilder(resource); + } + + /// + /// Sets the generator used to generate attributes used in metrics produced by span metrics + /// processor. If unset, defaults to . Must not be null. + /// + /// generator to store + /// Returns this instance of the builder + public AwsMetricAttributesSpanProcessorBuilder SetGenerator(IMetricAttributeGenerator generator) + { + if (generator == null) + { + throw new ArgumentNullException("generator must not be null", nameof(generator)); + } + + this.generator = generator; + return this; + } + + /// + /// Sets the scope name used in the creation of metrics by the span metrics processor. If unset, + /// defaults to . Must not be null. + /// + /// Returns this instance of the builder + public AwsMetricAttributesSpanProcessor Build() + { + return AwsMetricAttributesSpanProcessor.Create(this.generator, this.resource); + } +} diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs index 16fe86db..fb5609de 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs @@ -5,7 +5,7 @@ using System.Diagnostics.Metrics; using OpenTelemetry; using OpenTelemetry.Resources; -using static OpenTelemetry.Trace.TraceSemanticConventions; +using static AWS.OpenTelemetry.AutoInstrumentation.AwsSpanProcessingUtil; namespace AWS.OpenTelemetry.AutoInstrumentation; @@ -97,12 +97,14 @@ internal static AwsSpanMetricsProcessor Create( private void RecordErrorOrFault(Activity span, ActivityTagsCollection attributes) { KeyValuePair[] attributesArray = attributes.ToArray(); - object? httpStatusCode = span.GetTagItem(AttributeHttpStatusCode); + object? httpStatusCode = span.GetTagItem(AttributeHttpResponseStatusCode); ActivityStatusCode statusCode = span.Status; + Console.WriteLine("Inside the metric processor: " + httpStatusCode); + if (httpStatusCode == null) { - attributes.TryGetValue(AttributeHttpStatusCode, out httpStatusCode); + attributes.TryGetValue(AttributeHttpResponseStatusCode, out httpStatusCode); } if (httpStatusCode == null diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanProcessingUtil.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanProcessingUtil.cs index 2fedcbf6..3bb682fc 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanProcessingUtil.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanProcessingUtil.cs @@ -12,6 +12,21 @@ namespace AWS.OpenTelemetry.AutoInstrumentation; /** Utility class designed to support shared logic across AWS Span Processors. */ internal sealed class AwsSpanProcessingUtil { + // v1.21.0 + // https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-metrics.md#http-server + // TODO: Use TraceSemanticConventions once the below is officially released. + public const string AttributeHttpRequestMethod = "http.request.method"; // replaces: "http.method" (AttributeHttpMethod) + public const string AttributeHttpRequestMethodOriginal = "http.request.method_original"; + public const string AttributeHttpResponseStatusCode = "http.response.status_code"; // replaces: "http.status_code" (AttributeHttpStatusCode) + public const string AttributeUrlScheme = "url.scheme"; // replaces: "http.scheme" (AttributeHttpScheme) + public const string AttributeUrlFull = "url.full"; // replaces: "http.url" (AttributeHttpUrl) + public const string AttributeUrlPath = "url.path"; // replaces: "http.target" (AttributeHttpTarget) + + // TODO: Check whether the query part of the url is included in the path or not. + // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/f1fd71fdb60146be6399ecfd0dd90243e4c8cf1b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInListener.cs#L94 + public const string AttributeUrlQuery = "url.query"; // replaces: "http.target" (AttributeHttpTarget) + public const string AttributeServerSocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp) + // Default attribute values if no valid span attribute value is identified internal static readonly string UnknownService = "UnknownService"; internal static readonly string UnknownOperation = "UnknownOperation"; @@ -49,8 +64,15 @@ internal static List GetDialectKeywords() { string json = r.ReadToEnd(); JObject jObject = JObject.Parse(json); - JArray keywordArray = (JArray)jObject["keywords"]; + JArray? keywordArray = (JArray?)jObject["keywords"]; + if (keywordArray == null) + { + return new List(); + } + +#pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. List keywordList = keywordArray.Values().ToList(); +#pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. return keywordList; } } @@ -205,9 +227,9 @@ private static bool IsValidOperation(Activity span, string operation) return false; } - if (IsKeyPresent(span, AttributeHttpMethod)) + if (IsKeyPresent(span, AttributeHttpRequestMethod)) { - object? httpMethod = span.GetTagItem(AttributeHttpMethod); + object? httpMethod = span.GetTagItem(AttributeHttpRequestMethod); return !operation.Equals((string?)httpMethod); } @@ -219,9 +241,9 @@ private static bool IsValidOperation(Activity span, string operation) private static string GenerateIngressOperation(Activity span) { string operation = UnknownOperation; - if (IsKeyPresent(span, AttributeHttpTarget)) + if (IsKeyPresent(span, AttributeUrlPath)) { - object? httpTarget = span.GetTagItem(AttributeHttpTarget); + object? httpTarget = span.GetTagItem(AttributeUrlPath); // get the first part from API path string as operation value // the more levels/parts we get from API path the higher chance for getting high cardinality @@ -229,9 +251,9 @@ private static string GenerateIngressOperation(Activity span) if (httpTarget != null) { operation = ExtractAPIPathValue((string)httpTarget); - if (IsKeyPresent(span, AttributeHttpMethod)) + if (IsKeyPresent(span, AttributeHttpRequestMethod)) { - string? httpMethod = (string?)span.GetTagItem(AttributeHttpMethod); + string? httpMethod = (string?)span.GetTagItem(AttributeHttpRequestMethod); if (httpMethod != null) { operation = httpMethod + " " + operation; diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs index 14e18930..c912c544 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Reflection; +using System.Diagnostics.Metrics; using Microsoft.Extensions.Logging; using OpenTelemetry; -using OpenTelemetry.Contrib.Extensions.AWSXRay.Resources; using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; +using OpenTelemetry.ResourceDetectors.AWS; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -20,12 +20,10 @@ public class Plugin { private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddConsole()); private static readonly ILogger Logger = Factory.CreateLogger(); - private static readonly string APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"; private static readonly string ApplicationSignalsEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"; - private static readonly string APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"; private static readonly string ApplicationSignalsExporterEndpointConfig = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"; private static readonly string MetricExportIntervalConfig = "OTEL_METRIC_EXPORT_INTERVAL"; - private static readonly int DefaultMetricExportInternal = 60000; + private static readonly int DefaultMetricExportInterval = 60000; private static readonly string OtelTracesSampler = "OTEL_TRACES_SAMPLER"; private static readonly string OtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"; private static readonly string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; @@ -35,7 +33,6 @@ public class Plugin /// public void Initializing() public void Initializing() { - // My custom logic here } /// @@ -46,42 +43,43 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) { if (this.IsApplicationSignalsEnabled()) { - // add new export processor here. - // https://stackoverflow.com/questions/12993962/set-value-of-private-field - // use reflection to get the internal exporter and set the new modified exporter. - // I need to get the composite processor after SDK init and replace the exporter. - var processor = tracerProvider.GetType().GetProperty("Processor", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(tracerProvider); - var exporter = processor.GetType().GetField("exporter", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(processor); - tracerProvider.AddProcessor(AttributePropagatingSpanProcessorBuilder.Create().Build()); string? intervalConfigString = System.Environment.GetEnvironmentVariable(MetricExportIntervalConfig); - int exportInterval = DefaultMetricExportInternal; + int exportInterval = DefaultMetricExportInterval; try { - exportInterval = Convert.ToInt32(intervalConfigString); + int parsedExportInterval = Convert.ToInt32(intervalConfigString); + exportInterval = parsedExportInterval != 0 ? parsedExportInterval : DefaultMetricExportInterval; } catch (Exception) { Logger.Log(LogLevel.Trace, "Could not convert OTEL_METRIC_EXPORT_INTERVAL to integer. Using default value 60000."); } - if (exportInterval.CompareTo(DefaultMetricExportInternal) > 0) + if (exportInterval.CompareTo(DefaultMetricExportInterval) > 0) { - exportInterval = DefaultMetricExportInternal; + exportInterval = DefaultMetricExportInterval; Logger.Log(LogLevel.Information, "AWS Application Signals metrics export interval capped to {0}", exportInterval); } // https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md#enable-metric-exporter // for setting the temporatityPref. - var metricReader = new PeriodicExportingMetricReader(this.ApplicationSignalsExporterProvider(), exportInterval); + var metricReader = new PeriodicExportingMetricReader(this.ApplicationSignalsExporterProvider(), exportInterval) + { + TemporalityPreference = MetricReaderTemporalityPreference.Delta, + }; + MeterProvider provider = Sdk.CreateMeterProviderBuilder() .AddReader(metricReader) - .ConfigureResource(builder => builder - .AddDetector(new AWSEC2ResourceDetector()) - .AddDetector(new AWSECSResourceDetector()) - .AddDetector(new AWSEKSResourceDetector())) + .ConfigureResource(builder => this.GetResourceBuilder()) .AddMeter("AwsSpanMetricsProcessor") + .AddView(instrument => + { + return instrument.GetType().GetGenericTypeDefinition() == typeof(Histogram<>) + ? new Base2ExponentialBucketHistogramConfiguration() + : new ExplicitBucketHistogramConfiguration(); + }) .Build(); Resource resource = provider.GetResource(); @@ -90,10 +88,56 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) } } + /// + /// To configure tracing SDK before Auto Instrumentation configured SDK + /// + /// Provider to configure + /// Returns configured builder + public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder builder) + { + if (this.IsApplicationSignalsEnabled()) + { + var resource = this.GetResourceBuilder().Build(); + var processor = AwsMetricAttributesSpanProcessorBuilder.Create(resource).Build(); + builder.AddProcessor(processor); + } + + return builder; + } + + /// + /// To configure tracing SDK after Auto Instrumentation configured SDK + /// + /// Provider to configure + /// Returns configured builder + public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder builder) + { + if (this.IsApplicationSignalsEnabled()) + { + Logger.Log(LogLevel.Information, "AWS Application Signals enabled"); + Sampler alwaysRecordSampler = AlwaysRecordSampler.Create(this.GetSampler()); + builder.SetSampler(alwaysRecordSampler); + builder.AddXRayTraceId(); + } + + return builder; + } + + private bool IsApplicationSignalsEnabled() + { + return System.Environment.GetEnvironmentVariable(ApplicationSignalsEnabledConfig) == "true"; + } + + private ResourceBuilder GetResourceBuilder() + { + return ResourceBuilder.CreateDefault() + .AddDetector(new AWSEC2ResourceDetector()) + .AddDetector(new AWSECSResourceDetector()) + .AddDetector(new AWSEKSResourceDetector()); + } + private OtlpMetricExporter ApplicationSignalsExporterProvider() { - // https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#configuring-the-aggregation-of-a-histogram - // use the above to check about adding aggregation type. var options = new OtlpExporterOptions(); Logger.Log( @@ -123,44 +167,6 @@ private OtlpMetricExporter ApplicationSignalsExporterProvider() return new OtlpMetricExporter(options); } - /// - /// To access MeterProvider right after MeterProviderBuilder.Build() is executed. - /// - /// Provider to configure - public void MeterProviderInitialized(MeterProvider meterProvider) - { - // My custom logic here - } - - /// - /// To configure tracing SDK before Auto Instrumentation configured SDK - /// - /// Provider to configure - /// Returns configured builder - public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder builder) - { - // My custom logic here - return builder; - } - - /// - /// To configure tracing SDK after Auto Instrumentation configured SDK - /// - /// Provider to configure - /// Returns configured builder - public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder builder) - { - if (this.IsApplicationSignalsEnabled()) - { - Logger.Log(LogLevel.Information, "AWS Application Signals enabled"); - Sampler alwaysRecordSampler = AlwaysRecordSampler.Create(this.GetSampler()); - builder.SetSampler(alwaysRecordSampler); - } - - builder.AddXRayTraceId(); - return builder; - } - // This function is based on an internal function in Otel: // https://github.com/open-telemetry/opentelemetry-dotnet/blob/1bbafaa7b7bed6470ff52fc76b6e881cd19692a5/src/OpenTelemetry/Trace/TracerProviderSdk.cs#L408 // Unfortunately, that function is private. @@ -206,52 +212,4 @@ private Sampler GetSampler() return new ParentBasedSampler(alwaysOnSampler); } } - - // To configure any metrics options used by OpenTelemetry .NET Automatic Instrumentation - public void ConfigureMetricsOptions(MetricReaderOptions options) - { - // My custom logic here - // Find supported options below - } - - /// - /// To configure metrics SDK before Auto Instrumentation configured SDK - /// - /// Provider to configure - /// Returns configured builder - public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder builder) - { - // My custom logic here - return builder; - } - - /// - /// To configure metrics SDK after Auto Instrumentation configured SDK - /// - /// Provider to configure - /// Returns configured builder - public MeterProviderBuilder AfterConfigureMeterProvider(MeterProviderBuilder builder) - { - // My custom logic here - return builder; - } - - /// - /// To configure Resource - /// - /// Builder to configure - /// Returns configured builder - public ResourceBuilder ConfigureResource(ResourceBuilder builder) - { - // My custom logic here - // Please note this method is common to set the resource for trace, logs and metrics. - // This method could be overridden by ConfigureTracesOptions, ConfigureMeterProvider and ConfigureLogsOptions - // by calling SetResourceBuilder with new object. - return builder; - } - - private bool IsApplicationSignalsEnabled() - { - return System.Environment.GetEnvironmentVariable(ApplicationSignalsEnabledConfig) == "true"; - } } diff --git a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanProcessingUtilTest.cs b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanProcessingUtilTest.cs index 58516e4e..c41f03de 100644 --- a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanProcessingUtilTest.cs +++ b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanProcessingUtilTest.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using static AWS.OpenTelemetry.AutoInstrumentation.AwsAttributeKeys; using static OpenTelemetry.Trace.TraceSemanticConventions; +using static AWS.OpenTelemetry.AutoInstrumentation.AwsSpanProcessingUtil; using System.Reflection; namespace AWS.OpenTelemetry.AutoInstrumentation.Tests; @@ -10,10 +11,10 @@ namespace AWS.OpenTelemetry.AutoInstrumentation.Tests; public class AwsSpanProcessingUtilTest { private readonly ActivitySource testSource = new ActivitySource("Test Source"); - private readonly string internalOperation = "InternalOperation"; + private readonly string internalOperation = "InternalOperation"; private readonly string unknownOperation = "UnknownOperation"; private readonly string defaultPathValue = "/"; - + public AwsSpanProcessingUtilTest() { var listener = new ActivityListener @@ -45,20 +46,20 @@ public void TestGetIngressOperationWithnotServer() string actualOperation = AwsSpanProcessingUtil.GetIngressOperation(spanDataMock); Assert.Equal(internalOperation, actualOperation); } - - [Fact] + + [Fact] public void TestGetIngressOperationHttpMethodNameAndNoFallback() { string validName = "GET"; var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); - spanDataMock.SetTag(AttributeHttpMethod, validName); + spanDataMock.SetTag(AttributeHttpRequestMethod, validName); spanDataMock.DisplayName = validName; spanDataMock.Start(); string actualOperation = AwsSpanProcessingUtil.GetIngressOperation(spanDataMock); Assert.Equal(unknownOperation, actualOperation); } - - [Fact] + + [Fact] public void TestGetIngressOperationEmptyNameAndNoFallback() { var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); @@ -67,8 +68,8 @@ public void TestGetIngressOperationEmptyNameAndNoFallback() string actualOperation = AwsSpanProcessingUtil.GetIngressOperation(spanDataMock); Assert.Equal(unknownOperation, actualOperation); } - - [Fact] + + [Fact] public void TestGetIngressOperationUnknownNameAndNoFallback() { var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); @@ -77,22 +78,22 @@ public void TestGetIngressOperationUnknownNameAndNoFallback() string actualOperation = AwsSpanProcessingUtil.GetIngressOperation(spanDataMock); Assert.Equal(unknownOperation, actualOperation); } - - [Fact] + + [Fact] public void testGetIngressOperationInvalidNameAndValidTarget() { string invalidName = ""; string validTarget = "/"; var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); spanDataMock.DisplayName = invalidName; - spanDataMock.SetTag(AttributeHttpTarget, validTarget); + spanDataMock.SetTag(AttributeUrlPath, validTarget); spanDataMock.Start(); string actualOperation = AwsSpanProcessingUtil.GetIngressOperation(spanDataMock); - Assert.Equal( validTarget, actualOperation); - } + Assert.Equal(validTarget, actualOperation); + } + - - [Fact] + [Fact] public void testGetIngressOperationInvalidNameAndValidTargetAndMethod() { string invalidName = ""; @@ -100,13 +101,13 @@ public void testGetIngressOperationInvalidNameAndValidTargetAndMethod() string validMethod = "GET"; var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); spanDataMock.DisplayName = invalidName; - spanDataMock.SetTag(AttributeHttpMethod, validMethod); - spanDataMock.SetTag(AttributeHttpTarget, validTarget); + spanDataMock.SetTag(AttributeHttpRequestMethod, validMethod); + spanDataMock.SetTag(AttributeUrlPath, validTarget); spanDataMock.Start(); string actualOperation = AwsSpanProcessingUtil.GetIngressOperation(spanDataMock); Assert.Equal(validMethod + " " + validTarget, actualOperation); - } - + } + [Fact] public void TestGetEgressOperationUseInternalOperation() { @@ -115,8 +116,8 @@ public void TestGetEgressOperationUseInternalOperation() spanDataMock.Start(); string actualOperation = AwsSpanProcessingUtil.GetEgressOperation(spanDataMock); Assert.Equal(internalOperation, actualOperation); - } - + } + [Fact] public void TestGetEgressOperationUseLocalOperation() { @@ -127,7 +128,7 @@ public void TestGetEgressOperationUseLocalOperation() string actualOperation = AwsSpanProcessingUtil.GetEgressOperation(spanDataMock); Assert.Equal(operation, actualOperation); } - + [Fact] public void TestExtractAPIPathValueEmptyTarget() { @@ -135,7 +136,7 @@ public void TestExtractAPIPathValueEmptyTarget() string pathValue = AwsSpanProcessingUtil.ExtractAPIPathValue(invalidTarget); Assert.Equal(defaultPathValue, pathValue); } - + [Fact] public void TestExtractAPIPathValueNullTarget() { @@ -143,7 +144,7 @@ public void TestExtractAPIPathValueNullTarget() string pathValue = AwsSpanProcessingUtil.ExtractAPIPathValue(invalidTarget); Assert.Equal(defaultPathValue, pathValue); } - + [Fact] public void TestExtractAPIPathValueNoSlash() { @@ -175,32 +176,36 @@ public void TestExtractAPIPathValidPath() string pathValue = AwsSpanProcessingUtil.ExtractAPIPathValue(validTarget); Assert.Equal("/users", pathValue); } - + [Fact] - public void testIsKeyPresentKeyPresent() { + public void testIsKeyPresentKeyPresent() + { var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); - spanDataMock.SetTag(AttributeHttpTarget, "target"); + spanDataMock.SetTag(AttributeUrlPath, "target"); spanDataMock.Start(); - Assert.True(AwsSpanProcessingUtil.IsKeyPresent(spanDataMock, AttributeHttpTarget)); - } - + Assert.True(AwsSpanProcessingUtil.IsKeyPresent(spanDataMock, AttributeUrlPath)); + } + [Fact] - public void testIsKeyPresentKeyAbsent() { + public void testIsKeyPresentKeyAbsent() + { var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); spanDataMock.Start(); - Assert.False(AwsSpanProcessingUtil.IsKeyPresent(spanDataMock, AttributeHttpTarget)); - } - + Assert.False(AwsSpanProcessingUtil.IsKeyPresent(spanDataMock, AttributeUrlPath)); + } + [Fact] - public void testIsAwsSpanTrue() { + public void testIsAwsSpanTrue() + { var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); spanDataMock.SetTag(AttributeRpcSystem, "aws-api"); spanDataMock.Start(); Assert.True(AwsSpanProcessingUtil.IsAwsSDKSpan(spanDataMock)); - } - + } + [Fact] - public void testIsAwsSpanFalse() { + public void testIsAwsSpanFalse() + { var spanDataMock = testSource.StartActivity("test", ActivityKind.Server); spanDataMock.Start(); Assert.False(AwsSpanProcessingUtil.IsAwsSDKSpan(spanDataMock)); @@ -295,7 +300,7 @@ public void testShouldGenerateDependencyMetricAttributes() spanDataMock.SetTag(AttributeAWSConsumerParentSpanKind, ActivityKind.Consumer.GetType().Name); Assert.False(AwsSpanProcessingUtil.ShouldGenerateDependencyMetricAttributes(spanDataMock)); } - + using (var spanDataMock = testSource.StartActivity("test", ActivityKind.Consumer)) { spanDataMock.SetTag(AttributeMessagingOperation, MessagingOperationValues.Process); @@ -317,7 +322,7 @@ public void testIsLocalRoot() spanDataMock.SetParentId(parentSpan.TraceId, parentSpan.SpanId); Assert.False(AwsSpanProcessingUtil.IsLocalRoot(spanDataMock)); } - + using (var spanDataMock = testSource.StartActivity("test")) { spanDataMock.SetParentId(parentSpan.TraceId, parentSpan.SpanId); @@ -353,7 +358,7 @@ public void testNoMetricAttributesForSqsConsumerSpanAwsSdk() Assert.False(AwsSpanProcessingUtil.ShouldGenerateServiceMetricAttributes(spanDataMock)); Assert.False(AwsSpanProcessingUtil.ShouldGenerateDependencyMetricAttributes(spanDataMock)); } - + [Fact] public void testMetricAttributesGeneratedForOtherInstrumentationSqsConsumerSpan() { @@ -363,7 +368,7 @@ public void testMetricAttributesGeneratedForOtherInstrumentationSqsConsumerSpan( Assert.True(AwsSpanProcessingUtil.ShouldGenerateServiceMetricAttributes(spanDataMock)); Assert.True(AwsSpanProcessingUtil.ShouldGenerateDependencyMetricAttributes(spanDataMock)); } - + [Fact] public void testNoMetricAttributesForAwsSdkSqsConsumerProcessSpan() { @@ -375,7 +380,7 @@ public void testNoMetricAttributesForAwsSdkSqsConsumerProcessSpan() Assert.False(AwsSpanProcessingUtil.ShouldGenerateServiceMetricAttributes(spanDataMock)); Assert.False(AwsSpanProcessingUtil.ShouldGenerateDependencyMetricAttributes(spanDataMock)); spanDataMock.Dispose(); - + spanDataMock = awsActivitySource.StartActivity("SQS.ReceiveMessage", ActivityKind.Consumer); spanDataMock.SetTag(AttributeAWSServiceName, "SQS"); spanDataMock.SetTag(AttributeMessagingOperation, MessagingOperationValues.Receive); @@ -397,7 +402,7 @@ public void testSqlDialectKeywordsOrder() prevKeywordLength = currKeywordLength; } } - + [Fact] public void TestSqlDialectKeywordsMaxLength() { From 88e76b06e99ff54758957f1fd2a531fabf835636 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Fri, 24 May 2024 10:15:25 -0700 Subject: [PATCH 4/6] reverted change in build.cs --- build/Build.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/Build.cs b/build/Build.cs index 929124d6..394298a3 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -85,7 +85,6 @@ private static string GetOTelAutoInstrumentationFileName() fileName = "opentelemetry-dotnet-instrumentation-windows.zip"; break; case PlatformFamily.Linux: - case PlatformFamily.OSX: var architecture = RuntimeInformation.ProcessArchitecture; string architectureSuffix; switch (architecture) @@ -104,6 +103,9 @@ private static string GetOTelAutoInstrumentationFileName() ? $"opentelemetry-dotnet-instrumentation-linux-musl-{architectureSuffix}.zip" : $"opentelemetry-dotnet-instrumentation-linux-glibc-{architectureSuffix}.zip"; break; + case PlatformFamily.OSX: + fileName = "opentelemetry-dotnet-instrumentation-macos.zip"; + break; case PlatformFamily.Unknown: throw new NotSupportedException(); default: From 9e36da072c93f618a45392aab87cae09337bb87a Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Tue, 28 May 2024 12:20:35 -0700 Subject: [PATCH 5/6] Did more testing and applied comments --- .../integration-test-app/Dockerfile | 1 + .../AwsMetricAttributesSpanExporter.cs | 41 ---------- .../AwsMetricAttributesSpanExporterBuilder.cs | 54 ------------- .../AwsMetricAttributesSpanProcessor.cs | 4 + ...AwsMetricAttributesSpanProcessorBuilder.cs | 5 +- .../AwsSpanMetricsProcessor.cs | 14 ++-- .../AwsSpanMetricsProcessorBuilder.cs | 5 +- .../Plugin.cs | 77 ++++++------------- .../SamplerUtil.cs | 68 ++++++++++++++++ 9 files changed, 107 insertions(+), 162 deletions(-) delete mode 100644 src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporter.cs delete mode 100644 src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporterBuilder.cs create mode 100644 src/AWS.OpenTelemetry.AutoInstrumentation/SamplerUtil.cs diff --git a/sample-applications/integration-test-app/Dockerfile b/sample-applications/integration-test-app/Dockerfile index 8ec1e7eb..da3fbffa 100644 --- a/sample-applications/integration-test-app/Dockerfile +++ b/sample-applications/integration-test-app/Dockerfile @@ -26,3 +26,4 @@ ENV OTEL_DOTNET_AUTO_PLUGINS="AWS.OpenTelemetry.AutoInstrumentation.Plugin, AWS. ENV OTEL_AWS_APPLICATION_SIGNALS_ENABLED="true" ENV OTEL_TRACES_SAMPLER="always_on" ENV OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" +ENV OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT="http://otel:4318/v1/metrics" diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporter.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporter.cs deleted file mode 100644 index 77298d61..00000000 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporter.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace AWS.OpenTelemetry.AutoInstrumentation; - -/// -/// This exporter will update a span with metric attributes before exporting. It depends on a {@link -/// SpanExporter} being provided on instantiation, which the AwsSpanMetricsExporter will delegate -/// export to. Also, a {@link MetricAttributeGenerator} must be provided, which will provide a means -/// to determine attributes which should be applied to the span. Finally, a {@link Resource} must be -/// provided, which is used to generate metric attributes. -/// -///

This exporter should be coupled with the {@link AwsSpanMetricsProcessor} using the same {@link -/// MetricAttributeGenerator}. This will result in metrics and spans being produced with common -/// attributes. -///

-public class AwsMetricAttributesSpanExporter : BaseExporter -{ - private AwsMetricAttributesSpanExporter(BaseExporter exporterDelegate, IMetricAttributeGenerator generator, Resource resource) - { - // TODO: implement this - } - - /// - public override ExportResult Export(in Batch batch) - { - throw new NotImplementedException(); - } - - /// Use to construct this exporter. - internal static AwsMetricAttributesSpanExporter Create( - BaseExporter exporterDelegate, IMetricAttributeGenerator generator, Resource resource) - { - return new AwsMetricAttributesSpanExporter(exporterDelegate, generator, resource); - } -} diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporterBuilder.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporterBuilder.cs deleted file mode 100644 index ea778ee3..00000000 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanExporterBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; - -namespace AWS.OpenTelemetry.AutoInstrumentation; - -/// -/// TODO: Add Description -/// -public class AwsMetricAttributesSpanExporterBuilder -{ - // Defaults - private static readonly IMetricAttributeGenerator DefaultGenerator = new AwsMetricAttributeGenerator(); - - // Required builder elements - private readonly BaseExporter exporterDelegate; - private readonly Resource resource; - - // Optional builder elements - private IMetricAttributeGenerator generator = DefaultGenerator; - - private AwsMetricAttributesSpanExporterBuilder(BaseExporter exporterDelegate, Resource resource) - { - this.exporterDelegate = exporterDelegate; - this.resource = resource; - } - - public static AwsMetricAttributesSpanExporterBuilder Create( - BaseExporter exporterDelegate, Resource resource) - { - return new AwsMetricAttributesSpanExporterBuilder(exporterDelegate, resource); - } - - /// - /// Sets the generator used to generate attributes used spancs exported by the exporter. If unset, - /// defaults to {@link #DEFAULT_GENERATOR}. Must not be null. - /// - /// generator to set - /// Returns the builder - public AwsMetricAttributesSpanExporterBuilder SetGenerator(IMetricAttributeGenerator generator) - { - this.generator = generator; - return this; - } - - public AwsMetricAttributesSpanExporter Build() - { - return AwsMetricAttributesSpanExporter.Create(this.exporterDelegate, this.generator, this.resource); - } -} diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs index 664cb988..15a11537 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs @@ -45,6 +45,10 @@ public override void OnStart(Activity activity) /// /// Configure Resource Builder for Logs, Metrics and Traces + /// TODO: There is an OTEL discussion to add BeforeEnd to allow us to write to spans. Below is a hack and goes + /// against the otel specs (not to edit span in OnEnd) + /// Add BeforeEnd to have a callback where the span is still writeable open-telemetry/opentelemetry-specification#1089 + /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onendspan /// /// to configure public override void OnEnd(Activity activity) diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs index b8cf82a6..d7073f60 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs @@ -52,10 +52,9 @@ public AwsMetricAttributesSpanProcessorBuilder SetGenerator(IMetricAttributeGene } /// - /// Sets the scope name used in the creation of metrics by the span metrics processor. If unset, - /// defaults to . Must not be null. + /// Creates an instance of AwsMetricAttributesSpanProcessor /// - /// Returns this instance of the builder + /// Returns AwsMetricAttributesSpanProcessor public AwsMetricAttributesSpanProcessor Build() { return AwsMetricAttributesSpanProcessor.Create(this.generator, this.resource); diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs index fb5609de..af8b8e5d 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessor.cs @@ -100,16 +100,14 @@ private void RecordErrorOrFault(Activity span, ActivityTagsCollection attributes object? httpStatusCode = span.GetTagItem(AttributeHttpResponseStatusCode); ActivityStatusCode statusCode = span.Status; - Console.WriteLine("Inside the metric processor: " + httpStatusCode); - if (httpStatusCode == null) { attributes.TryGetValue(AttributeHttpResponseStatusCode, out httpStatusCode); } if (httpStatusCode == null - || (long)httpStatusCode < ErrorCodeLowerBound - || (long)httpStatusCode > FaultCodeUpperBound) + || (int)httpStatusCode < ErrorCodeLowerBound + || (int)httpStatusCode > FaultCodeUpperBound) { if (ActivityStatusCode.Error.Equals(statusCode)) { @@ -122,14 +120,14 @@ private void RecordErrorOrFault(Activity span, ActivityTagsCollection attributes this.faultHistogram.Record(0, attributesArray); } } - else if ((long)httpStatusCode >= ErrorCodeLowerBound - && (long)httpStatusCode <= ErrorCodeUpperBound) + else if ((int)httpStatusCode >= ErrorCodeLowerBound + && (int)httpStatusCode <= ErrorCodeUpperBound) { this.errorHistogram.Record(1, attributesArray); this.faultHistogram.Record(0, attributesArray); } - else if ((long)httpStatusCode >= FaultCodeLowerBound - && (long)httpStatusCode <= FaultCodeUpperBound) + else if ((int)httpStatusCode >= FaultCodeLowerBound + && (int)httpStatusCode <= FaultCodeUpperBound) { this.errorHistogram.Record(0, attributesArray); this.faultHistogram.Record(1, attributesArray); diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs index b7de5334..f2e6e052 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsSpanMetricsProcessorBuilder.cs @@ -78,10 +78,9 @@ public AwsSpanMetricsProcessorBuilder SetScopeName(string scopeName) } /// - /// Sets the scope name used in the creation of metrics by the span metrics processor. If unset, - /// defaults to . Must not be null. + /// Creates AwsSpanMetricsProcessor with Histograms subscribed the meter with this.scopeName /// - /// Returns this instance of the builder + /// Returns AwsSpanMetricsProcessor public AwsSpanMetricsProcessor Build() { Meter meter = new Meter(this.scopeName); diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs index c912c544..d7443095 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -19,13 +19,11 @@ namespace AWS.OpenTelemetry.AutoInstrumentation; public class Plugin { private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddConsole()); - private static readonly ILogger Logger = Factory.CreateLogger(); + private static readonly ILogger Logger = Factory.CreateLogger(); private static readonly string ApplicationSignalsEnabledConfig = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"; private static readonly string ApplicationSignalsExporterEndpointConfig = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"; private static readonly string MetricExportIntervalConfig = "OTEL_METRIC_EXPORT_INTERVAL"; private static readonly int DefaultMetricExportInterval = 60000; - private static readonly string OtelTracesSampler = "OTEL_TRACES_SAMPLER"; - private static readonly string OtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"; private static readonly string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; /// @@ -76,9 +74,11 @@ public void TracerProviderInitialized(TracerProvider tracerProvider) .AddMeter("AwsSpanMetricsProcessor") .AddView(instrument => { + // we currently only listen and meter Histograms and for that, + // we use Base2ExponentialBucketHistogramConfiguration return instrument.GetType().GetGenericTypeDefinition() == typeof(Histogram<>) ? new Base2ExponentialBucketHistogramConfiguration() - : new ExplicitBucketHistogramConfiguration(); + : null; }) .Build(); @@ -115,7 +115,7 @@ public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder if (this.IsApplicationSignalsEnabled()) { Logger.Log(LogLevel.Information, "AWS Application Signals enabled"); - Sampler alwaysRecordSampler = AlwaysRecordSampler.Create(this.GetSampler()); + Sampler alwaysRecordSampler = AlwaysRecordSampler.Create(SamplerUtil.GetSampler()); builder.SetSampler(alwaysRecordSampler); builder.AddXRayTraceId(); } @@ -123,6 +123,23 @@ public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder return builder; } + /// + /// To configure Resource + /// TODO: Add versioning similar to Python + /// + /// Provider to configure + /// Returns configured builder + public ResourceBuilder ConfigureResource(ResourceBuilder builder) + { + Dictionary attributes = new Dictionary + { + { "telemetry.distro.name", "aws-otel-dotnet-instrumentation" }, + }; + builder + .AddAttributes(attributes); + return builder; + } + private bool IsApplicationSignalsEnabled() { return System.Environment.GetEnvironmentVariable(ApplicationSignalsEnabledConfig) == "true"; @@ -132,8 +149,8 @@ private ResourceBuilder GetResourceBuilder() { return ResourceBuilder.CreateDefault() .AddDetector(new AWSEC2ResourceDetector()) - .AddDetector(new AWSECSResourceDetector()) - .AddDetector(new AWSEKSResourceDetector()); + .AddDetector(new AWSEKSResourceDetector()) + .AddDetector(new AWSECSResourceDetector()); } private OtlpMetricExporter ApplicationSignalsExporterProvider() @@ -166,50 +183,4 @@ private OtlpMetricExporter ApplicationSignalsExporterProvider() return new OtlpMetricExporter(options); } - - // This function is based on an internal function in Otel: - // https://github.com/open-telemetry/opentelemetry-dotnet/blob/1bbafaa7b7bed6470ff52fc76b6e881cd19692a5/src/OpenTelemetry/Trace/TracerProviderSdk.cs#L408 - // Unfortunately, that function is private. - private Sampler GetSampler() - { - string? tracesSampler = System.Environment.GetEnvironmentVariable(OtelTracesSampler); - string? tracesSamplerArg = System.Environment.GetEnvironmentVariable(OtelTracesSamplerArg); - double samplerProbability = 1.0; - if (tracesSampler != null) - { - try - { - samplerProbability = Convert.ToDouble(tracesSamplerArg); - } - catch (Exception) - { - Logger.Log(LogLevel.Trace, "Could not convert OTEL_TRACES_SAMPLER_ARG to double. Using default value 1.0."); - } - } - - // based on the list of available samplers: - // https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/77256e3a9666ee0f1f72fec5f4ca1a6d8500f229/docs/config.md#samplers - // Need to also add XRay Sampler here. - // Currently, this is the only way to get the sampler as there is no factory and we can't get the sampler - // that was already set in the TracerProviderBuilder - switch (tracesSampler) - { - case "always_on": - return new AlwaysOnSampler(); - case "always_off": - return new AlwaysOffSampler(); - case "traceidratio": - return new TraceIdRatioBasedSampler(samplerProbability); - case "parentbased_always_off": - Sampler alwaysOffSampler = new AlwaysOffSampler(); - return new ParentBasedSampler(alwaysOffSampler); - case "parentbased_traceidratio": - Sampler traceIdRatioSampler = new TraceIdRatioBasedSampler(samplerProbability); - return new ParentBasedSampler(traceIdRatioSampler); - case "parentbased_always_on": - default: - Sampler alwaysOnSampler = new AlwaysOnSampler(); - return new ParentBasedSampler(alwaysOnSampler); - } - } } diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/SamplerUtil.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/SamplerUtil.cs new file mode 100644 index 00000000..1f1fa3b4 --- /dev/null +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/SamplerUtil.cs @@ -0,0 +1,68 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; + +namespace AWS.OpenTelemetry.AutoInstrumentation; + +/// +/// Class for getting sampler for instrumentation +/// +public class SamplerUtil +{ + private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddConsole()); + private static readonly ILogger Logger = Factory.CreateLogger(); + private static readonly string OtelTracesSampler = "OTEL_TRACES_SAMPLER"; + private static readonly string OtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG"; + + /// + /// This function is based on an internal function in Otel: + /// https://github.com/open-telemetry/opentelemetry-dotnet/blob/1bbafaa7b7bed6470ff52fc76b6e881cd19692a5/src/OpenTelemetry/Trace/TracerProviderSdk.cs#L408 + /// Unfortunately, that function is private. + /// + /// Sampler to wrap AlwaysRecordSampler around + public static Sampler GetSampler() + { + string? tracesSampler = System.Environment.GetEnvironmentVariable(OtelTracesSampler); + string? tracesSamplerArg = System.Environment.GetEnvironmentVariable(OtelTracesSamplerArg); + double samplerProbability = 1.0; + if (tracesSampler != null) + { + try + { + samplerProbability = Convert.ToDouble(tracesSamplerArg); + } + catch (Exception) + { + Logger.Log(LogLevel.Trace, "Could not convert OTEL_TRACES_SAMPLER_ARG to double. Using default value 1.0."); + } + } + + // based on the list of available samplers: + // https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/77256e3a9666ee0f1f72fec5f4ca1a6d8500f229/docs/config.md#samplers + // Currently, this is the only way to get the sampler as there is no factory and we can't get the sampler + // that was already set in the TracerProviderBuilder + // TODO: Add case for X-Ray Sampler when implemented and tested + switch (tracesSampler) + { + case "always_on": + return new AlwaysOnSampler(); + case "always_off": + return new AlwaysOffSampler(); + case "traceidratio": + return new TraceIdRatioBasedSampler(samplerProbability); + case "parentbased_always_off": + Sampler alwaysOffSampler = new AlwaysOffSampler(); + return new ParentBasedSampler(alwaysOffSampler); + case "parentbased_traceidratio": + Sampler traceIdRatioSampler = new TraceIdRatioBasedSampler(samplerProbability); + return new ParentBasedSampler(traceIdRatioSampler); + case "parentbased_always_on": + default: + Sampler alwaysOnSampler = new AlwaysOnSampler(); + return new ParentBasedSampler(alwaysOnSampler); + } + } +} From 284ef41910456d7fb24758110f39d49276760a80 Mon Sep 17 00:00:00 2001 From: Mohamed Asaker Date: Wed, 29 May 2024 09:55:59 -0700 Subject: [PATCH 6/6] applied some comments and merged main --- build/Build.cs | 2 +- .../AwsMetricAttributesSpanProcessor.cs | 7 ++++--- .../AwsSpanMetricsProcessorTest.cs | 17 +++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/build/Build.cs b/build/Build.cs index 394298a3..c4dcccd8 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -124,7 +124,7 @@ private static string GetOTelAutoInstrumentationFileName() "net8.0" / "AWS.OpenTelemetry.AutoInstrumentation.dll", this.openTelemetryDistributionFolder / "net"); - //TODO: fix build script to copy dependencies without manually setting them here. + // TODO: fix build script to copy dependencies without manually setting them here. FileSystemTasks.CopyFileToDirectory( RootDirectory / "src" / "AWS.OpenTelemetry.AutoInstrumentation" / "bin" / this.configuration / "net8.0" / "Newtonsoft.Json.dll", diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs index 15a11537..875cfd07 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs @@ -24,8 +24,8 @@ namespace AWS.OpenTelemetry.AutoInstrumentation; /// public class AwsMetricAttributesSpanProcessor : BaseProcessor { - private IMetricAttributeGenerator generator; - private Resource resource; + private readonly IMetricAttributeGenerator generator; + private readonly Resource resource; private AwsMetricAttributesSpanProcessor( IMetricAttributeGenerator generator, @@ -46,8 +46,9 @@ public override void OnStart(Activity activity) /// /// Configure Resource Builder for Logs, Metrics and Traces /// TODO: There is an OTEL discussion to add BeforeEnd to allow us to write to spans. Below is a hack and goes - /// against the otel specs (not to edit span in OnEnd) + /// against the otel specs (not to edit span in OnEnd) but is required for the time being. /// Add BeforeEnd to have a callback where the span is still writeable open-telemetry/opentelemetry-specification#1089 + /// https://github.com/open-telemetry/opentelemetry-specification/issues/1089 /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onendspan /// /// to configure diff --git a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs index bd1ea744..7849965f 100644 --- a/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs +++ b/test/AWS.OpenTelemetry.AutoInstrumentation.Tests/AwsSpanMetricsProcessorTest.cs @@ -9,6 +9,7 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; using static AWS.OpenTelemetry.AutoInstrumentation.AwsAttributeKeys; +using static AWS.OpenTelemetry.AutoInstrumentation.AwsSpanProcessingUtil; @@ -120,7 +121,7 @@ public void testOnEndMetricsGenerationWithoutMetricAttributes() { spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); setLatency(); - spanDataMock.SetTag(AwsAttributeKeys.AttributeHttpStatusCode, (long)500); + spanDataMock.SetTag(AttributeHttpResponseStatusCode, (long)500); Dictionary expectAttributes = buildMetricAttributes(false, spanDataMock); Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) .Returns(expectAttributes); @@ -226,7 +227,7 @@ public void testsOnEndMetricsGenerationProducerSpan() public void testOnEndMetricsGenerationWithoutEndRequired() { spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); - spanDataMock.SetTag(AttributeHttpStatusCode, (long)500); + spanDataMock.SetTag(AttributeHttpResponseStatusCode, (long)500); setLatency(); Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) @@ -247,7 +248,7 @@ public void testOnEndMetricsGenerationWithoutEndRequired() public void testOnEndMetricsGenerationWithLatency() { spanDataMock = activitySource.StartActivity("test", ActivityKind.Server); - spanDataMock.SetTag(AttributeHttpStatusCode, (long)200); + spanDataMock.SetTag(AttributeHttpResponseStatusCode, (long)200); setLatency(5.5); Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) @@ -335,12 +336,12 @@ private void validateMetricsGeneratedForAttributeStatusCode( expectAttributes[IMetricAttributeGenerator.ServiceMetric]["new service key"] = "new service value"; expectAttributes[IMetricAttributeGenerator.ServiceMetric] - .Add(new KeyValuePair(AttributeHttpStatusCode, awsStatusCode)); + .Add(new KeyValuePair(AttributeHttpResponseStatusCode, awsStatusCode)); expectAttributes[IMetricAttributeGenerator.DependencyMetric]["new dependency key"] = "new dependency value"; expectAttributes[IMetricAttributeGenerator.DependencyMetric] - .Add(new KeyValuePair(AttributeHttpStatusCode, awsStatusCode)); + .Add(new KeyValuePair(AttributeHttpResponseStatusCode, awsStatusCode)); } Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource)) @@ -354,7 +355,7 @@ private void validateMetricsGeneratedForAttributeStatusCode( private void validateMetricsGeneratedForStatusDataOk( long? httpStatusCode, ExpectedStatusMetric expectedStatusMetric) { spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); - spanDataMock.SetTag(AttributeHttpStatusCode, httpStatusCode); + spanDataMock.SetTag(AttributeHttpResponseStatusCode, httpStatusCode); spanDataMock.SetStatus(ActivityStatusCode.Ok); setLatency(); Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); @@ -369,7 +370,7 @@ private void validateMetricsGeneratedForStatusDataOk( private void validateMetricsGeneratedForStatusDataError( long? httpStatusCode, ExpectedStatusMetric expectedStatusMetric) { spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); - spanDataMock.SetTag(AttributeHttpStatusCode, httpStatusCode); + spanDataMock.SetTag(AttributeHttpResponseStatusCode, httpStatusCode); spanDataMock.SetStatus(ActivityStatusCode.Error); setLatency(); Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); @@ -384,7 +385,7 @@ private void validateMetricsGeneratedForHttpStatusCode( long? httpStatusCode, ExpectedStatusMetric expectedStatusMetric) { spanDataMock = activitySource.StartActivity("test", ActivityKind.Producer); - spanDataMock.SetTag(AttributeHttpStatusCode, httpStatusCode); + spanDataMock.SetTag(AttributeHttpResponseStatusCode, httpStatusCode); setLatency(); Dictionary expectAttributes = buildMetricAttributes(true, spanDataMock); Generator.Setup(g => g.GenerateMetricAttributeMapFromSpan(spanDataMock, resource))