-
-
Notifications
You must be signed in to change notification settings - Fork 220
feat: Add W3C traceparent support to tracing client and utils #4084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add W3C traceparent support to tracing client and utils #4084
Conversation
Don't need this as an external API.
… class I want to use this as a base for parsing, as an extension method doesn't really help for this.
… for better coverage
This is amazing @hangy - thank you so much for the PR! ❤️ It might take a day or two to get a review done, but having prior implementations in PHP should make things easier. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4084 +/- ##
==========================================
- Coverage 75.73% 73.60% -2.13%
==========================================
Files 357 445 +88
Lines 13466 16062 +2596
Branches 2671 3177 +506
==========================================
+ Hits 10198 11822 +1624
- Misses 2593 3425 +832
- Partials 675 815 +140 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Note that equality and hashcode methods were removed, as they're not implemented in the `SentryTraceHeader` class, either.
Note: I didn't find any tests testing the HTTP headers to verify the changes to |
TLDR; I think you've already got all the right tests in place. 👍🏻 Reading headers basically happens in the middleware. For example, in sentry-dotnet/src/Sentry.AspNetCore/SentryMiddleware.cs Lines 108 to 111 in 4818a25
So the sentry-dotnet/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs Lines 692 to 697 in f14cad5
ASP.NET is similar - only with a different implementation of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hangy this is awesome 🚀.
I didn't see any issues with the code - I've just made a few suggestions for some changes we could make to the unit tests (many of which are things that were present prior to this PR... but we may as well tidy them up while we're changing the code).
If the changes seem to onerous, I'm happy to push some commits myself to cross the t's and dot the i's on this.
Once again, thank you so much for the PR! ❤️
h.Key == headerName && | ||
string.Concat(h.Value) == expectedValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From "A quick TL;DR: How to Implement" in the docs, it looks like both headers should be present in outgoing requests. Maybe worth expanding this test to ensure that is the case (so check for both headers regardless of what value for headerName
was passed in via the theory data).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! It's quite unlikely anyone will accidentally produce a code where only one of the headers will be passed without already modifying one of the test, but verifying that the behaviour matches the definition in the task won't hurt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a new, explicit test case in 44c0d98
[InlineData("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0", "sentry-trace", "75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")] | ||
[InlineData("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0", "traceparent", "00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00")] | ||
[InlineData("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-1", "traceparent", "00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-01")] | ||
public async Task SendAsync_SentryTraceHeaderNotSet_SetsHeader_ByDefault(string traceHeader, string headerName, string expectedValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not introduced by your PR, but from what I can tell this test is redundant... all of this functionality is already tested by SendAsync_SentryTraceHeaderNotSet_SetsHeader_WhenUrlMatchesPropagationOptions
.
It was probably added before TracePropagationTargets
was added to the SentryOptions
. Feel free to remove it in this PR if you want (will save duplicating identical changes across both of the tests).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. tbh, I probably just duplicated/parameterized some of the existing test cases to also support the traceparent
header without checking existing tests for redundancy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be resolved by 9c56dbb
public void Parse_ValidW3CHeader_ReturnsW3CTraceHeader() | ||
{ | ||
// Arrange | ||
var header = "00-4bc7d217a6721c0e60e85e46d25fb3e5-f51f11f284da5299-01"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you wanted to go above and beyond, we could future proof this by testing with a value of 09
for the trace flag as well... to ensure that IsSampled()
still returns true
in that hypothetical case. At the moment, I believe only the last bit is used, so this isn't yet necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! That's a valid case I haven't seen in the spec yet. At least with version 00
, other flags won't be allowed, so I'll probably stick to string comparison for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be resolved by dc5c2cf
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
Perfect. Thank you! I was wondering if a E2E test for them might be interesting, but if this is enough, that's fine by me.
Yep I noticed that. And I noticed that they used (or at least some of them use) the sealed
I already committed some of your suggestions. Thank you! I'm taking a look at the rest now. |
SendAsync_SentryTraceHeaderNotSet_SetsHeader_ByDefault is already covered by SendAsync_SentryTraceHeaderNotSet_SetsHeader_WhenUrlMatchesPropagationOptions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, there's just one change I think needs making to W3CTraceHeader.cs
- otherwise LGTM!
I notice CI broke as well (due to XCode 16.3) but we'll address that separately.
src/Sentry/W3CTraceHeader.cs
Outdated
var isSampled = string.Equals(components[3], "01", StringComparison.Ordinal) || | ||
string.Equals(components[3], "09", StringComparison.Ordinal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a little bit fragile. components[2]
contains a hex encoded a bit flag type. The last bit in that flag indicates whether the trace is sampled or not.
So yes, 01 and 09 are examples of values where the sampled flag is 1... there are other examples as well though (e.g. FF)
HEX | Binary |
---|---|
0x01 | 0b00000001 |
0x09 | 0b00001001 |
.. | .. |
0xFF | 0b11111111 |
Currently I think the W3C spec only uses the last bit in this flag, so in practice the value will only ever be "00" or "01" (today)... but I think worth writing this code with the expectation that the other bits in the flag may be used in the future.
Something like this:
var isSampled = Convert.ToInt32(components[3], 16) & 0x01 == 1;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to avoid parsing the string as honestly, as long as only version 00
is supported, other bits being set would be against the W3C spec, anyways. Will look into it, though. Potentially with byte.Parse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to avoid parsing the string
For performance reasons?
We could leave it as is (don't bother with 09 either) and just put a comment in the code warning that as only the least significant bit is used at the moment, this code only works for scenarios where all the other bits are zero.
I'm a bit concerned we might see variants like "01" and "1" in the string representation... which wouldn't be per the W3C spec either but, as Sentry is in the business of capturing bugs, we don't want to be crashing people's applications... so we want to take a pretty cautious approach about assumptions re inputs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For performance reasons?
Yup. My assumption was that string comparison is highly optimized and will work well. I don't want to argue based solely on my gut feeling, so I just ran a benchmark that kinda surprised me:
benchmark
using System.Globalization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<Benchmarks>();
[MemoryDiagnoser, RankColumn]
public class Benchmarks
{
[Benchmark]
public bool StringCompareNoMatchOrdinalIgnoreCase() => "00".Equals("01", StringComparison.OrdinalIgnoreCase);
[Benchmark]
public bool StringCompareMatchOrdinalIgnoreCase() => "00".Equals("00", StringComparison.OrdinalIgnoreCase);
[Benchmark]
public bool StringCompareNoMatchOrdinal() => "00".Equals("01", StringComparison.Ordinal);
[Benchmark]
public bool ByteParseMatch() => byte.TryParse("01", System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte b) && (b & 0x01) == 1;
[Benchmark]
public bool ByteParseNoMatch() => byte.TryParse("00", System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte b) && (b & 0x01) == 1;
[Benchmark]
public bool IntParseMatch() => int.TryParse("01", System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b) && (b & 0x01) == 1;
[Benchmark]
public bool IntParseNoMatch() => int.TryParse("00", System.Globalization.NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b) && (b & 0x01) == 1;
}
// * Summary *
BenchmarkDotNet v0.14.0, Ubuntu 24.10 (Oracular Oriole) WSL
AMD Ryzen 7 7800X3D, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.202
[Host] : .NET 9.0.3 (9.0.325.11113), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 9.0.3 (9.0.325.11113), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | Mean | Error | StdDev | Rank | Allocated |
|-------------------------------------- |----------:|----------:|----------:|-----:|----------:|
| StringCompareNoMatchOrdinalIgnoreCase | 0.0000 ns | 0.0000 ns | 0.0000 ns | 1 | - |
| StringCompareMatchOrdinalIgnoreCase | 0.0000 ns | 0.0000 ns | 0.0000 ns | 1 | - |
| StringCompareNoMatchOrdinal | 0.0000 ns | 0.0000 ns | 0.0000 ns | 1 | - |
| ByteParseMatch | 3.0734 ns | 0.0212 ns | 0.0199 ns | 3 | - |
| ByteParseNoMatch | 2.6390 ns | 0.0196 ns | 0.0183 ns | 2 | - |
| IntParseMatch | 3.2432 ns | 0.0161 ns | 0.0151 ns | 4 | - |
| IntParseNoMatch | 2.6208 ns | 0.0158 ns | 0.0140 ns | 2 | - |
// * Warnings *
ZeroMeasurement
Benchmarks.StringCompareNoMatchOrdinalIgnoreCase: Default -> The method duration is indistinguishable from the empty method duration
Benchmarks.StringCompareMatchOrdinalIgnoreCase: Default -> The method duration is indistinguishable from the empty method duration
Benchmarks.StringCompareNoMatchOrdinal: Default -> The method duration is indistinguishable from the empty method duration
// * Hints *
Outliers
Benchmarks.ByteParseMatch: Default -> 1 outlier was detected (4.12 ns)
Benchmarks.IntParseNoMatch: Default -> 1 outlier was removed (3.76 ns)
// * Legends *
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Rank : Relative position of current benchmark mean among all benchmarks (Arabic style)
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 ns : 1 Nanosecond (0.000000001 sec)
This makes it look like parsing the string to a byte/int and then checking for the bitmask is orders of magnitude slower than string compare. There's no real harm in combining this, though, so I'll probably do that. 😄
I'm a bit concerned we might see variants like "01" and "1" in the string representation... which wouldn't be per the W3C spec either but, as Sentry is in the business of capturing bugs, we don't want to be crashing people's applications... so we want to take a pretty cautious approach about assumptions re inputs.
ACK. The worst case should be that a sampled = true
flag isn't captured. That should be covered by the approach, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take a look at 358e1da for a proposed solution
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, that seems like the best of both worlds. Ordinarily the most efficient code path should run. And if processes are doing weird stuff with the traceparent headers upstream, it won't break (will just burn a few more cycles to process it). 👍🏻
…er class for use in unit tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huge thanks for the PR @hangy ! ❤️
Thanks for the kind and quick review! 😊 |
@hangy there's a discussing to revert this PR, and remove all traceparent support from Sentry's server's SDKs. Did you add this to contribute to .NET what was going on on PHP or was this something you need for some reason? Thanks! |
This adds support for propagating the Sentry TraceId as a W3C TraceSpan header to support #3069, loosely based on getsentry/sentry-php@a31d418
I know the PR got kinda large to support both headers. I don't have any prior experience with this SDK, so please do critique thoroughly. :)