diff --git a/examples/Console/TestMetrics.cs b/examples/Console/TestMetrics.cs index 34824b12461..5b814be1cc8 100644 --- a/examples/Console/TestMetrics.cs +++ b/examples/Console/TestMetrics.cs @@ -27,7 +27,14 @@ internal class TestMetrics { internal static object Run(MetricsOptions options) { - using var meter = new Meter("TestMeter"); + var meterVersion = "1.0"; + var meterTags = new List> + { + new( + "MeterTagKey", + "MeterTagValue"), + }; + using var meter = new Meter("TestMeter", meterVersion, meterTags); var providerBuilder = Sdk.CreateMeterProviderBuilder() .ConfigureResource(r => r.AddService("myservice")) diff --git a/examples/Console/TestOtlpExporter.cs b/examples/Console/TestOtlpExporter.cs index 793d9f11937..af73004b0b0 100644 --- a/examples/Console/TestOtlpExporter.cs +++ b/examples/Console/TestOtlpExporter.cs @@ -33,10 +33,10 @@ internal static object Run(string endpoint, string protocol) * launch the OpenTelemetry Collector with an OTLP receiver, by running: * * - On Unix based systems use: - * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:latest --config=/cfg/otlp-collector-example/config.yaml * * - On Windows use: - * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:0.48.0 --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:latest --config=/cfg/otlp-collector-example/config.yaml * * Open another terminal window at the examples/Console/ directory and * launch the OTLP example by running: diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md index 3255a0371d4..976da186951 100644 --- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +* Add support for Instrumentation Scope Attributes (i.e [Meter + Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + fixing issue + [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). + ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) + ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index e0483f82c9c..fe45b0cf118 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -50,8 +50,8 @@ public override ExportResult Export(in Batch batch) foreach (var metric in batch) { - var msg = new StringBuilder($"\nExport "); - msg.Append(metric.Name); + var msg = new StringBuilder($"\n"); + msg.Append($"Metric Name: {metric.Name}"); if (metric.Description != string.Empty) { msg.Append(", "); @@ -75,6 +75,15 @@ public override ExportResult Export(in Batch batch) this.WriteLine(msg.ToString()); + foreach (var meterTag in metric.MeterTags) + { + this.WriteLine("\tMeter Tags:"); + if (ConsoleTagTransformer.Instance.TryTransformTag(meterTag, out var result)) + { + this.WriteLine($"\t\t{result}"); + } + } + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { string valueDisplay = string.Empty; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 71559a538c6..62c49881578 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -15,6 +15,12 @@ accepts a `name` parameter to support named options. ([#4916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4916)) +* Add support for Instrumentation Scope Attributes (i.e [Meter + Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + fixing issue + [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). + ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) + ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index 9c20d38cb78..95ad61f286d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -58,7 +58,7 @@ internal static void AddMetrics( var meterName = metric.MeterName; if (!metricsByLibrary.TryGetValue(meterName, out var scopeMetrics)) { - scopeMetrics = GetMetricListFromPool(meterName, metric.MeterVersion); + scopeMetrics = GetMetricListFromPool(meterName, metric.MeterVersion, metric.MeterTags); metricsByLibrary.Add(meterName, scopeMetrics); resourceMetrics.ScopeMetrics.Add(scopeMetrics); @@ -85,7 +85,7 @@ internal static void Return(this OtlpCollector.ExportMetricsServiceRequest reque } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static OtlpMetrics.ScopeMetrics GetMetricListFromPool(string name, string version) + internal static OtlpMetrics.ScopeMetrics GetMetricListFromPool(string name, string version, IEnumerable> meterTags) { if (!MetricListPool.TryTake(out var metrics)) { @@ -97,11 +97,21 @@ internal static OtlpMetrics.ScopeMetrics GetMetricListFromPool(string name, stri Version = version ?? string.Empty, // NRE throw by proto }, }; + + if (meterTags != null) + { + AddAttributes(meterTags, metrics.Scope.Attributes); + } } else { metrics.Scope.Name = name; metrics.Scope.Version = version ?? string.Empty; + if (meterTags != null) + { + metrics.Scope.Attributes.Clear(); + AddAttributes(meterTags, metrics.Scope.Attributes); + } } return metrics; @@ -368,6 +378,17 @@ private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField> meterTags, RepeatedField attributes) + { + foreach (var tag in meterTags) + { + if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result)) + { + attributes.Add(result); + } + } + } + /* [MethodImpl(MethodImplOptions.AggressiveInlining)] private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) diff --git a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..01e38e9263f 100644 --- a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +OpenTelemetry.Metrics.Metric.MeterTags.get -> System.Collections.Generic.IEnumerable>? diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index acecd41105e..79926bb48d2 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -58,6 +58,12 @@ implementationFactory)`. ([#4916](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4916)) +* Add support for Instrumentation Scope Attributes (i.e [Meter + Tags](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter.tags)), + fixing issue + [#4563](https://github.com/open-telemetry/opentelemetry-dotnet/issues/4563). + ([#5089](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5089)) + ## 1.7.0-alpha.1 Released 2023-Oct-16 diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index fc9d2bb9099..7deba4b36e9 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -207,6 +207,11 @@ internal Metric( /// public string MeterVersion => this.InstrumentIdentity.MeterVersion; + /// + /// Gets the attributes (tags) for the metric stream. + /// + public IEnumerable>? MeterTags => this.InstrumentIdentity.MeterTags; + /// /// Gets the for the metric stream. /// diff --git a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs index a8a42f57708..cdaca57db09 100644 --- a/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs +++ b/src/OpenTelemetry/Metrics/MetricStreamIdentity.cs @@ -27,6 +27,7 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me { this.MeterName = instrument.Meter.Name; this.MeterVersion = instrument.Meter.Version ?? string.Empty; + this.MeterTags = instrument.Meter.Tags; this.InstrumentName = metricStreamConfiguration?.Name ?? instrument.Name; this.Unit = instrument.Unit ?? string.Empty; this.Description = metricStreamConfiguration?.Description ?? instrument.Description ?? string.Empty; @@ -75,6 +76,8 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me hash = (hash * 31) + this.InstrumentType.GetHashCode(); hash = (hash * 31) + this.MeterName.GetHashCode(); hash = (hash * 31) + this.MeterVersion.GetHashCode(); + + // MeterTags is not part of identity, so not included here. hash = (hash * 31) + this.InstrumentName.GetHashCode(); hash = (hash * 31) + this.HistogramRecordMinMax.GetHashCode(); hash = (hash * 31) + this.ExponentialHistogramMaxSize.GetHashCode(); @@ -101,6 +104,8 @@ public MetricStreamIdentity(Instrument instrument, MetricStreamConfiguration? me public string MeterVersion { get; } + public IEnumerable>? MeterTags { get; } + public string InstrumentName { get; } public string Unit { get; } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs index 8eb3a124dbf..2f077636c08 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricApiTestsBase.cs @@ -178,6 +178,90 @@ public void MetricDescriptionIsExportedCorrectly(string description) Assert.Equal(description ?? string.Empty, metric.Description); } + [Fact] + public void MetricInstrumentationScopeIsExportedCorrectly() + { + var exportedItems = new List(); + var meterName = Utils.GetCurrentMethodName(); + var meterVersion = "1.0"; + var meterTags = new List> + { + new( + "MeterTagKey", + "MeterTagValue"), + }; + using var meter = new Meter($"{meterName}", meterVersion, meterTags); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems)); + + var counter = meter.CreateCounter("name1"); + counter.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(meterName, metric.MeterName); + Assert.Equal(meterVersion, metric.MeterVersion); + + Assert.Single(metric.MeterTags.Where(kvp => kvp.Key == meterTags[0].Key && kvp.Value == meterTags[0].Value)); + } + + [Fact] + public void MetricInstrumentationScopeAttributesAreNotTreatedAsIdentifyingProperty() + { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#get-a-meter + // Meters are identified by name, version, and schema_url fields + // and not with tags. + var exportedItems = new List(); + var meterName = "MyMeter"; + var meterVersion = "1.0"; + var meterTags1 = new List> + { + new( + "Key1", + "Value1"), + }; + var meterTags2 = new List> + { + new( + "Key2", + "Value2"), + }; + using var meter1 = new Meter(meterName, meterVersion, meterTags1); + using var meter2 = new Meter(meterName, meterVersion, meterTags2); + using var container = this.BuildMeterProvider(out var meterProvider, builder => builder + .AddMeter(meterName) + .AddInMemoryExporter(exportedItems)); + + var counter1 = meter1.CreateCounter("my-counter"); + counter1.Add(10); + var counter2 = meter2.CreateCounter("my-counter"); + counter2.Add(15); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // The instruments differ only in the Meter.Tags, which is not an identifying property. + // The first instrument's Meter.Tags is exported. + // It is considered a user-error to create Meters with same name,version but with + // different tags. TODO: See if we can emit an internal log about this. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(meterName, metric.MeterName); + Assert.Equal(meterVersion, metric.MeterVersion); + + Assert.Single(metric.MeterTags.Where(kvp => kvp.Key == meterTags1[0].Key && kvp.Value == meterTags1[0].Value)); + Assert.Empty(metric.MeterTags.Where(kvp => kvp.Key == meterTags2[0].Key && kvp.Value == meterTags2[0].Value)); + + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint1 = metricPoints[0]; + Assert.Equal(25, metricPoint1.GetSumLong()); + } + [Fact] public void DuplicateInstrumentRegistration_NoViews_IdenticalInstruments() {