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
- 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
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