Skip to content

Commit 54511b5

Browse files
Ensure HTTP Response Status Code is searchable when using OTel (#4283)
Resolves #4094: - #4094
1 parent 29ff6df commit 54511b5

File tree

4 files changed

+144
-7
lines changed

4 files changed

+144
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Added non-allocating `ConfigureScope` and `ConfigureScopeAsync` overloads ([#4244](https://github.com/getsentry/sentry-dotnet/pull/4244))
88
- Add .NET MAUI `AutomationId` element information to breadcrumbs ([#4248](https://github.com/getsentry/sentry-dotnet/pull/4248))
9+
- The HTTP Response Status Code for spans instrumented using OpenTelemetry is now searchable ([#4283](https://github.com/getsentry/sentry-dotnet/pull/4283))
910

1011
### Fixes
1112

src/Sentry.OpenTelemetry/OpenTelemetryExtensions.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@ internal static class OpenTelemetryExtensions
77
{
88
public static SpanId AsSentrySpanId(this ActivitySpanId id) => SpanId.Parse(id.ToHexString());
99

10-
public static ActivitySpanId AsActivitySpanId(this SpanId id) => ActivitySpanId.CreateFromString(id.ToString().AsSpan());
10+
public static ActivitySpanId AsActivitySpanId(this SpanId id) =>
11+
ActivitySpanId.CreateFromString(id.ToString().AsSpan());
1112

1213
public static SentryId AsSentryId(this ActivityTraceId id) => SentryId.Parse(id.ToHexString());
1314

14-
public static ActivityTraceId AsActivityTraceId(this SentryId id) => ActivityTraceId.CreateFromString(id.ToString().AsSpan());
15+
public static ActivityTraceId AsActivityTraceId(this SentryId id) =>
16+
ActivityTraceId.CreateFromString(id.ToString().AsSpan());
1517

16-
public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string, string?>> baggage, bool useSentryPrefix = false) =>
18+
public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string, string?>> baggage,
19+
bool useSentryPrefix = false) =>
1720
BaggageHeader.Create(
1821
baggage.Where(member => member.Value != null)
19-
.Select(kvp => (KeyValuePair<string, string>)kvp!),
22+
.Select(kvp => (KeyValuePair<string, string>)kvp!),
2023
useSentryPrefix
21-
);
24+
);
2225

2326
/// <summary>
2427
/// The names that OpenTelemetry gives to attributes, by convention, have changed over time so we often need to
@@ -40,18 +43,29 @@ public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string
4043
return value;
4144
}
4245
}
46+
4347
return default;
4448
}
4549

4650
public static string? HttpMethodAttribute(this IDictionary<string, object?> attributes) =>
4751
attributes.GetFirstMatchingAttribute<string>(
4852
OtelSemanticConventions.AttributeHttpRequestMethod,
4953
OtelSemanticConventions.AttributeHttpMethod // Fallback pre-1.5.0
50-
);
54+
);
5155

5256
public static string? UrlFullAttribute(this IDictionary<string, object?> attributes) =>
5357
attributes.GetFirstMatchingAttribute<string>(
5458
OtelSemanticConventions.AttributeUrlFull,
5559
OtelSemanticConventions.AttributeHttpUrl // Fallback pre-1.5.0
56-
);
60+
);
61+
62+
public static short? HttpResponseStatusCodeAttribute(this IDictionary<string, object?> attributes)
63+
{
64+
var statusCode = attributes.GetFirstMatchingAttribute<int?>(
65+
OtelSemanticConventions.AttributeHttpResponseStatusCode
66+
);
67+
return statusCode is >= short.MinValue and <= short.MaxValue
68+
? (short)statusCode.Value
69+
: null;
70+
}
5771
}

src/Sentry.OpenTelemetry/SentrySpanProcessor.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,16 @@ public override void OnEnd(Activity data)
230230
span.Operation = operation;
231231
span.Description = description;
232232

