Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

### Fixes

Expand Down
28 changes: 21 additions & 7 deletions src/Sentry.OpenTelemetry/OpenTelemetryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ internal static class OpenTelemetryExtensions
{
public static SpanId AsSentrySpanId(this ActivitySpanId id) => SpanId.Parse(id.ToHexString());

public static ActivitySpanId AsActivitySpanId(this SpanId id) => ActivitySpanId.CreateFromString(id.ToString().AsSpan());
public static ActivitySpanId AsActivitySpanId(this SpanId id) =>
ActivitySpanId.CreateFromString(id.ToString().AsSpan());

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

public static ActivityTraceId AsActivityTraceId(this SentryId id) => ActivityTraceId.CreateFromString(id.ToString().AsSpan());
public static ActivityTraceId AsActivityTraceId(this SentryId id) =>
ActivityTraceId.CreateFromString(id.ToString().AsSpan());

public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string, string?>> baggage, bool useSentryPrefix = false) =>
public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string, string?>> baggage,
bool useSentryPrefix = false) =>
BaggageHeader.Create(
baggage.Where(member => member.Value != null)
.Select(kvp => (KeyValuePair<string, string>)kvp!),
.Select(kvp => (KeyValuePair<string, string>)kvp!),
useSentryPrefix
);
);

/// <summary>
/// The names that OpenTelemetry gives to attributes, by convention, have changed over time so we often need to
Expand All @@ -40,18 +43,29 @@ public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string
return value;
}
}

return default;
}

public static string? HttpMethodAttribute(this IDictionary<string, object?> attributes) =>
attributes.GetFirstMatchingAttribute<string>(
OtelSemanticConventions.AttributeHttpRequestMethod,
OtelSemanticConventions.AttributeHttpMethod // Fallback pre-1.5.0
);
);

public static string? UrlFullAttribute(this IDictionary<string, object?> attributes) =>
attributes.GetFirstMatchingAttribute<string>(
OtelSemanticConventions.AttributeUrlFull,
OtelSemanticConventions.AttributeHttpUrl // Fallback pre-1.5.0
);
);

public static short? HttpResponseStatusCodeAttribute(this IDictionary<string, object?> attributes)
{
var statusCode = attributes.GetFirstMatchingAttribute<int?>(
OtelSemanticConventions.AttributeHttpResponseStatusCode
);
return statusCode is >= short.MinValue and <= short.MaxValue
? (short)statusCode.Value
: null;
}
}
8 changes: 8 additions & 0 deletions src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,13 @@ public override void OnEnd(Activity data)
span.Operation = operation;
span.Description = description;

// Handle HTTP response status code specially
var statusCode = attributes.HttpResponseStatusCodeAttribute();
if (span is TransactionTracer transaction)
{
transaction.Name = description;
transaction.NameSource = source;
transaction.Contexts.Response.StatusCode = statusCode;

// Use the end timestamp from the activity data.
transaction.EndTimestamp = data.StartTimeUtc + data.Duration;
Expand All @@ -250,6 +253,11 @@ public override void OnEnd(Activity data)
// Resource attributes do not need to be set, as they would be identical as those set on the transaction.
spanTracer.SetExtras(attributes);
spanTracer.SetExtra("otel.kind", data.Kind);
if (statusCode is { } responseStatusCode)
{
// Set this as a tag so that it's searchable in Sentry
span.SetTag("HTTP Response Status Code", responseStatusCode.ToString());
}
}

// In ASP.NET Core the middleware finishes up (and the scope gets popped) before the activity is ended. So we
Expand Down
77 changes: 77 additions & 0 deletions test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,83 @@ public void OnEnd_Unsampled_Span_DoesNotThrow()
// UnsampledSpan.Finish() is basically a no-op.
}

[Fact]
public void OnEnd_Transaction_SetsResponseStatusCode()
{
// Arrange
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
var sut = _fixture.GetSut();

var tags = new Dictionary<string, object> {
{ OtelSemanticConventions.AttributeHttpResponseStatusCode, 404 }
};
var data = Tracer.StartActivity(
name: "test operation",
kind: ActivityKind.Server,
parentContext: default,
tags
);
sut.OnStart(data);

sut._map.TryGetValue(data.SpanId, out var span);

// Act
sut.OnEnd(data);

// Assert
if (span is not TransactionTracer transaction)
{
Assert.Fail("Span is not a transaction tracer");
return;
}

using (new AssertionScope())
{
transaction.Contexts.Response.StatusCode.Should().Be(404);
}
}

[Fact]
public void OnEnd_Span_SetsResponseStatusCode()
{
// Arrange
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
var sut = _fixture.GetSut();

var parent = Tracer.StartActivity(name: "transaction")!;
sut.OnStart(parent);

var tags = new Dictionary<string, object> {
{ OtelSemanticConventions.AttributeHttpResponseStatusCode, 404 }
};
var data = Tracer.StartActivity(
name: "test operation",
kind: ActivityKind.Server,
parentContext: default,
tags
);
sut.OnStart(data);

sut._map.TryGetValue(data.SpanId, out var span);

// Act
sut.OnEnd(data);

// Assert
if (span is not SpanTracer spanTracer)
{
Assert.Fail("Span is not a transaction tracer");
return;
}

using (new AssertionScope())
{
spanTracer.Tags.TryGetValue("HTTP Response Status Code", out var responseStatusCode)
.Should().BeTrue();
responseStatusCode.Should().Be("404");
}
}

[Fact]
public void OnEnd_Transaction_RestoresSavedScope()
{
Expand Down