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
Summary
In an ASP.NET Core app, metrics and
SentrySdk.Loggerlogs emitted from inside a request handler are sent with nosentry.sdk.nameand nosentry.sdk.versionattribute. Events, transactions andILoggerlogs 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.AspNetCore6.9.0,<ImplicitUsings>enable</ImplicitUsings>. The custom transport captures envelopes locally so no real DSN is needed.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, nosentry.sdk.version.Expected
Both carry the SDK name and version, consistent with events and
ILoggerlogs from the same request.Cause
SentryMetric.FactoryandDefaultSentryStructuredLoggerread the SDK identity fromscope.Sdk, and during a request that object is empty.Adding a scope dump to the app above shows it directly:
Two writers populate
scope.Sdk, and neither reaches the request scope:SentryLoggerProviderdoeshub.PushScope()+ConfigureScopein 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 setssentry.dotnet.aspnetcore) is subscribed toscope.OnEvaluating.Scope.Evaluate()is only called fromSentryClient.CaptureEvent/CaptureTransaction/CaptureFeedback— never on the log or metric path.So the request scope's
Sdkhas nullNameandVersion, and both guards inSentryAttributes.SetDefaultAttributesare false.ILoggerlogs are unaffected becauseSentryAspNetCoreStructuredLoggerProviderpasses its ownSdkVersionexplicitly rather than going through the scope. Events and transactions are unaffected becauseEvaluate()runs beforescope.Apply(@event).Possible fix
Stop routing SDK identity through the scope on these paths.
SentryLog.SetDefaultAttributesalready accepts an optionalsdkparameter, which is how the logging integrations supply their own identity:SentryMetric.Factoryhas no equivalent - it only readsscope?.Sdk:Giving metrics the same explicit
sdkparameter, and having each integration pass its own build-timeConstants.SdkName(asSentryAspNetCoreStructuredLoggerProvideralready 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.Instancein the snippet above is a guard forscopeitself being null (HubExtensions.GetScopereturns null for anyIHubthat isn't the concreteHub) - it is not a fallback for an emptySdkVersion, which is why it does not cover this case.Related
SdkVersion.NameandSdkVersion.Versionare empty for Console-Apps #5352 - same underlying defect, reported for console apps.SdkVersion.Instance. With that applied, both items above carrysentry.dotnet- better than nothing, but still notsentry.dotnet.aspnetcorelike the event andILoggerlog from the same request. That PR does not close this issue.