getsentry / getsentry/sentry-dotnet

Metrics and `SentrySdk.Logger` logs emitted during a request carry no `sentry.sdk.name`/`sentry.sdk.version` on ASP.NET Core

Open
#5,497 11 comments 0 reactions 0 assignees View on GitHub
.NET Bug Logs Waiting for: Product Owner
Dominant language
C#
Stars
770
Forks
248
Avg merge
2d 22h
Merged PRs (30d)
51

Description

## Summary

In an [ASP.NET]() Core app, metrics and `SentrySdk.Logger` logs emitted **from inside a request handler** are sent with no `sentry.sdk.name` and no `sentry.sdk.version` attribute. Events, transactions and `ILogger` logs from the same request are correct (`sentry.dotnet.aspnetcore`).

Reproduced on Sentry.AspNetCore 6.9.0.

This is the same underlying defect as getsentry/sentry-dotnet#5352, but it is not console-specific — a stock [ASP.NET]() Core app is affected for these two data categories.

## Repro

New web project, `Sentry.AspNetCore` 6.9.0, `enable`. The custom transport captures envelopes locally so no real DSN is needed.

```csharp
using System.Text;
using Sentry.Extensibility;
using Sentry.Protocol.Envelopes;

var captured = new List();

var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
builder.WebHost.UseUrls("http://127.0.0.1:5199");
builder.WebHost.UseSentry(o =>
{
o.Dsn = "https://abc123@o1.ingest.sentry.io/1";
o.EnableLogs = true;
o.Transport = new CapturingTransport(captured);
});

var app = builder.Build();

app.MapGet("/probe", () =>
{
// Emitted from inside a request handler - the common case.
SentrySdk.Metrics.EmitCounter("repro_counter", 1);
SentrySdk.Logger.LogInfo("repro-log");
return Results.Ok("done");
});

await app.StartAsync();
using (var http = new HttpClient())
{
await http.GetStringAsync("http://127.0.0.1:5199/probe");
}
await SentrySdk.FlushAsync(TimeSpan.FromSeconds(10));
await app.StopAsync();

foreach (var envelope in captured.Where(e => e.Contains("\"items\":")))
{
Console.WriteLine(envelope.Split('\n').Last(l => l.Contains("\"items\":")));
}

internal sealed class CapturingTransport(List captured) : ITransport
{
public async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default)
{
using var ms = new MemoryStream();
await envelope.SerializeAsync(ms, null, cancellationToken);
lock (captured) { captured.Add(Encoding.UTF8.GetString(ms.ToArray())); }
}
}
```

### Actual

```json
{"items":[{"body":"repro-log","attributes":{"sentry.environment":{"value":"production","type":"string"},"sentry.release":{"value":"SdkAttrRepro@1.0.0","type":"string"}}}]}
{"items":[{"type":"counter","name":"repro_counter","attributes":{"sentry.environment":{"value":"production","type":"string"},"sentry.release":{"value":"SdkAttrRepro@1.0.0","type":"string"}}}]}
```

No `sentry.sdk.name`, no `sentry.sdk.version`.

### Expected

Both carry the SDK name and version, consistent with events and `ILogger` logs from the same request.

## Cause

`SentryMetric.Factory` and `DefaultSentryStructuredLogger` read the SDK identity from `scope.Sdk`, and during a request that object is empty.

Adding a scope dump to the app above shows it directly:

```
[scope] after Build(), before any request: scope.Sdk.Name=sentry.dotnet.extensions.logging
[scope] inside request handler: scope.Sdk.Name=
[scope] after request completed: scope.Sdk.Name=sentry.dotnet.extensions.logging
```

Two writers populate `scope.Sdk`, and neither reaches the request scope:

* `SentryLoggerProvider` does `hub.PushScope()` + `ConfigureScope` in its constructor. That runs on the startup flow and the pushed scope is async-local to it, so Kestrel's request flow never sees it.
* `SentryMiddleware.PopulateScope` (which sets `sentry.dotnet.aspnetcore`) is subscribed to `scope.OnEvaluating`. `Scope.Evaluate()` is only called from `SentryClient.CaptureEvent` / `CaptureTransaction` / `CaptureFeedback` — never on the log or metric path.

So the request scope's `Sdk` has null `Name` and `Version`, and both guards in `SentryAttributes.SetDefaultAttributes` are false.

`ILogger` logs are unaffected because `SentryAspNetCoreStructuredLoggerProvider` passes its own `SdkVersion` explicitly rather than going through the scope. Events and transactions are unaffected because `Evaluate()` runs before `scope.Apply(@event)`.

## Brainstorming

- In https://github.com/getsentry/sentry-dotnet/issues/5245 we'll be dropping the ability to initialise the SDK via the logging integrations... so something like `sentry.dotnet.serilog` would never be assigned to the Sdk.Name - the Sdk.Name will only ever indicate the integration that initialised the Hub options.
- The logging integration that was used to capture something like a logging event gets [recorded in the origin](https://develop.sentry.dev/sdk/telemetry/logs/#sdk-integration-origin) - e.g. `auto.log.serilog`.

This implies setting the SDK name on the options when the SDK is initialised and applying it from the options to logs, metrics etc. when these are captured.

## Related

* getsentry/sentry-dotnet#5352 - same underlying defect, reported for console apps.
* getsentry/sentry-dotnet#5483 - fixes the missing-attribute symptom by falling back to `SdkVersion.Instance`. With that applied, both items above carry `sentry.dotnet` - better than nothing, but still not `sentry.dotnet.aspnetcore` like the event and `ILogger` log from the same request. That PR does not close this issue.

Contributor guide

Open the contributing guide

Research direction

Reproduce the request-handler case with the supplied custom transport, then trace SentryMetric.Factory and DefaultSentryStructuredLogger through the request scope. Read SentryLoggerProvider, SentryMiddleware.PopulateScope, Scope.Evaluate, and SentryAttributes.SetDefaultAttributes to compare how events and ILogger logs obtain SDK identity. Done means request-emitted metrics and SentrySdk.Logger logs include the same SDK name and version as the corresponding request data.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend, observability
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.