Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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;
}
}
11 changes: 11 additions & 0 deletions src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,16 @@ 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;
if (statusCode is { } responseStatusCode)
{
transaction.Contexts.Response.StatusCode = responseStatusCode;
}

// Use the end timestamp from the activity data.
transaction.EndTimestamp = data.StartTimeUtc + data.Duration;
Expand All @@ -250,6 +256,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(OtelSemanticConventions.AttributeHttpResponseStatusCode, responseStatusCode.ToString());
}
}

// In ASP.NET Core the middleware finishes up (and the scope gets popped) before the activity is ended. So we
Expand Down
111 changes: 111 additions & 0 deletions test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,117 @@ 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_Transaction_DoesNotClearResponseStatusCode()
{
// Arrange
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
var sut = _fixture.GetSut();

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

sut._map.TryGetValue(data.SpanId, out var span);
(span as TransactionTracer)!.Contexts.Response.StatusCode = 200;

// 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(200);
}
}

[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 span tracer");
return;
}

using (new AssertionScope())
{
spanTracer.Tags.TryGetValue(OtelSemanticConventions.AttributeHttpResponseStatusCode,
out var responseStatusCode).Should().BeTrue();
responseStatusCode.Should().Be("404");
}
}

[Fact]
public void OnEnd_Transaction_RestoresSavedScope()
{
Expand Down
Loading