Skip to content

Commit bad5402

Browse files
authored
[OTEL] Metrics API Support - Collection Implementation (#7511)
## Summary of changes This PR implements the metrics collection foundation for OpenTelemetry Metrics API supportKey additions include: - MetricReader: Component for collecting instrument measurements and aggregating them - MetricReaderHandler: High-performance measurement processing with proper aggregation (Sum, Last Value, Histogram) - MetricPoint: Thread-safe data structure for metric aggregation with histogram bucket support - Testing: Unit test covering all 7 instrument types (4 sync + 3 async) with exact OTLP value verification This is the second of a series of 3 PRs to complete the OpenTelemetry Metrics API support. 1. [[OTEL] Metrics API Support - Configurations & Telemetry #7420](#7420) 2. [[OTEL] Metrics API Support - Collection Implementation #7511](#7511) <- You are here. 3. [[OTEL] Metrics API Support - Metrics Reader & OTLP Exporter #7514](#7514) ## Reason for change The [RFC] OpenTelemetry Metrics API support requires implementing metrics collection that's compatible with the OpenTelemetry data model. This PR provides the collection foundation that will enable customers to use .NET's `System.Diagnostics.Metrics` API with Datadog as a drop-in replacement for the OpenTelemetry SDK. Business Impact: Enables the collection phase of OpenTelemetry Metrics API support, laying the groundwork for OTLP export in the next PR. ## Implementation details ✅ Metrics Collection Components: - MetricReader - RFC-compliant component for initialization and async collection - MetricReaderHandler - Measurement processing and aggregation - MetricPoint - Aggregation data structure with histogram bucket support ✅ Aggregation Mapping: - Counter/Asynchronous Counter → Sum Aggregation → Sum Metric Point ✅ - UpDownCounter/Asynchronous UpDownCounter → Sum Aggregation → Sum Metric Point ✅ - Gauge/Asynchronous Gauge → Last Value Aggregation → Gauge Metric Point ✅ - Histogram → Explicit Bucket Histogram Aggregation → Histogram Metric Point ✅ ## Test coverage Added unit test `MeterListenerCapturesAllMetrics()` that verifies: - All 7 instrument types (Counter, Histogram, Gauge, UpDownCounter + 3 Async variants) - Value verification matching OTLP snapshot expectations Histogram bucket logic verification - Sync + Async instrument collection - Tag capture and temporality settings ## Other details Jira: [APMAPI-1572](https://datadoghq.atlassian.net/browse/APMAPI-1572) <!-- Fixes #{issue} --> <!-- ⚠️ Note: Where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. MergeQueue is NOT enabled in this repository. If you have write access to the repo, the PR has 1-2 approvals (see above), and all of the required checks have passed, you can use the Squash and Merge button to merge the PR. If you don't have write access, or you need help, reach out in the #apm-dotnet channel in Slack. --> [APMAPI-1572]: https://datadoghq.atlassian.net/browse/APMAPI-1572?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 792a204 commit bad5402

File tree

12 files changed

+1153
-7
lines changed

12 files changed

+1153
-7
lines changed

tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@
239239
<assembly fullname="System.Diagnostics.DiagnosticSource">
240240
<type fullname="System.Diagnostics.DiagnosticListener" />
241241
<type fullname="System.Diagnostics.DistributedContextPropagator" />
242+
<type fullname="System.Diagnostics.Metrics.Instrument" />
243+
<type fullname="System.Diagnostics.Metrics.MeasurementCallback`1" />
244+
<type fullname="System.Diagnostics.Metrics.Meter" />
245+
<type fullname="System.Diagnostics.Metrics.MeterListener" />
242246
</assembly>
243247
<assembly fullname="System.Diagnostics.Process">
244248
<type fullname="System.Diagnostics.Process" />

tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,25 +378,23 @@ internal static void InitializeNoNativeParts(Stopwatch sw = null)
378378
{
379379
if (Tracer.Instance.Settings.OpenTelemetryMetricsEnabled)
380380
{
381-
Log.Debug("Initializing OTel Metrics Exporter.");
382381
if (Tracer.Instance.Settings.OpenTelemetryMeterNames.Length > 0)
383382
{
383+
Log.Debug("Initializing OTel Metrics Exporter.");
384384
OTelMetrics.OtlpMetricsExporter.Initialize();
385-
}
386-
else
387-
{
388-
Log.Debug("No meters were found for DD_METRICS_OTEL_METER_NAMES, OTel Metrics Exporter won't be initialized.");
385+
Log.Debug("Initializing OTel Metrics Reader.");
386+
OTelMetrics.MetricReader.Initialize();
389387
}
390388
}
391389
}
392390
catch (Exception ex)
393391
{
394-
Log.Error(ex, "Error initializing OTel Metrics Exporter");
392+
Log.Error(ex, "Error initializing OTel Metrics Reader");
395393
}
396394
#else
397395
if (Tracer.Instance.Settings.OpenTelemetryMetricsEnabled)
398396
{
399-
Log.Information("Unable to initialize OTel Metrics collection, this is only available starting with .NET 6.0..");
397+
Log.Information("Unable to initialize OTel Metrics collection, this is only available starting with .NET 6.0.");
400398
}
401399
#endif
402400

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// <copyright file="AggregationTemporality.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if NET6_0_OR_GREATER
7+
8+
#nullable enable
9+
10+
namespace Datadog.Trace.OTelMetrics;
11+
12+
/// <summary>
13+
/// Represents the aggregation temporality of a metric.
14+
/// </summary>
15+
public enum AggregationTemporality
16+
{
17+
/// <summary>
18+
/// Delta temporality, representing changes since the last measurement.
19+
/// </summary>
20+
Delta = 0,
21+
22+
/// <summary>
23+
/// Cumulative temporality, representing the total value since the start.
24+
/// </summary>
25+
Cumulative = 1,
26+
}
27+
#endif
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// <copyright file="InstrumentType.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if NET6_0_OR_GREATER
7+
8+
#nullable enable
9+
10+
namespace Datadog.Trace.OTelMetrics;
11+
12+
/// <summary>
13+
/// Represents the type of OpenTelemetry instrument
14+
/// </summary>
15+
internal enum InstrumentType
16+
{
17+
/// <summary>
18+
/// Counter instrument - monotonic, additive
19+
/// </summary>
20+
Counter = 0,
21+
22+
/// <summary>
23+
/// UpDownCounter instrument - non-monotonic, additive
24+
/// </summary>
25+
UpDownCounter = 1,
26+
27+
/// <summary>
28+
/// Histogram instrument - statistical distribution
29+
/// </summary>
30+
Histogram = 2,
31+
32+
/// <summary>
33+
/// Asynchronous Counter instrument - monotonic, additive, callback-based
34+
/// </summary>
35+
ObservableCounter = 3,
36+
37+
/// <summary>
38+
/// Asynchronous UpDownCounter instrument - non-monotonic, additive, callback-based
39+
/// </summary>
40+
ObservableUpDownCounter = 4,
41+
42+
/// <summary>
43+
/// Gauge instrument - non-additive, last value
44+
/// </summary>
45+
Gauge = 5,
46+
47+
/// <summary>
48+
/// Asynchronous Gauge instrument - non-additive, last value, callback-based
49+
/// </summary>
50+
ObservableGauge = 6
51+
}
52+
#endif
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// <copyright file="MetricPoint.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if NET6_0_OR_GREATER
7+
8+
#nullable enable
9+
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Threading;
13+
14+
namespace Datadog.Trace.OTelMetrics;
15+
16+
internal class MetricPoint(string instrumentName, string meterName, InstrumentType instrumentType, AggregationTemporality? temporality, Dictionary<string, object?> tags)
17+
{
18+
internal static readonly double[] DefaultHistogramBounds = [0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000];
19+
private readonly long[] _runningBucketCounts = instrumentType == InstrumentType.Histogram ? new long[DefaultHistogramBounds.Length + 1] : [];
20+
private readonly object _histogramLock = new();
21+
private long _runningCountValue;
22+
private double _runningDoubleValue;
23+
private double _runningMin = double.PositiveInfinity;
24+
private double _runningMax = double.NegativeInfinity;
25+
26+
public string InstrumentName { get; } = instrumentName;
27+
28+
public string MeterName { get; } = meterName;
29+
30+
public InstrumentType InstrumentType { get; } = instrumentType;
31+
32+
public AggregationTemporality? AggregationTemporality { get; } = temporality;
33+
34+
public Dictionary<string, object?> Tags { get; } = tags;
35+
36+
internal long RunningCount => _runningCountValue;
37+
38+
internal double RunningSum => _runningDoubleValue;
39+
40+
internal double RunningMin => _runningMin;
41+
42+
internal double RunningMax => _runningMax;
43+
44+
internal long[] RunningBucketCounts => _runningBucketCounts;
45+
46+
internal void UpdateCounter(double value)
47+
{
48+
lock (_histogramLock)
49+
{
50+
_runningDoubleValue += value;
51+
}
52+
}
53+
54+
internal void UpdateGauge(double value)
55+
{
56+
Interlocked.Exchange(ref _runningDoubleValue, value);
57+
}
58+
59+
internal void UpdateHistogram(double value)
60+
{
61+
var bucketIndex = FindBucketIndex(value);
62+
63+
lock (_histogramLock)
64+
{
65+
unchecked
66+
{
67+
_runningCountValue++;
68+
_runningDoubleValue += value;
69+
_runningBucketCounts[bucketIndex]++;
70+
}
71+
72+
_runningMin = Math.Min(_runningMin, value);
73+
_runningMax = Math.Max(_runningMax, value);
74+
}
75+
}
76+
77+
private static int FindBucketIndex(double value)
78+
{
79+
for (var i = 0; i < DefaultHistogramBounds.Length; i++)
80+
{
81+
if (value <= DefaultHistogramBounds[i])
82+
{
83+
return i;
84+
}
85+
}
86+
87+
return DefaultHistogramBounds.Length;
88+
}
89+
}
90+
#endif
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// <copyright file="MetricReader.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if NET6_0_OR_GREATER
7+
8+
#nullable enable
9+
10+
using System;
11+
using System.Threading;
12+
using Datadog.Trace.Logging;
13+
14+
namespace Datadog.Trace.OTelMetrics;
15+
16+
internal static class MetricReader
17+
{
18+
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(MetricReader));
19+
20+
private static System.Diagnostics.Metrics.MeterListener? _meterListenerInstance;
21+
private static int _initialized;
22+
private static int _stopped;
23+
24+
public static bool IsRunning =>
25+
Interlocked.CompareExchange(ref _initialized, 1, 1) == 1 &&
26+
Interlocked.CompareExchange(ref _stopped, 0, 0) == 0;
27+
28+
public static void Initialize()
29+
{
30+
if (Interlocked.CompareExchange(ref _initialized, 1, 0) == 1)
31+
{
32+
return;
33+
}
34+
35+
var meterListener = new System.Diagnostics.Metrics.MeterListener();
36+
37+
#if NET6_0 || NET7_0 || NET8_0
38+
// Ensures instruments are fully de-registered on Dispose() for 6–8
39+
// Static lambda => no captures/allocations
40+
meterListener.MeasurementsCompleted = static (_, __) => { };
41+
#endif
42+
43+
meterListener.InstrumentPublished = MetricReaderHandler.OnInstrumentPublished;
44+
45+
meterListener.SetMeasurementEventCallback<byte>(MetricReaderHandler.OnMeasurementRecordedByte);
46+
meterListener.SetMeasurementEventCallback<short>(MetricReaderHandler.OnMeasurementRecordedShort);
47+
meterListener.SetMeasurementEventCallback<int>(MetricReaderHandler.OnMeasurementRecordedInt);
48+
meterListener.SetMeasurementEventCallback<long>(MetricReaderHandler.OnMeasurementRecordedLong);
49+
meterListener.SetMeasurementEventCallback<float>(MetricReaderHandler.OnMeasurementRecordedFloat);
50+
meterListener.SetMeasurementEventCallback<double>(MetricReaderHandler.OnMeasurementRecordedDouble);
51+
52+
meterListener.Start();
53+
54+
Interlocked.Exchange(ref _meterListenerInstance, meterListener);
55+
Interlocked.Exchange(ref _stopped, 0);
56+
57+
Log.Debug("MeterListener initialized successfully.");
58+
}
59+
60+
public static void Stop()
61+
{
62+
var listener = Interlocked.Exchange(ref _meterListenerInstance, null);
63+
if (listener is IDisposable disposableListener)
64+
{
65+
disposableListener.Dispose();
66+
Interlocked.Exchange(ref _stopped, 1);
67+
Interlocked.Exchange(ref _initialized, 0);
68+
Log.Debug("MeterListener stopped.");
69+
}
70+
}
71+
72+
internal static void CollectObservableInstruments()
73+
{
74+
if (_meterListenerInstance != null)
75+
{
76+
try
77+
{
78+
_meterListenerInstance.RecordObservableInstruments();
79+
}
80+
catch (Exception ex)
81+
{
82+
Log.Warning(ex, "Error collecting observable instruments.");
83+
}
84+
}
85+
}
86+
}
87+
#endif
88+

0 commit comments

Comments
 (0)