diff --git a/src/NATS.Client.OpenTelemetry/NatsInstrumentationOptionsExtensions.cs b/src/NATS.Client.OpenTelemetry/NatsInstrumentationOptionsExtensions.cs index 8a039c8cc..da84aa7c0 100644 --- a/src/NATS.Client.OpenTelemetry/NatsInstrumentationOptionsExtensions.cs +++ b/src/NATS.Client.OpenTelemetry/NatsInstrumentationOptionsExtensions.cs @@ -88,21 +88,38 @@ public static NatsInstrumentationOptions FilterSubjects( } // NATS subject match: '*' matches exactly one token, '>' matches one or more trailing tokens. + // Walks the subject token by token instead of allocating a string[] per message; the pattern is + // already tokenized once at configuration time. private static bool Matches(string subject, string[] pattern) { - var tokens = subject.Split('.'); + var remaining = subject.AsSpan(); + var exhausted = false; + for (var i = 0; i < pattern.Length; i++) { if (pattern[i] == ">") - return tokens.Length > i; + return !exhausted; // '>' needs at least one remaining subject token; exhausted means the subject is fully consumed - if (i >= tokens.Length) - return false; + if (exhausted) + return false; // pattern has more tokens than the subject + + var dot = remaining.IndexOf('.'); + ReadOnlySpan token; + if (dot < 0) + { + token = remaining; + exhausted = true; // this was the subject's last token + } + else + { + token = remaining.Slice(0, dot); + remaining = remaining.Slice(dot + 1); + } - if (pattern[i] != "*" && !string.Equals(pattern[i], tokens[i], StringComparison.Ordinal)) + if (pattern[i] != "*" && !pattern[i].AsSpan().SequenceEqual(token)) return false; } - return tokens.Length == pattern.Length; + return exhausted; // subject and pattern must have the same token count } } diff --git a/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs b/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs index a5d1d9527..803ef2043 100644 --- a/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs +++ b/tests/NATS.Net.OpenTelemetry.Tests/NatsInstrumentationExtensionsTest.cs @@ -115,6 +115,32 @@ public void FilterSubjects_composes_with_existing_filter() options.Filter!(Context("payments.new")).Should().BeFalse(); // dropped by the pre-existing filter } + // Trailing dots are not valid in NATS subjects; this pins the matcher to the same + // behavior as the previous Split-based implementation, not protocol compliance. + [Theory] + [InlineData("foo.", true)] // trailing dot: tokens are "foo" and "", '*' matches the empty token + [InlineData("foo", false)] // only one token, pattern needs two + [InlineData("foo.bar.", false)] // three tokens ("foo", "bar", ""), pattern needs exactly two + public void FilterSubjects_invalid_subject_empty_trailing_token(string subject, bool expected) + { + var options = new NatsInstrumentationOptions().FilterSubjects(include: ["foo.*"]); + + options.Filter!(Context(subject)).Should().Be(expected); + } + + // Consecutive dots are not valid in NATS subjects; this pins the matcher to the same + // behavior as the previous Split-based implementation, not protocol compliance. + [Theory] + [InlineData("foo..bar", true)] // consecutive dots: tokens "foo", "", "bar" + [InlineData("foo.bar", true)] + [InlineData("foo", false)] // '>' needs at least one trailing token + public void FilterSubjects_invalid_subject_greater_than_spans_empty_tokens(string subject, bool expected) + { + var options = new NatsInstrumentationOptions().FilterSubjects(include: ["foo.>"]); + + options.Filter!(Context(subject)).Should().Be(expected); + } + [Fact] public void FilterSubjects_without_patterns_leaves_filter_unset() {