From de622c310db9874aaaa48cc7161a775a8a009585 Mon Sep 17 00:00:00 2001 From: Ali Mahboubi Date: Tue, 28 Jan 2025 22:04:16 +0330 Subject: [PATCH] Add support for tracer tags in GetTracer method for .NET 9.0 and greater --- src/OpenTelemetry.Api/Trace/TracerProvider.cs | 67 +++++++++++++- .../Trace/TracerTest.cs | 90 ++++++++++++++++++- 2 files changed, 149 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Api/Trace/TracerProvider.cs b/src/OpenTelemetry.Api/Trace/TracerProvider.cs index 21197d0abdb..4dd02e40929 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProvider.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProvider.cs @@ -27,6 +27,50 @@ protected TracerProvider() /// public static TracerProvider Default { get; } = new TracerProvider(); +#if NET9_0_OR_GREATER + /// + /// Gets a tracer with given name, version and tags. + /// + /// Name identifying the instrumentation library. + /// Version of the instrumentation library. + /// Tags associated with the tracer. + /// Tracer instance. + public Tracer GetTracer( + [AllowNull] string name, + string? version = null, + IEnumerable>? tags = null) + { + var tracers = this.Tracers; + if (tracers == null) + { + // Note: Returns a no-op Tracer once dispose has been called. + return new(activitySource: null); + } + + var key = new TracerKey(name, version, tags); + + if (!tracers.TryGetValue(key, out var tracer)) + { + lock (tracers) + { + if (this.Tracers == null) + { + // Note: We check here for a race with Dispose and return a + // no-op Tracer in that case. + return new(activitySource: null); + } + + tracer = new(new(key.Name, key.Version, key.Tags)); + bool result = tracers.TryAdd(key, tracer); +#if DEBUG + System.Diagnostics.Debug.Assert(result, "Write into tracers cache failed"); +#endif + } + } + + return tracer; + } +#else /// /// Gets a tracer with given name and version. /// @@ -60,18 +104,17 @@ public Tracer GetTracer( return new(activitySource: null); } - tracer = new(new(key.Name, key.Version)); -#if DEBUG + tracer = new (new (key.Name, key.Version)); bool result = tracers.TryAdd(key, tracer); +#if DEBUG System.Diagnostics.Debug.Assert(result, "Write into tracers cache failed"); -#else - tracers.TryAdd(key, tracer); #endif } } return tracer; } +#endif /// protected override void Dispose(bool disposing) @@ -99,6 +142,21 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } +#if NET9_0_OR_GREATER + internal readonly record struct TracerKey + { + public readonly string Name; + public readonly string? Version; + public readonly IEnumerable>? Tags; + + public TracerKey(string? name, string? version, IEnumerable>? tags) + { + this.Name = name ?? string.Empty; + this.Version = version; + this.Tags = tags?.OrderBy(e => e.Key); + } + } +#else internal readonly record struct TracerKey { public readonly string Name; @@ -110,4 +168,5 @@ public TracerKey(string? name, string? version) this.Version = version; } } +#endif } diff --git a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs b/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs index 642795202d0..ca7f97a37b2 100644 --- a/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs +++ b/test/OpenTelemetry.Api.Tests/Trace/TracerTest.cs @@ -235,7 +235,8 @@ public void Tracer_StartActiveSpan_CreatesActiveSpan() Assert.NotNull(span3.Activity); Assert.Equal(span3.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); - var spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); + var spanContext = new SpanContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), + ActivityTraceFlags.Recorded); var span4 = this.tracer.StartActiveSpan("Test", SpanKind.Client, spanContext); Assert.NotNull(span4.Activity); Assert.Equal(span4.Activity.SpanId, Tracer.CurrentSpan.Context.SpanId); @@ -303,8 +304,8 @@ public void TracerBecomesNoopWhenParentProviderIsDisposedTest() Tracer? tracer1; using (var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("mytracer") - .Build()) + .AddSource("mytracer") + .Build()) { provider = tracerProvider; tracer1 = tracerProvider.GetTracer("mytracer"); @@ -338,7 +339,8 @@ public void TracerConcurrencyTest() this.output.WriteLine($"Bugs, if any: {string.Join("\n", test.TestReport.BugReports)}"); var dir = Directory.GetCurrentDirectory(); - if (test.TryEmitReports(dir, $"{nameof(this.TracerConcurrencyTest)}_CoyoteOutput", out IEnumerable reportPaths)) + if (test.TryEmitReports(dir, $"{nameof(this.TracerConcurrencyTest)}_CoyoteOutput", + out IEnumerable reportPaths)) { foreach (var reportPath in reportPaths) { @@ -408,6 +410,86 @@ static void InnerTest() } } + +#if NET9_0_OR_GREATER + [Fact] + public void GetTracer_WithTags_ReturnsSameInstanceForSameTags() + { + var tags1 = new List> { new("tag1", "value1"), new("tag2", "value2") }; + var tags2 = new List> { new("tag1", "value1"), new("tag2", "value2") }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Equivalent(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithTags_ReturnsDifferentInstancesForDifferentTags() + { + var tags1 = new List> { new("tag1", "value1") }; + var tags2 = new List> { new("tag2", "value2") }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.NotEqual(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithTags_OrdersTagsByKey() + { + var tags1 = new List> { new("tag2", "value2"), new("tag1", "value1"), }; + var tags2 = new List> { new("tag1", "value1"), new("tag2", "value2"), }; + + using var tracerProvider = new TestTracerProvider(); + var tracer1 = tracerProvider.GetTracer("test", "1.0.0", tags1); + var tracer2 = tracerProvider.GetTracer("test", "1.0.0", tags2); + + Assert.Equivalent(tracer1, tracer2); + } + + [Fact] + public void GetTracer_WithTagsAndWithoutTags_ReturnsDifferentInstances() + { + var tags = new List> { new("tag1", "value1") }; + + using var tracerProvider = new TestTracerProvider(); + var tracerWithTags = tracerProvider.GetTracer("test", "1.0.0", tags); + var tracerWithoutTags = tracerProvider.GetTracer("test", "1.0.0"); + + Assert.NotSame(tracerWithTags, tracerWithoutTags); + } + + [Fact] + public void GetTracer_WithTags_AppliesTagsToActivities() + { + var exportedItems = new List(); + var tags = new List> { new("tracerTag", "tracerValue") }; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("test") + .AddInMemoryExporter(exportedItems) + .SetSampler(new AlwaysOnSampler()) + .Build(); + + var tracer = tracerProvider.GetTracer("test", "1.0.0", tags); + + using (var span = tracer.StartActiveSpan("TestSpan")) + { + // Activity started by the tracer with tags + } + + Assert.Single(exportedItems); + var activity = exportedItems[0]; + + // Verify the tracer's tags are applied to the activity + Assert.Contains(activity.Source.Tags, kvp => kvp.Key == "tracerTag" && (string)kvp.Value == "tracerValue"); + } +#endif + public void Dispose() { Activity.Current = null;