diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs index 38f986c03..35e684fbf 100644 --- a/src/NATS.Client.Core/NatsConnection.cs +++ b/src/NATS.Client.Core/NatsConnection.cs @@ -384,9 +384,16 @@ internal string SpanDestinationName(string subject) if (subject.StartsWith(Opts.InboxPrefix, StringComparison.Ordinal)) return "inbox"; + if (NatsInstrumentationOptions.Default.SpanDestinationNameFormatter is { } formatter) + return formatter(subject); + // to avoid long span names and low cardinality, only take the first two tokens - var tokens = subject.Split('.'); - return tokens.Length < 2 ? subject : $"{tokens[0]}.{tokens[1]}"; + var subjectSpan = subject.AsSpan(); + var firstSeparator = subjectSpan.IndexOf('.'); + var secondSeparator = firstSeparator < 0 ? -1 : subjectSpan[(firstSeparator + 1)..].IndexOf('.'); + return secondSeparator < 0 + ? subject + : subjectSpan[..(firstSeparator + 1 + secondSeparator)].ToString(); } internal NatsStats GetStats() => Counter.ToStats(); diff --git a/src/NATS.Client.Core/NatsInstrumentationOptions.cs b/src/NATS.Client.Core/NatsInstrumentationOptions.cs index 877facb32..2ec442b4c 100644 --- a/src/NATS.Client.Core/NatsInstrumentationOptions.cs +++ b/src/NATS.Client.Core/NatsInstrumentationOptions.cs @@ -23,4 +23,12 @@ public sealed class NatsInstrumentationOptions /// Gets or sets an action to enrich an Activity. /// public Action? Enrich { get; set; } + + /// + /// Gets or sets a function that formats the destination name used in span names. + /// + /// + /// The input is the raw NATS subject. This only changes activity names, not telemetry tags. + /// + public Func? SpanDestinationNameFormatter { get; set; } } diff --git a/src/NATS.Client.OpenTelemetry/NatsInstrumentationExtensions.cs b/src/NATS.Client.OpenTelemetry/NatsInstrumentationExtensions.cs index 0ac563ce7..7a768b285 100644 --- a/src/NATS.Client.OpenTelemetry/NatsInstrumentationExtensions.cs +++ b/src/NATS.Client.OpenTelemetry/NatsInstrumentationExtensions.cs @@ -18,7 +18,7 @@ public static TracerProviderBuilder AddNatsClientInstrumentation(this TracerProv /// /// Adds the NATS .NET client to the tracer provider and - /// configures the shared (filter and enrich callbacks). + /// configures the shared . /// /// The to add the source to. /// Action that mutates the process-wide . diff --git a/tests/NATS.Client.CoreUnit.Tests/NatsConnectionTelemetryTests.cs b/tests/NATS.Client.CoreUnit.Tests/NatsConnectionTelemetryTests.cs new file mode 100644 index 000000000..aad239ec3 --- /dev/null +++ b/tests/NATS.Client.CoreUnit.Tests/NatsConnectionTelemetryTests.cs @@ -0,0 +1,56 @@ +namespace NATS.Client.CoreUnit.Tests; + +public class NatsConnectionTelemetryTests +{ + [Theory] + [InlineData("foo", "foo")] + [InlineData("foo.bar", "foo.bar")] + [InlineData("foo.bar.baz", "foo.bar")] + [InlineData("foo.bar.baz.qux", "foo.bar")] + [InlineData("foo.", "foo.")] + [InlineData("foo..bar", "foo.")] + [InlineData(".foo.bar", ".foo")] + [InlineData("..", ".")] + public async Task SpanDestinationName_collapses_to_first_two_subject_tokens(string subject, string expected) + { + await using var nats = new NatsConnection(); + + nats.SpanDestinationName(subject).Should().Be(expected); + } + + [Fact] + public async Task SpanDestinationName_uses_configured_formatter() + { + var previous = NatsInstrumentationOptions.Default.SpanDestinationNameFormatter; + try + { + NatsInstrumentationOptions.Default.SpanDestinationNameFormatter = subject => $"custom:{subject}"; + + await using var nats = new NatsConnection(); + + nats.SpanDestinationName("foo.bar.baz").Should().Be("custom:foo.bar.baz"); + } + finally + { + NatsInstrumentationOptions.Default.SpanDestinationNameFormatter = previous; + } + } + + [Fact] + public async Task SpanDestinationName_collapses_inbox_before_configured_formatter() + { + var previous = NatsInstrumentationOptions.Default.SpanDestinationNameFormatter; + try + { + NatsInstrumentationOptions.Default.SpanDestinationNameFormatter = subject => $"custom:{subject}"; + + await using var nats = new NatsConnection(); + + nats.SpanDestinationName("_INBOX.abc.def").Should().Be("inbox"); + } + finally + { + NatsInstrumentationOptions.Default.SpanDestinationNameFormatter = previous; + } + } +} diff --git a/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs b/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs index 0fce374ca..a5d1d9527 100644 --- a/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs +++ b/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs @@ -39,17 +39,20 @@ public void AddNatsClientInstrumentation_with_configure_sets_options() configured = true; options.Filter = _ => true; options.Enrich = (_, _) => { }; + options.SpanDestinationNameFormatter = subject => subject; }) .Build(); configured.Should().BeTrue(); NatsInstrumentationOptions.Default.Filter.Should().NotBeNull(); NatsInstrumentationOptions.Default.Enrich.Should().NotBeNull(); + NatsInstrumentationOptions.Default.SpanDestinationNameFormatter.Should().NotBeNull(); } finally { NatsInstrumentationOptions.Default.Filter = null; NatsInstrumentationOptions.Default.Enrich = null; + NatsInstrumentationOptions.Default.SpanDestinationNameFormatter = null; } }