Skip to content

Commit bbda41b

Browse files
authored
Add Application Signals runtime metrics with feature disabled (#148)
1 parent 976ffd0 commit bbda41b

File tree

5 files changed

+291
-53
lines changed

5 files changed

+291
-53
lines changed

src/AWS.Distro.OpenTelemetry.AutoInstrumentation/AwsMetricAttributeGenerator.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@ internal class AwsMetricAttributeGenerator : IMetricAttributeGenerator
5151
// Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
5252
private static readonly string GraphQL = "graphql";
5353

54-
// As per https://opentelemetry.io/docs/specs/semconv/resource/#service
55-
// If service name is not specified, SDK defaults the service name starting with unknown_service
56-
private static readonly string OtelUnknownServicePrefix = "unknown_service";
57-
5854
/// <inheritdoc/>
5955
public virtual Dictionary<string, ActivityTagsCollection> GenerateMetricAttributeMapFromSpan(Activity span, Resource resource)
6056
{
@@ -100,10 +96,10 @@ private ActivityTagsCollection GenerateDependencyMetricAttributes(Activity span,
10096
private static void SetService(Resource resource, Activity span, ActivityTagsCollection attributes)
10197
#pragma warning restore SA1204 // Static elements should appear before instance elements
10298
{
103-
string? service = (string?)resource.Attributes.FirstOrDefault(attribute => attribute.Key == AttributeServiceName).Value;
99+
string? service = (string?)resource.Attributes.FirstOrDefault(attribute => attribute.Key == AttributeAWSLocalService).Value;
104100

105101
// In practice the service name is never null, but we can be defensive here.
106-
if (service == null || service.StartsWith(OtelUnknownServicePrefix))
102+
if (service == null)
107103
{
108104
LogUnknownAttribute(AttributeAWSLocalService, span);
109105
service = UnknownService;

src/AWS.Distro.OpenTelemetry.AutoInstrumentation/Plugin.cs

Lines changed: 117 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class Plugin
4141
private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider()));
4242
private static readonly ILogger Logger = Factory.CreateLogger<Plugin>();
4343
private static readonly string ApplicationSignalsExporterEndpointConfig = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT";
44+
private static readonly string MetricExporterConfig = "OTEL_METRICS_EXPORTER";
4445
private static readonly string MetricExportIntervalConfig = "OTEL_METRIC_EXPORT_INTERVAL";
4546
private static readonly int DefaultMetricExportInterval = 60000;
4647
private static readonly string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL";
@@ -59,6 +60,11 @@ public class Plugin
5960

6061
private static readonly string FormatOtelSampledTracesBinaryPrefix = "T1S";
6162
private static readonly string FormatOtelUnSampledTracesBinaryPrefix = "T1U";
63+
private static readonly string RuntimeMetricMeterName = "OpenTelemetry.Instrumentation.Runtime";
64+
65+
// As per https://opentelemetry.io/docs/specs/semconv/resource/#service
66+
// If service name is not specified, SDK defaults the service name starting with unknown_service
67+
private static readonly string OtelUnknownServicePrefix = "unknown_service";
6268

6369
private static readonly int LambdaSpanExportBatchSize = 10;
6470

@@ -119,27 +125,9 @@ public void TracerProviderInitialized(TracerProvider tracerProvider)
119125
// Disable Application Metrics for Lambda environment
120126
if (!AwsSpanProcessingUtil.IsLambdaEnvironment())
121127
{
122-
string? intervalConfigString = System.Environment.GetEnvironmentVariable(MetricExportIntervalConfig);
123-
int exportInterval = DefaultMetricExportInterval;
124-
try
125-
{
126-
int parsedExportInterval = Convert.ToInt32(intervalConfigString);
127-
exportInterval = parsedExportInterval != 0 ? parsedExportInterval : DefaultMetricExportInterval;
128-
}
129-
catch (Exception)
130-
{
131-
Logger.Log(LogLevel.Trace, "Could not convert OTEL_METRIC_EXPORT_INTERVAL to integer. Using default value 60000.");
132-
}
133-
134-
if (exportInterval.CompareTo(DefaultMetricExportInterval) > 0)
135-
{
136-
exportInterval = DefaultMetricExportInterval;
137-
Logger.Log(LogLevel.Information, "AWS Application Signals metrics export interval capped to {0}", exportInterval);
138-
}
139-
140128
// https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md#enable-metric-exporter
141129
// for setting the temporatityPref.
142-
var metricReader = new PeriodicExportingMetricReader(this.ApplicationSignalsExporterProvider(), exportInterval)
130+
var metricReader = new PeriodicExportingMetricReader(this.CreateApplicationSignalsMetricExporter(), GetMetricExportInterval())
143131
{
144132
TemporalityPreference = MetricReaderTemporalityPreference.Delta,
145133
};
@@ -229,6 +217,40 @@ public TracerProviderBuilder AfterConfigureTracerProvider(TracerProviderBuilder
229217
return builder;
230218
}
231219

220+
/// <summary>
221+
/// // To configure metrics SDK after Auto Instrumentation configured SDK
222+
/// </summary>
223+
/// <param name="builder">The metric provider builder</param>
224+
/// <returns>The configured metric provider builder</returns>
225+
public MeterProviderBuilder AfterConfigureMeterProvider(MeterProviderBuilder builder)
226+
{
227+
if (!this.IsApplicationSignalsRuntimeEnabled())
228+
{
229+
return builder;
230+
}
231+
232+
var exporters = System.Environment.GetEnvironmentVariable(MetricExporterConfig);
233+
if (!string.IsNullOrEmpty(exporters) && exporters.Contains("none"))
234+
{
235+
Logger.Log(LogLevel.Information, "Install runtime metric filter in metrics collection.");
236+
builder.AddView(instrument => instrument.Meter.Name == RuntimeMetricMeterName
237+
? null
238+
: MetricStreamConfiguration.Drop);
239+
}
240+
241+
var runtimeScopeName = new HashSet<string>() { RuntimeMetricMeterName };
242+
var metricReader = new PeriodicExportingMetricReader(
243+
this.CreateScopeBasedOtlpMetricExporter(runtimeScopeName), GetMetricExportInterval())
244+
{
245+
TemporalityPreference = MetricReaderTemporalityPreference.Delta,
246+
};
247+
248+
builder.AddReader(metricReader);
249+
Logger.Log(LogLevel.Information, "AWS Application Signals runtime metrics enabled.");
250+
251+
return builder;
252+
}
253+
232254
/// <summary>
233255
/// To configure Resource with resource detectors and <see cref="DistroAttributes"/>
234256
/// Check <see cref="ResourceBuilderCustomizer"/> for more information.
@@ -357,6 +379,58 @@ public void ConfigureTracesOptions(AspNetTraceInstrumentationOptions options)
357379
}
358380
#endif
359381

382+
private static int GetMetricExportInterval()
383+
{
384+
var intervalConfigString = System.Environment.GetEnvironmentVariable(MetricExportIntervalConfig);
385+
var exportInterval = DefaultMetricExportInterval;
386+
try
387+
{
388+
var parsedExportInterval = Convert.ToInt32(intervalConfigString);
389+
exportInterval = parsedExportInterval != 0 ? parsedExportInterval : DefaultMetricExportInterval;
390+
}
391+
catch (Exception)
392+
{
393+
Logger.Log(LogLevel.Warning, "Could not convert OTEL_METRIC_EXPORT_INTERVAL to integer. Using default value 60000.");
394+
}
395+
396+
if (exportInterval.CompareTo(DefaultMetricExportInterval) > 0)
397+
{
398+
exportInterval = DefaultMetricExportInterval;
399+
Logger.Log(LogLevel.Information, "AWS Application Signals metrics export interval capped to {0}", exportInterval);
400+
}
401+
402+
return exportInterval;
403+
}
404+
405+
private static void ConfigureOtlpExporterOptions(OtlpExporterOptions options)
406+
{
407+
var applicationSignalsEndpoint = System.Environment.GetEnvironmentVariable(ApplicationSignalsExporterEndpointConfig);
408+
var protocolString = System.Environment.GetEnvironmentVariable(DefaultProtocolEnvVarName) ?? "http/protobuf";
409+
OtlpExportProtocol protocol;
410+
411+
switch (protocolString)
412+
{
413+
case "http/protobuf":
414+
applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4316/v1/metrics";
415+
protocol = OtlpExportProtocol.HttpProtobuf;
416+
break;
417+
case "grpc":
418+
applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4315";
419+
protocol = OtlpExportProtocol.Grpc;
420+
break;
421+
default:
422+
throw new NotSupportedException("Unsupported AWS Application Signals export protocol: " + protocolString);
423+
}
424+
425+
options.Endpoint = new Uri(applicationSignalsEndpoint);
426+
options.Protocol = protocol;
427+
428+
Logger.Log(
429+
LogLevel.Debug, "AWS Application Signals export protocol: %{0}", options.Protocol);
430+
Logger.Log(
431+
LogLevel.Debug, "AWS Application Signals export endpoint: %{0}", options.Endpoint);
432+
}
433+
360434
// This new function runs the sampler a second time after the needed attributes (such as UrlPath and HttpTarget)
361435
// are finally available from the http instrumentation libraries. The sampler hooked into the Opentelemetry SDK
362436
// runs right before any activity is started so for the purposes of our X-Ray sampler, that isn't work and breaks
@@ -401,9 +475,23 @@ private bool IsApplicationSignalsEnabled()
401475
return System.Environment.GetEnvironmentVariable(ApplicationSignalsEnabledConfig) == "true";
402476
}
403477

478+
private bool IsApplicationSignalsRuntimeEnabled()
479+
{
480+
return false;
481+
}
482+
404483
private ResourceBuilder ResourceBuilderCustomizer(ResourceBuilder builder)
405484
{
406485
builder.AddAttributes(DistroAttributes);
486+
var resource = builder.Build();
487+
var serviceName = (string?)resource.Attributes.FirstOrDefault(attr => attr.Key == ResourceSemanticConventions.AttributeServiceName).Value;
488+
if (serviceName == null || serviceName.StartsWith(OtelUnknownServicePrefix))
489+
{
490+
Logger.Log(LogLevel.Warning, "No valid service name provided.");
491+
serviceName = AwsSpanProcessingUtil.UnknownService;
492+
}
493+
494+
builder.AddAttributes(new Dictionary<string, object> { { AwsAttributeKeys.AttributeAWSLocalService, serviceName } });
407495

408496
// ResourceDetectors are enabled by default. Adding config to be able to disable during local testing
409497
var resourceDetectorsEnabled = System.Environment.GetEnvironmentVariable(ResourceDetectorEnableConfig) ?? "true";
@@ -428,39 +516,21 @@ private ResourceBuilder ResourceBuilderCustomizer(ResourceBuilder builder)
428516
return builder;
429517
}
430518

431-
private OtlpMetricExporter ApplicationSignalsExporterProvider()
519+
private OtlpMetricExporter CreateApplicationSignalsMetricExporter()
432520
{
433521
var options = new OtlpExporterOptions();
434-
435-
string? applicationSignalsEndpoint = System.Environment.GetEnvironmentVariable(ApplicationSignalsExporterEndpointConfig);
436-
string? protocolString = System.Environment.GetEnvironmentVariable(DefaultProtocolEnvVarName) ?? "http/protobuf";
437-
OtlpExportProtocol protocol;
438-
if (protocolString == "http/protobuf")
439-
{
440-
applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4316/v1/metrics";
441-
protocol = OtlpExportProtocol.HttpProtobuf;
442-
}
443-
else if (protocolString == "grpc")
444-
{
445-
applicationSignalsEndpoint = applicationSignalsEndpoint ?? "http://localhost:4315";
446-
protocol = OtlpExportProtocol.Grpc;
447-
}
448-
else
449-
{
450-
throw new NotSupportedException("Unsupported AWS Application Signals export protocol: " + protocolString);
451-
}
452-
453-
options.Endpoint = new Uri(applicationSignalsEndpoint);
454-
options.Protocol = protocol;
455-
456-
Logger.Log(
457-
LogLevel.Debug, "AWS Application Signals export protocol: %{0}", options.Protocol);
458-
Logger.Log(
459-
LogLevel.Debug, "AWS Application Signals export endpoint: %{0}", options.Endpoint);
460-
522+
ConfigureOtlpExporterOptions(options);
461523
return new OtlpMetricExporter(options);
462524
}
463525

526+
private ScopeBasedOtlpMetricExporter CreateScopeBasedOtlpMetricExporter(HashSet<string> registeredScopeNames)
527+
{
528+
var options = new ScopeBasedOtlpMetricExporter.ScopeBasedOtlpExporterOptions();
529+
ConfigureOtlpExporterOptions(options);
530+
options.RegisteredScopeNames = registeredScopeNames;
531+
return new ScopeBasedOtlpMetricExporter(options);
532+
}
533+
464534
private bool HasCustomTracesEndpoint()
465535
{
466536
// detect if running in AWS Lambda environment
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using AWS.Distro.OpenTelemetry.AutoInstrumentation.Logging;
5+
using Microsoft.Extensions.Logging;
6+
using OpenTelemetry;
7+
using OpenTelemetry.Exporter;
8+
using OpenTelemetry.Metrics;
9+
10+
namespace AWS.Distro.OpenTelemetry.AutoInstrumentation;
11+
12+
/// <summary>
13+
/// A custom metric exporter that extends <see cref="OtlpMetricExporter"/>.
14+
/// Exports metrics only for a specific instrumentation scope, filtering based on
15+
/// the configured scope name. This allows selective metric exporting to the OTLP endpoint,
16+
/// limiting exports to metrics that match the specified scope.
17+
/// </summary>
18+
public class ScopeBasedOtlpMetricExporter : OtlpMetricExporter
19+
{
20+
private static readonly ILoggerFactory Factory = LoggerFactory.Create(builder => builder.AddProvider(new ConsoleLoggerProvider()));
21+
private static readonly ILogger Logger = Factory.CreateLogger<ScopeBasedOtlpMetricExporter>();
22+
23+
private readonly HashSet<string> registeredScopedNames;
24+
private readonly Func<Batch<Metric>, ExportResult> exportHandler;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="ScopeBasedOtlpMetricExporter"/> class.
28+
/// </summary>
29+
/// <param name="options">Configuration options for the exporter.</param>
30+
public ScopeBasedOtlpMetricExporter(ScopeBasedOtlpExporterOptions options)
31+
: base(options)
32+
{
33+
this.registeredScopedNames = options.RegisteredScopeNames ?? new HashSet<string>();
34+
this.exportHandler = batch => base.Export(batch);
35+
}
36+
37+
internal ScopeBasedOtlpMetricExporter(
38+
ScopeBasedOtlpExporterOptions options,
39+
Func<Batch<Metric>, ExportResult> exportHandler)
40+
: base(options)
41+
{
42+
this.registeredScopedNames = options.RegisteredScopeNames ?? new HashSet<string>();
43+
this.exportHandler = exportHandler;
44+
}
45+
46+
/// <inheritdoc />
47+
public override ExportResult Export(in Batch<Metric> metrics)
48+
{
49+
var exportingMetrics = new List<Metric>();
50+
foreach (var metric in metrics)
51+
{
52+
if (this.registeredScopedNames.Contains(metric.MeterName))
53+
{
54+
exportingMetrics.Add(metric);
55+
}
56+
}
57+
58+
if (exportingMetrics.Count == 0)
59+
{
60+
Logger.Log(LogLevel.Debug, "No metrics to export.");
61+
return ExportResult.Success;
62+
}
63+
64+
return this.exportHandler.Invoke(new Batch<Metric>(exportingMetrics.ToArray(), exportingMetrics.Count));
65+
}
66+
67+
/// <summary>
68+
/// Scope based OTLP exporter options.
69+
/// </summary>
70+
public class ScopeBasedOtlpExporterOptions : OtlpExporterOptions
71+
{
72+
/// <summary>
73+
/// Gets or sets registered meter names whose metrics will be reserved.
74+
/// </summary>
75+
public HashSet<string>? RegisteredScopeNames { get; set; }
76+
}
77+
}

test/AWS.Distro.OpenTelemetry.AutoInstrumentation.Tests/AwsMetricAttributesGeneratorTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,7 @@ private void UpdateResourceWithServiceName()
11811181
List<KeyValuePair<string, object?>> resourceAttributes = new List<KeyValuePair<string, object?>>
11821182
{
11831183
new (AutoInstrumentation.AwsMetricAttributeGenerator.AttributeServiceName, this.serviceNameValue),
1184+
new (AwsAttributeKeys.AttributeAWSLocalService, this.serviceNameValue),
11841185
};
11851186
this.resource = new Resource(resourceAttributes);
11861187
}

0 commit comments

Comments
 (0)