233+
// Handle HTTP response status code specially
234+
var statusCode = attributes.HttpResponseStatusCodeAttribute();
233235
if (span is TransactionTracer transaction)
234236
{
235237
transaction.Name = description;
236238
transaction.NameSource = source;
239+
if (statusCode is { } responseStatusCode)
240+
{
241+
transaction.Contexts.Response.StatusCode = responseStatusCode;
242+
}
237243

238244
// Use the end timestamp from the activity data.
239245
transaction.EndTimestamp = data.StartTimeUtc + data.Duration;
@@ -250,6 +256,11 @@ public override void OnEnd(Activity data)
250256
// Resource attributes do not need to be set, as they would be identical as those set on the transaction.
251257
spanTracer.SetExtras(attributes);
252258
spanTracer.SetExtra("otel.kind", data.Kind);
259+
if (statusCode is { } responseStatusCode)
260+
{
261+
// Set this as a tag so that it's searchable in Sentry
262+
span.SetTag(OtelSemanticConventions.AttributeHttpResponseStatusCode, responseStatusCode.ToString());
263+
}
253264
}
254265

255266
// In ASP.NET Core the middleware finishes up (and the scope gets popped) before the activity is ended. So we

test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,117 @@ public void OnEnd_Unsampled_Span_DoesNotThrow()
429429
// UnsampledSpan.Finish() is basically a no-op.
430430
}
431431

432+
[Fact]
433+
public void OnEnd_Transaction_SetsResponseStatusCode()
434+
{
435+
// Arrange
436+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
437+
var sut = _fixture.GetSut();
438+
439+
var tags = new Dictionary<string, object> {
440+
{ OtelSemanticConventions.AttributeHttpResponseStatusCode, 404 }
441+
};
442+
var data = Tracer.StartActivity(
443+
name: "test operation",
444+
kind: ActivityKind.Server,
445+
parentContext: default,
446+
tags
447+
);
448+
sut.OnStart(data);
449+
450+
sut._map.TryGetValue(data.SpanId, out var span);
451+
452+
// Act
453+
sut.OnEnd(data);
454+
455+
// Assert
456+
if (span is not TransactionTracer transaction)
457+
{
458+
Assert.Fail("Span is not a transaction tracer");
459+
return;
460+
}
461+
462+
using (new AssertionScope())
463+
{
464+
transaction.Contexts.Response.StatusCode.Should().Be(404);
465+
}
466+
}
467+
468+
[Fact]
469+
public void OnEnd_Transaction_DoesNotClearResponseStatusCode()
470+
{
471+
// Arrange
472+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
473+
var sut = _fixture.GetSut();
474+
475+
var data = Tracer.StartActivity(
476+
name: "test operation",
477+
kind: ActivityKind.Server,
478+
parentContext: default,
479+
new Dictionary<string, object>()
480+
);
481+
sut.OnStart(data);
482+
483+
sut._map.TryGetValue(data.SpanId, out var span);
484+
(span as TransactionTracer)!.Contexts.Response.StatusCode = 200;
485+
486+
// Act
487+
sut.OnEnd(data);
488+
489+
// Assert
490+
if (span is not TransactionTracer transaction)
491+
{
492+
Assert.Fail("Span is not a transaction tracer");
493+
return;
494+
}
495+
496+
using (new AssertionScope())
497+
{
498+
transaction.Contexts.Response.StatusCode.Should().Be(200);
499+
}
500+
}
501+
502+
[Fact]
503+
public void OnEnd_Span_SetsResponseStatusCode()
504+
{
505+
// Arrange
506+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
507+
var sut = _fixture.GetSut();
508+
509+
var parent = Tracer.StartActivity(name: "transaction")!;
510+
sut.OnStart(parent);
511+
512+
var tags = new Dictionary<string, object> {
513+
{ OtelSemanticConventions.AttributeHttpResponseStatusCode, 404 }
514+
};
515+
var data = Tracer.StartActivity(
516+
name: "test operation",
517+
kind: ActivityKind.Server,
518+
parentContext: default,
519+
tags
520+
);
521+
sut.OnStart(data);
522+
523+
sut._map.TryGetValue(data.SpanId, out var span);
524+
525+
// Act
526+
sut.OnEnd(data);
527+
528+
// Assert
529+
if (span is not SpanTracer spanTracer)
530+
{
531+
Assert.Fail("Span is not a span tracer");
532+
return;
533+
}
534+
535+
using (new AssertionScope())
536+
{
537+
spanTracer.Tags.TryGetValue(OtelSemanticConventions.AttributeHttpResponseStatusCode,
538+
out var responseStatusCode).Should().BeTrue();
539+
responseStatusCode.Should().Be("404");
540+
}
541+
}
542+
432543
[Fact]
433544
public void OnEnd_Transaction_RestoresSavedScope()
434545
{

0 commit comments

Comments
 (0)