Skip to content

Commit

Permalink
Ensure outgoing spans show on the Request dashboard in Sentry (#3357)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamescrosswell authored May 14, 2024
1 parent 3d5201f commit 46ce6ee
Show file tree
Hide file tree
Showing 14 changed files with 150 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Fixed SentryHttpMessageHandler and SentryGraphQLHttpMessageHandler not creating spans when there is no active Transaction on the scope ([#3360](https://github.com/getsentry/sentry-dotnet/pull/3360))
- The SDK no longer (wrongly) initializes sentry-native on Blazor WASM builds with `RunAOTCompilation` enabled. ([#3363](https://github.com/getsentry/sentry-dotnet/pull/3363))
- HttpClient requests now show on the Requests dashboard in Sentry ([#3357](https://github.com/getsentry/sentry-dotnet/pull/3357))

### Dependencies

Expand Down
55 changes: 29 additions & 26 deletions samples/Sentry.Samples.Console.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
*/

// Initialize the Sentry SDK. (It is not necessary to dispose it.)

using System.Net.Http;

SentrySdk.Init(options =>
{
// A Sentry Data Source Name (DSN) is required.
// TODO: Configure a Sentry Data Source Name (DSN).
// See https://docs.sentry.io/product/sentry-basics/dsn-explainer/
// You can set it in the SENTRY_DSN environment variable, or you can set it in code here.
// options.Dsn = "... Your DSN ...";
options.Dsn = "... Your DSN ...";
// When debug is enabled, the Sentry client will emit detailed debugging information to the console.
// This might be helpful, or might interfere with the normal operation of your application.
Expand All @@ -38,30 +41,27 @@
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);

// Do some work. (This is where you'd have your own application logic.)
await FirstFunctionAsync();
await SecondFunctionAsync();
await ThirdFunctionAsync();
await FirstFunction();
await SecondFunction();
await ThirdFunction();

// Always try to finish the transaction successfully.
// Unhandled exceptions will fail the transaction automatically.
// Optionally, you can try/catch the exception, and call transaction.Finish(exception) on failure.
transaction.Finish();

async Task FirstFunctionAsync()
async Task FirstFunction()
{
// This shows how you might instrument a particular function.
var span = transaction.StartChild("function", nameof(FirstFunctionAsync));

// Simulate doing some work
await Task.Delay(100);

// Finish the span successfully.
span.Finish();
// This is an example of making an HttpRequest. A trace us automatically captured by Sentry for this.
var messageHandler = new SentryHttpMessageHandler();
var httpClient = new HttpClient(messageHandler, true);
var html = await httpClient.GetStringAsync("https://example.com/");
Console.WriteLine(html);
}

async Task SecondFunctionAsync()
async Task SecondFunction()
{
var span = transaction.StartChild("function", nameof(SecondFunctionAsync));
var span = transaction.StartChild("function", nameof(SecondFunction));

try
{
Expand All @@ -81,16 +81,19 @@ async Task SecondFunctionAsync()
span.Finish();
}

async Task ThirdFunctionAsync()
async Task ThirdFunction()
{
var span = transaction.StartChild("function", nameof(ThirdFunctionAsync));

// Simulate doing some work
await Task.Delay(100);

// This is an example of an unhandled exception. It will be captured automatically.
throw new InvalidOperationException("Something happened that crashed the app!");
var span = transaction.StartChild("function", nameof(ThirdFunction));
try
{
// Simulate doing some work
await Task.Delay(100);

// In this case, we can't attempt to finish the span, due to the exception.
// span.Finish();
// This is an example of an unhandled exception. It will be captured automatically.
throw new InvalidOperationException("Something happened that crashed the app!");
}
finally
{
span.Finish();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,8 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Sentry\Sentry.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>

</Project>
37 changes: 22 additions & 15 deletions samples/Sentry.Samples.OpenTelemetry.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,39 @@

SentrySdk.Init(options =>
{
// options.Dsn = "... Your DSN ...";
// TODO: Replace the DSN below with your own. You can find this value in your Sentry project settings.
// https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/#where-to-find-your-dsn
options.Dsn = "... Your DSN ...";
options.TracesSampleRate = 1.0;
options.UseOpenTelemetry(); // <-- Configure Sentry to use OpenTelemetry trace information
options.Debug = true;
});

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddSource(activitySource.Name)
.AddHttpClientInstrumentation()
.AddSentry() // <-- Configure OpenTelemetry to send traces to Sentry
.Build();

// Finally we can use OpenTelemetry to instrument our code. These activities will be captured as a Sentry transaction.
using var activity = activitySource.StartActivity("Main");
Console.WriteLine("Hello World!");
using (var task = activitySource.StartActivity("Task 1"))
{
task?.SetTag("Answer", 42);
Thread.Sleep(100); // simulate some work
Console.WriteLine("Task 1 completed");
task?.SetStatus(Status.Ok);
}

using (var task = activitySource.StartActivity("Task 2"))
// Finally we can use OpenTelemetry to instrument our code. This activity will be captured as a Sentry transaction.
using (var activity = activitySource.StartActivity("Main"))
{
task?.SetTag("Question", "???");
Thread.Sleep(100); // simulate some more work
Console.WriteLine("Task 2 unresolved");
task?.SetStatus(Status.Error);
// This creates a span called "Task 1" within the transaction
using (var task = activitySource.StartActivity("Task 1"))
{
task?.SetTag("Answer", 42);
Thread.Sleep(100); // simulate some work
Console.WriteLine("Task 1 completed");
task?.SetStatus(Status.Ok);
}

// Since we use `AddHttpClientInstrumentation` when initializing OpenTelemetry, the following Http request will also
// be captured as a Sentry span
var httpClient = new HttpClient();
var html = await httpClient.GetStringAsync("https://example.com/");
Console.WriteLine(html);
}

Console.WriteLine("Goodbye cruel world...");
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.8.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
</ItemGroup>

<!-- In your own project, this would be a PackageReference to the latest version of Sentry. -->
Expand Down
38 changes: 38 additions & 0 deletions src/Sentry.OpenTelemetry/OpenTelemetryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Sentry.Internal.Extensions;
using Sentry.Internal.OpenTelemetry;

namespace Sentry.OpenTelemetry;

internal static class OpenTelemetryExtensions
Expand All @@ -16,4 +19,39 @@ public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string
.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
/// check for both the new attribute and any obsolete ones.
/// </summary>
/// <param name="attributes">The attributes to be searched</param>
/// <param name="attributeNames">The list of possible names for the attribute you want to retrieve</param>
/// <typeparam name="T">The expected type of the attribute</typeparam>
/// <returns>The first attribute it finds matching one of the supplied <paramref name="attributeNames"/>
/// or null, if no matching attribute is found
/// </returns>
private static T? GetFirstMatchingAttribute<T>(this IDictionary<string, object?> attributes,
params string[] attributeNames)
{
foreach (var name in attributeNames)
{
if (attributes.TryGetTypedValue(name, out T value))
{
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
);
}
22 changes: 11 additions & 11 deletions src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ private void CreateRootSpan(Activity data)
bool? isSampled = data.HasRemoteParent ? data.Recorded : null;

// No parent span found - start a new transaction
var transactionContext = new TransactionContext(data.DisplayName,
var transactionContext = new TransactionContext(
data.DisplayName,
data.OperationName,
data.SpanId.AsSentrySpanId(),
data.ParentSpanId.AsSentrySpanId(),
Expand Down Expand Up @@ -164,11 +165,7 @@ public override void OnEnd(Activity data)
// Make a dictionary of the attributes (aka "tags") for faster lookup when used throughout the processor.
var attributes = data.TagObjects.ToDict();

var url =
attributes.TryGetTypedValue(OtelSemanticConventions.AttributeUrlFull, out string? tempUrl) ? tempUrl
: attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpUrl, out string? fallbackUrl) ? fallbackUrl // Falling back to pre-1.5.0
: null;

var url = attributes.UrlFullAttribute();
if (!string.IsNullOrEmpty(url) && (_options?.IsSentryRequest(url) ?? false))
{
_options?.DiagnosticLogger?.LogDebug($"Ignoring Activity {data.SpanId} for Sentry request.");
Expand Down Expand Up @@ -326,22 +323,25 @@ private static SpanStatus GetErrorSpanStatus(IDictionary<string, object?> attrib
return SpanStatus.UnknownError;
}

private static (string operation, string description, TransactionNameSource source) ParseOtelSpanDescription(
Activity activity,
IDictionary<string, object?> attributes)
internal static (string operation, string description, TransactionNameSource source) ParseOtelSpanDescription(
Activity activity,
IDictionary<string, object?> attributes)
{
// This function should loosely match the JavaScript implementation at:
// https://github.com/getsentry/sentry-javascript/blob/3487fa3af7aa72ac7fdb0439047cb7367c591e77/packages/opentelemetry-node/src/utils/parseOtelSpanDescription.ts
// However, it should also follow the OpenTelemetry semantic conventions specification, as indicated.

// HTTP span
// https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/http/
if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpMethod, out string httpMethod))
if (attributes.HttpMethodAttribute() is { } httpMethod)
{
if (activity.Kind == ActivityKind.Client)
{
// Per OpenTelemetry spec, client spans use only the method.
return ("http.client", httpMethod, TransactionNameSource.Custom);
var description = (attributes.UrlFullAttribute() is { } fullUrl)
? $"{httpMethod} {fullUrl}"
: httpMethod;
return ("http.client", description, TransactionNameSource.Custom);
}

if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpRoute, out string httpRoute))
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/SentryGraphQLHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ internal SentryGraphQLHttpMessageHandler(IHub? hub, SentryOptions? options,
$"{method} {url}" // e.g. "GET https://example.com"
);
span?.SetExtra(OtelSemanticConventions.AttributeHttpRequestMethod, method);
if (!string.IsNullOrWhiteSpace(request.RequestUri?.Host))
{
span?.SetExtra(OtelSemanticConventions.AttributeServerAddress, request.RequestUri!.Host);
}
return span;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/SentryHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ internal SentryHttpMessageHandler(IHub? hub, SentryOptions? options, HttpMessage
$"{method} {url}" // e.g. "GET https://example.com"
);
span?.SetExtra(OtelSemanticConventions.AttributeHttpRequestMethod, method);
if (request.RequestUri is not null && !string.IsNullOrWhiteSpace(request.RequestUri.Host))
{
span?.SetExtra(OtelSemanticConventions.AttributeServerAddress, request.RequestUri.Host);
}
return span;
}

Expand Down
1 change: 0 additions & 1 deletion src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Sentry.Infrastructure;
using Sentry.Internal;
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;

namespace Sentry;

Expand Down
20 changes: 20 additions & 0 deletions test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,4 +582,24 @@ public void PruneFilteredSpans_RecentlyPruned_DoesNothing()
Assert.True(sut._map.TryGetValue(activity1.SpanId, out var _));
Assert.True(sut._map.TryGetValue(activity2.SpanId, out var _));
}

[Fact]
public void ParseOtelSpanDescription_HttpClient()
{
// Arrange
var data = Tracer.StartActivity("test op", ActivityKind.Client)!;
var attributes = new Dictionary<string, object>()
{
[OtelSemanticConventions.AttributeHttpRequestMethod] = "POST",
[OtelSemanticConventions.AttributeUrlFull] = "https://example.com/foo",
};

// Act
var (operation, description, source) = SentrySpanProcessor.ParseOtelSpanDescription(data, attributes);

// Assert
operation.Should().Be("http.client");
description.Should().Be("POST https://example.com/foo");
source.Should().Be(TransactionNameSource.Custom);
}
}
9 changes: 7 additions & 2 deletions test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ public void ProcessRequest_SetsSpanData()
var sut = new SentryGraphQLHttpMessageHandler(hub, null);

var method = "POST";
var url = "http://example.com/graphql";
var host = "example.com";
var url = $"https://{host}/graphql";
var query = ValidQuery;
var request = SentryGraphQlTestHelpers.GetRequestQuery(query);
var request = SentryGraphQlTestHelpers.GetRequestQuery(query, url);

// Act
var returnedSpan = sut.ProcessRequest(request, method, url);
Expand All @@ -68,6 +69,10 @@ public void ProcessRequest_SetsSpanData()
kvp.Key == OtelSemanticConventions.AttributeHttpRequestMethod &&
kvp.Value.ToString() == method
);
returnedSpan.Extra.Should().Contain(kvp =>
kvp.Key == OtelSemanticConventions.AttributeServerAddress &&
kvp.Value.ToString() == host
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/Sentry.Tests/SentryGraphQlTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static HttpRequestMessage GetRequestQuery(string query, string url = "htt
return GetRequest(content, url);
}

public static HttpRequestMessage GetRequest(HttpContent content, string url = "http://foo") => new(HttpMethod.Post, url)
public static HttpRequestMessage GetRequest(HttpContent content, string url = "http://foo") => new(HttpMethod.Post, new Uri(url))
{
Content = content
};
Expand Down
12 changes: 9 additions & 3 deletions test/Sentry.Tests/SentryHttpMessageHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,11 @@ public void ProcessRequest_SetsSpanData()
using var innerHandler = new FakeHttpMessageHandler();
var sut = new SentryHttpMessageHandler(hub, _fixture.Options, innerHandler);

const string method = "GET";
const string url = "https://example.com/graphql";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var method = "GET";
var host = "example.com";
var url = $"https://{host}/graphql";
var uri = new Uri(url);
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// Act
var returnedSpan = sut.ProcessRequest(request, method, url);
Expand All @@ -281,6 +283,10 @@ public void ProcessRequest_SetsSpanData()
kvp.Key == OtelSemanticConventions.AttributeHttpRequestMethod &&
Equals(kvp.Value, method)
);
returnedSpan.Extra.Should().Contain(kvp =>
kvp.Key == OtelSemanticConventions.AttributeServerAddress &&
Equals(kvp.Value, host)
);
}

[Fact]
Expand Down

0 comments on commit 46ce6ee

Please sign in to comment.