diff --git a/docs/metrics/README.md b/docs/metrics/README.md index a57d92e9b61..db91e62f529 100644 --- a/docs/metrics/README.md +++ b/docs/metrics/README.md @@ -86,7 +86,7 @@ static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0"); | [Asynchronous Gauge](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#asynchronous-gauge) | [`ObservableGauge`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.observablegauge-1) | | [Asynchronous UpDownCounter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#asynchronous-updowncounter) | [`ObservableUpDownCounter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.observableupdowncounter-1) | | [Counter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#counter) | [`Counter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.counter-1) | - | [Gauge](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#gauge) (experimental) | N/A | + | [Gauge](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#gauge) | [`Gauge`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.gauge-1) | | [Histogram](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#histogram) | [`Histogram`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.histogram-1) | | [UpDownCounter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#updowncounter) | [`UpDownCounter`](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.updowncounter-1) | diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 47b6536a5e5..893b9c6c4d7 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -31,6 +31,11 @@ Notes](../../RELEASENOTES.md). See [#5854](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5854) for details. +* Added support for collecting metrics emitted via the .NET 9 + [Gauge<T>](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.gauge-1) + API. + ([#5867](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5867)) + ## 1.9.0 Released 2024-Jun-14 diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 7801c2dd452..37dd3198b71 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -117,6 +117,12 @@ internal Metric( aggType = AggregationType.DoubleGauge; this.MetricType = MetricType.DoubleGauge; } + else if (instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge)) + { + aggType = AggregationType.DoubleGauge; + this.MetricType = MetricType.DoubleGauge; + } else if (instrumentIdentity.InstrumentType == typeof(ObservableGauge) || instrumentIdentity.InstrumentType == typeof(ObservableGauge) || instrumentIdentity.InstrumentType == typeof(ObservableGauge) @@ -125,6 +131,14 @@ internal Metric( aggType = AggregationType.LongGauge; this.MetricType = MetricType.LongGauge; } + else if (instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge) + || instrumentIdentity.InstrumentType == typeof(Gauge)) + { + aggType = AggregationType.LongGauge; + this.MetricType = MetricType.LongGauge; + } else if (instrumentIdentity.IsHistogram) { var explicitBucketBounds = instrumentIdentity.HistogramBucketBounds; diff --git a/src/OpenTelemetry/Metrics/Reader/MetricReader.cs b/src/OpenTelemetry/Metrics/Reader/MetricReader.cs index fce60c50f3a..52353a1155a 100644 --- a/src/OpenTelemetry/Metrics/Reader/MetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReader.cs @@ -27,6 +27,7 @@ public abstract partial class MetricReader : IDisposable // Temporality is not defined for gauges, so this does not really affect anything. var type when type == typeof(ObservableGauge<>) => AggregationTemporality.Delta, + var type when type == typeof(Gauge<>) => AggregationTemporality.Delta, var type when type == typeof(UpDownCounter<>) => AggregationTemporality.Cumulative, var type when type == typeof(ObservableUpDownCounter<>) => AggregationTemporality.Cumulative, diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index 41726da14d7..d4088bb0d1a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -1613,6 +1613,93 @@ public void UnsupportedMetricInstrument() Assert.Empty(exportedItems); } + [Fact] + public void GaugeIsExportedCorrectly() + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var gauge = meter.CreateGauge(name: "NoiseLevel", unit: "dB", description: "Background Noise Level"); + gauge.Record(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("Background Noise Level", metric.Description); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + var lastValue = metricPoints[0].GetGaugeLastValueLong(); + Assert.Equal(10, lastValue); + } + + [Theory] + [InlineData(MetricReaderTemporalityPreference.Cumulative)] + [InlineData(MetricReaderTemporalityPreference.Delta)] + public void GaugeHandlesNoNewMeasurementsCorrectlyWithTemporality(MetricReaderTemporalityPreference temporalityPreference) + { + var exportedMetrics = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}"); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedMetrics, metricReaderOptions => + { + metricReaderOptions.TemporalityPreference = temporalityPreference; + })); + + var noiseLevelGauge = meter.CreateGauge(name: "NoiseLevel", unit: "dB", description: "Background Noise Level"); + noiseLevelGauge.Record(10); + + // Force a flush to export the recorded data + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Validate first export / flush + var firstMetric = exportedMetrics[0]; + var firstMetricPoints = new List(); + foreach (ref readonly var metricPoint in firstMetric.GetMetricPoints()) + { + firstMetricPoints.Add(metricPoint); + } + + Assert.Single(firstMetricPoints); + var firstMetricPoint = firstMetricPoints[0]; + Assert.Equal(10, firstMetricPoint.GetGaugeLastValueLong()); + + // Flush the metrics again without recording any new measurements + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Validate second export / flush + if (temporalityPreference == MetricReaderTemporalityPreference.Cumulative) + { + // For cumulative temporality, data points should still be collected + // without any new measurements + Assert.Equal(2, exportedMetrics.Count); + var secondMetric = exportedMetrics[1]; + var secondMetricPoints = new List(); + foreach (ref readonly var metricPoint in secondMetric.GetMetricPoints()) + { + secondMetricPoints.Add(metricPoint); + } + + Assert.Single(secondMetricPoints); + var secondMetricPoint = secondMetricPoints[0]; + Assert.Equal(10, secondMetricPoint.GetGaugeLastValueLong()); + } + else if (temporalityPreference == MetricReaderTemporalityPreference.Delta) + { + // For delta temporality, no new metric should be collected + Assert.Single(exportedMetrics); + } + } + internal static IConfiguration BuildConfiguration(bool emitOverflowAttribute, bool shouldReclaimUnusedMetricPoints) { var configurationData = new Dictionary();