-
Notifications
You must be signed in to change notification settings - Fork 159
[Runtime Metrics] Support for OTLP Runtime Metrics #8457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b0c9cbc
5c97823
79db8cd
5df066e
e5737b4
855e32f
314f287
f541511
a6b7850
07d2628
e427f9f
151a895
88132cd
df39724
1b582a2
b8baa65
22ed38a
3e0c3b5
5578d15
2b2124a
c662ee4
a8441ae
42368b6
109909d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,6 +165,15 @@ public MetricPoint CreateSnapshotAndReset() | |
| var previousCumulative = double.IsNaN(_lastObservedCumulative) ? 0 : _lastObservedCumulative; | ||
| var delta = _runningDoubleValue - previousCumulative; | ||
|
|
||
| // OTel spec: for monotonic counters (ObservableCounter), a negative delta | ||
| // indicates a counter reset (e.g. process restart). Report currentValue | ||
| // as the delta, as if the previous cumulative was 0. | ||
| // ObservableUpDownCounter is non-monotonic so negative deltas are expected. | ||
| if (delta < 0 && InstrumentType is InstrumentType.ObservableCounter) | ||
| { | ||
| delta = _runningDoubleValue; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had to work this one through to convince myself it's correct, so included here so others don't need to 😅 In an overflow scenario - given that you would have (for example)
Then
So we hit this branch and set
so LGTM!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aay thank you for adding this investigation here! |
||
| } | ||
|
|
||
| sumForSnapshot = AggregationTemporality == Metrics.AggregationTemporality.Delta | ||
| ? delta | ||
| : _runningDoubleValue; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // <copyright file="GcPauseTimeReflection.cs" company="Datadog"> | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
| // </copyright> | ||
|
|
||
| #if NET6_0_OR_GREATER | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Reflection; | ||
| using Datadog.Trace.Logging; | ||
|
|
||
| namespace Datadog.Trace.RuntimeMetrics; | ||
|
|
||
| /// <summary> | ||
| /// Shared helper for resolving <c>GC.GetTotalPauseDuration()</c> via reflection. | ||
| /// Used by both the OTLP polyfill and the DogStatsD runtime metrics listener on .NET 6–8. | ||
| /// Each caller is responsible for converting the <see cref="TimeSpan"/> to its preferred unit | ||
| /// (seconds for OTel, milliseconds for DogStatsD). | ||
| /// </summary> | ||
| internal static class GcPauseTimeReflection | ||
| { | ||
| private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(GcPauseTimeReflection)); | ||
|
|
||
| /// <summary> | ||
| /// Returns a delegate for <c>GC.GetTotalPauseDuration()</c>, or <c>null</c> if the method | ||
| /// is not available on the current runtime (i.e. .NET versions older than 6.0.21). | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <c>GC.GetTotalPauseDuration()</c> was introduced in .NET 6.0.21: | ||
| /// https://github.com/dotnet/runtime/pull/87143 | ||
| /// This is also what the OTel runtime instrumentation package uses: | ||
| /// https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/5aa6d868/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetrics.cs#L105C40-L107 | ||
| /// We use reflection rather than duck typing because this is a simple static method with no overloads. | ||
| /// </remarks> | ||
| public static Func<TimeSpan>? TryCreateDelegate() | ||
| { | ||
| var version = FrameworkDescription.Instance.RuntimeVersion; | ||
| if (version.Major <= 6 && version is not { Major: 6, Build: >= 21 }) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var methodInfo = typeof(GC).GetMethod("GetTotalPauseDuration", BindingFlags.Public | BindingFlags.Static); | ||
| if (methodInfo is null) | ||
| { | ||
| Log.Debug("GC.GetTotalPauseDuration() is not available on this runtime version; gc pause time will not be reported."); | ||
| return null; | ||
| } | ||
|
|
||
| return methodInfo.CreateDelegate<Func<TimeSpan>>(); | ||
| } | ||
| } | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // <copyright file="MeterObservableUpDownCounterReflection.cs" company="Datadog"> | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. | ||
| // </copyright> | ||
|
|
||
| #if NET6_0_OR_GREATER | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.Metrics; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
|
|
||
| namespace Datadog.Trace.RuntimeMetrics; | ||
|
|
||
| /// <summary> | ||
| /// Resolves <c>Meter.CreateObservableUpDownCounter</c> overloads via reflection. | ||
| /// <c>Datadog.Trace</c> compiles against the net6.0 ref assembly (no UpDownCounter API), but on | ||
| /// .NET 7+ host processes roll-forward provides <c>System.Diagnostics.DiagnosticSource</c> 7.0+ | ||
| /// where the API exists. On .NET 6 hosts callers should fall back to <c>ObservableGauge</c>. | ||
| /// </summary> | ||
| internal static class MeterObservableUpDownCounterReflection | ||
| { | ||
| // Pin to the 4-parameter overloads (string name, Func<...>, string? unit, string? description). | ||
| // .NET 7's DiagnosticSource 7.0 only ships the 4-param shape; .NET 8+ adds 5-param overloads with | ||
| // a required `tags` argument alongside the original 4-param ones. Pinning to Length == 4 picks | ||
| // an overload that exists on every supported host and avoids ambiguity on .NET 8+. | ||
| private static readonly MethodInfo? FuncOfTMethod = FindOverload(secondParam => | ||
| secondParam.IsGenericType | ||
| && secondParam.GetGenericTypeDefinition() == typeof(Func<>) | ||
| && secondParam.GetGenericArguments()[0].IsGenericMethodParameter); | ||
|
|
||
| private static readonly MethodInfo? FuncOfMeasurementsMethod = FindOverload(secondParam => | ||
| { | ||
| if (!secondParam.IsGenericType || secondParam.GetGenericTypeDefinition() != typeof(Func<>)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| var inner = secondParam.GetGenericArguments()[0]; | ||
| if (!inner.IsGenericType || inner.GetGenericTypeDefinition() != typeof(IEnumerable<>)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| var measurement = inner.GetGenericArguments()[0]; | ||
| return measurement.IsGenericType && measurement.GetGenericTypeDefinition() == typeof(Measurement<>); | ||
| }); | ||
|
|
||
| public static bool TryRegister<T>(Meter meter, string name, Func<T> observeValue, string unit, string description) | ||
| where T : struct | ||
| => Invoke(FuncOfTMethod, meter, typeof(T), name, observeValue, unit, description); | ||
|
|
||
| public static bool TryRegisterMulti<T>(Meter meter, string name, Func<IEnumerable<Measurement<T>>> observeValues, string unit, string description) | ||
| where T : struct | ||
| => Invoke(FuncOfMeasurementsMethod, meter, typeof(T), name, observeValues, unit, description); | ||
|
|
||
| private static bool Invoke(MethodInfo? method, Meter meter, Type genericArg, string name, object observe, string unit, string description) | ||
| { | ||
| if (method is null) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| method.MakeGenericMethod(genericArg).Invoke(meter, [name, observe, unit, description]); | ||
| return true; | ||
| } | ||
|
|
||
| private static MethodInfo? FindOverload(Func<Type, bool> matchesSecondParam) => | ||
| typeof(Meter).GetMethods(BindingFlags.Public | BindingFlags.Instance) | ||
| .FirstOrDefault(m => | ||
| m.Name == "CreateObservableUpDownCounter" | ||
| && m.IsGenericMethodDefinition | ||
| && m.GetParameters() is { Length: 4 } p | ||
| && p[0].ParameterType == typeof(string) | ||
| && matchesSecondParam(p[1].ParameterType)); | ||
| } | ||
|
|
||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a thought, and not strictly related to this PR: Do we need a similar guard somewhere for negative values in
InstrumentTrype.Countertoo? 🤔 I don't think it needs to be right here (because this method is all about observable counters), but I believe we could have the same wrapping case to handle.I think we may also need to handle this in our statsd case too - I'll look into it!