Skip to content

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

Description

@jamescrosswell

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 #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, <ImplicitUsings>enable</ImplicitUsings>. The custom transport captures envelopes locally so no real DSN is needed.

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

var captured = new List<string>();

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

{"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=<null>
[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).

Possible fix

Stop routing SDK identity through the scope on these paths.

SentryLog.SetDefaultAttributes already accepts an optional sdk parameter, which is how the logging integrations supply their own identity:

internal void SetDefaultAttributes(SentryOptions options, Scope? scope, SdkVersion? sdk = null)

SentryMetric.Factory has no equivalent - it only reads scope?.Sdk:

metric.Attributes.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance);

Giving metrics the same explicit sdk parameter, and having each integration pass its own build-time Constants.SdkName (as SentryAspNetCoreStructuredLoggerProvider already does for logs), would make all data categories agree and remove the dependency on scope state that isn't populated when these are emitted.

Note the ?? SdkVersion.Instance in the snippet above is a guard for scope itself being null (HubExtensions.GetScope returns null for any IHub that isn't the concrete Hub) - it is not a fallback for an empty SdkVersion, which is why it does not cover this case.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .net codeBugSomething isn't workingLogs

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions