diff --git a/build/Build.cs b/build/Build.cs index 6bf3d949..c4dcccd8 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -124,6 +124,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..da3fbffa 100644 --- a/sample-applications/integration-test-app/Dockerfile +++ b/sample-applications/integration-test-app/Dockerfile @@ -22,4 +22,8 @@ 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" +ENV OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT="http://otel:4318/v1/metrics" diff --git a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj index 2ef29a31..737673bc 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AWS.OpenTelemetry.AutoInstrumentation.csproj @@ -10,10 +10,12 @@ + - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive 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 b8851f3d..dc31bb05 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/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 new file mode 100644 index 00000000..875cfd07 --- /dev/null +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessor.cs @@ -0,0 +1,130 @@ +// 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 readonly IMetricAttributeGenerator generator; + private readonly 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 + /// 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) 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 + 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..d7073f60 --- /dev/null +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/AwsMetricAttributesSpanProcessorBuilder.cs @@ -0,0 +1,62 @@ +// 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; + } + + /// + /// Creates an instance of AwsMetricAttributesSpanProcessor + /// + /// 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 16fe86db..af8b8e5d 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,17 +97,17 @@ 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; if (httpStatusCode == null) { - attributes.TryGetValue(AttributeHttpStatusCode, out httpStatusCode); + 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)) { @@ -120,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 0ce7b3fe..f2e6e052 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; @@ -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/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 f9402211..d7443095 100644 --- a/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs +++ b/src/AWS.OpenTelemetry.AutoInstrumentation/Plugin.cs @@ -1,8 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; +using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; +using OpenTelemetry.ResourceDetectors.AWS; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -13,12 +18,19 @@ 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 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 DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + /// /// To configure plugin, before OTel SDK configuration is called. /// public void Initializing() public void Initializing() { - // My custom logic here } /// @@ -27,16 +39,53 @@ public void Initializing() /// Provider to configure public void TracerProviderInitialized(TracerProvider tracerProvider) { - // My custom logic here - } + if (this.IsApplicationSignalsEnabled()) + { + tracerProvider.AddProcessor(AttributePropagatingSpanProcessorBuilder.Create().Build()); - /// - /// To access MeterProvider right after MeterProviderBuilder.Build() is executed. - /// - /// Provider to configure - public void MeterProviderInitialized(MeterProvider meterProvider) - { - // My custom logic here + string? intervalConfigString = System.Environment.GetEnvironmentVariable(MetricExportIntervalConfig); + int exportInterval = DefaultMetricExportInterval; + try + { + 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(DefaultMetricExportInterval) > 0) + { + 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) + { + TemporalityPreference = MetricReaderTemporalityPreference.Delta, + }; + + MeterProvider provider = Sdk.CreateMeterProviderBuilder() + .AddReader(metricReader) + .ConfigureResource(builder => this.GetResourceBuilder()) + .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() + : null; + }) + .Build(); + + Resource resource = provider.GetResource(); + BaseProcessor spanMetricsProcessor = AwsSpanMetricsProcessorBuilder.Create(resource).Build(); + tracerProvider.AddProcessor(spanMetricsProcessor); + } } /// @@ -46,7 +95,13 @@ public void MeterProviderInitialized(MeterProvider meterProvider) /// Returns configured builder public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder builder) { - // My custom logic here + if (this.IsApplicationSignalsEnabled()) + { + var resource = this.GetResourceBuilder().Build(); + var processor = AwsMetricAttributesSpanProcessorBuilder.Create(resource).Build(); + builder.AddProcessor(processor); + } + return builder; } @@ -57,67 +112,75 @@ public TracerProviderBuilder BeforeConfigureTracerProvider(TracerProviderBuilder /// Returns configured builder public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder builder) { - // My custom logic here - return builder; - } + if (this.IsApplicationSignalsEnabled()) + { + Logger.Log(LogLevel.Information, "AWS Application Signals enabled"); + Sampler alwaysRecordSampler = AlwaysRecordSampler.Create(SamplerUtil.GetSampler()); + builder.SetSampler(alwaysRecordSampler); + builder.AddXRayTraceId(); + } - /// - /// 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) - { - // My custom logic here - // Find supported options below + return builder; } /// - /// To configure metrics SDK before Auto Instrumentation configured SDK + /// To configure Resource + /// TODO: Add versioning similar to Python /// - /// Provider to configure + /// Provider to configure /// Returns configured builder - public MeterProviderBuilder BeforeConfigureMeterProvider(MeterProviderBuilder builder) + public ResourceBuilder ConfigureResource(ResourceBuilder builder) { - // My custom logic here + Dictionary attributes = new Dictionary + { + { "telemetry.distro.name", "aws-otel-dotnet-instrumentation" }, + }; + builder + .AddAttributes(attributes); return builder; } - /// - /// To configure metrics SDK after Auto Instrumentation configured SDK - /// - /// Provider to configure - /// Returns configured builder - public MeterProviderBuilder AfterConfigureMeterProvider(MeterProviderBuilder builder) + private bool IsApplicationSignalsEnabled() { - // My custom logic here - return builder; + return System.Environment.GetEnvironmentVariable(ApplicationSignalsEnabledConfig) == "true"; } - /// - /// 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) + private ResourceBuilder GetResourceBuilder() { - // My custom logic here - // Find supported options below + return ResourceBuilder.CreateDefault() + .AddDetector(new AWSEC2ResourceDetector()) + .AddDetector(new AWSEKSResourceDetector()) + .AddDetector(new AWSECSResourceDetector()); } - /// - /// To configure Resource - /// - /// Builder to configure - /// Returns configured builder - public ResourceBuilder ConfigureResource(ResourceBuilder builder) + private OtlpMetricExporter ApplicationSignalsExporterProvider() { - // 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; + 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); } } 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); + } + } +} 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)) 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() {