Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion samples/Sentry.Samples.NLog/NLog.config
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
ignoreEventsWithNoException="False"
includeEventDataOnBreadcrumbs="False"
includeEventPropertiesAsTags="True"
minimumEventLevel="Error">
minimumEventLevel="Error"
enableLogs="True">

<!-- Advanced options can be configured here-->
<options
Expand Down
1 change: 1 addition & 0 deletions samples/Sentry.Samples.NLog/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ private static void UsingCodeConfiguration()
options.MinimumEventLevel = LogLevel.Error; // Error and higher is sent as event (default is Error)

options.AttachStacktrace = true;
options.EnableLogs = true; // send structured logs to Sentry
options.SendDefaultPii = true; // Send Personal Identifiable information like the username of the user logged in to the device

options.IncludeEventDataOnBreadcrumbs = true; // Optionally include event properties with breadcrumbs
Expand Down
15 changes: 15 additions & 0 deletions src/Sentry.NLog/LogLevelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogLevel level)
_ => BreadcrumbLevel.Info
};
}

public static SentryLogLevel? ToSentryLogLevel(this LogLevel level)
{
return level.Name switch
{
nameof(LogLevel.Trace) => SentryLogLevel.Trace,
nameof(LogLevel.Debug) => SentryLogLevel.Debug,
nameof(LogLevel.Info) => SentryLogLevel.Info,
nameof(LogLevel.Warn) => SentryLogLevel.Warning,
nameof(LogLevel.Error) => SentryLogLevel.Error,
nameof(LogLevel.Fatal) => SentryLogLevel.Fatal,
nameof(LogLevel.Off) => null,
_ => null,
};
}
}
6 changes: 6 additions & 0 deletions src/Sentry.NLog/Sentry.NLog.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@
<InternalsVisibleTo Include="Sentry.NLog.Tests" PublicKey="$(SentryPublicKey)" />
</ItemGroup>

<ItemGroup>
<Compile Update="SentryTarget.Structured.cs">
<DependentUpon>SentryTarget.cs</DependentUpon>
</Compile>
</ItemGroup>

</Project>
72 changes: 72 additions & 0 deletions src/Sentry.NLog/SentryTarget.Structured.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Sentry.NLog;

public sealed partial class SentryTarget
{
private static void CaptureStructuredLog(IHub hub, SentryOptions options, LogEventInfo logEvent)
{
var level = logEvent.Level.ToSentryLogLevel();
if (level.HasValue)
{
DateTimeOffset timestamp = new(logEvent.TimeStamp);
GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes);

var log = SentryLog.Create(hub, timestamp, level.Value, logEvent.FormattedMessage, logEvent.Message, parameters);

log.SetDefaultAttributes(options, Sdk);
log.SetOrigin("auto.log.nlog");

foreach (var attribute in attributes)
{
log.SetAttribute(attribute.Key, attribute.Value);
}

hub.Logger.CaptureLog(log);
}
}

private static void GetStructuredLoggingParametersAndAttributes(LogEventInfo logEvent, out ImmutableArray<KeyValuePair<string, object>> parameters, out List<KeyValuePair<string, object>> attributes)
{
parameters = GetParameters(logEvent, out var parameterNames);
attributes = new List<KeyValuePair<string, object>>();

if (logEvent.HasProperties)
{
foreach (var property in logEvent.Properties)
{
if (property.Key is string key && !string.IsNullOrWhiteSpace(key) &&
property.Value is { } value &&
!parameterNames.Contains(key))
{
attributes.Add(new KeyValuePair<string, object>($"property.{key}", value));
}
}
}
}

private static ImmutableArray<KeyValuePair<string, object>> GetParameters(LogEventInfo logEvent, out HashSet<string> parameterNames)
{
var parameters = logEvent.MessageTemplateParameters;

if (parameters.Count == 0)
{
parameterNames = new HashSet<string>();
return ImmutableArray<KeyValuePair<string, object>>.Empty;
}

#if NETSTANDARD2_1_OR_GREATER || NET472_OR_GREATER || NETCOREAPP2_0_OR_GREATER

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could potentially simplify this. I think the only scenario where we don't take this codepath is if the TFM is netstandard2.0 right?

parameterNames = new HashSet<string>(parameters.Count);
#else
parameterNames = new HashSet<string>();
#endif

var @params = ImmutableArray.CreateBuilder<KeyValuePair<string, object>>(parameters.Count);

foreach (var parameter in parameters)
{
parameterNames.Add(parameter.Name);
@params.Add(new KeyValuePair<string, object>(parameter.Name, parameter.Value));
}

return @params.DrainToImmutable();
}
}
22 changes: 21 additions & 1 deletion src/Sentry.NLog/SentryTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// Sentry NLog Target.
/// </summary>
[Target("Sentry")]
public sealed class SentryTarget : TargetWithContext
public sealed partial class SentryTarget : TargetWithContext
{
// For testing:
internal Func<IHub> HubAccessor { get; }
Expand All @@ -14,6 +14,12 @@

internal static readonly SdkVersion NameAndVersion = typeof(SentryTarget).Assembly.GetNameAndVersion();

private static readonly SdkVersion Sdk = new()
{
Name = Constants.SdkName,
Version = NameAndVersion.Version,
};

internal static readonly string AdditionalGroupingKeyProperty = "AdditionalGroupingKey";

private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name;
Expand Down Expand Up @@ -129,6 +135,15 @@
set => Options.MinimumBreadcrumbLevel = LogLevel.FromString(value);
}

/// <summary>
/// Controls whether logs are generated and sent.
/// </summary>
public bool EnableLogs
{
get => Options.EnableLogs;
set => Options.EnableLogs = value;
}

/// <summary>
/// Whether the NLog integration should initialize the SDK.
/// </summary>
Expand Down Expand Up @@ -331,6 +346,11 @@
var shouldOnlyLogExceptions = exception == null && IgnoreEventsWithNoException;
var shouldIncludeProperties = ContextProperties?.Count > 0 || ShouldIncludeProperties(logEvent);

if (Options.EnableLogs)
{
CaptureStructuredLog(hub, Options, logEvent);
}

Check warning on line 352 in src/Sentry.NLog/SentryTarget.cs

View check run for this annotation

@sentry/warden / warden: find-bugs

NLog integration ignores hub-level `EnableLogs` when SDK is initialized externally

The structured-log guard reads only the NLog target's own `Options.EnableLogs` (defaults to `false`), so if the SDK is initialized elsewhere (e.g. ASP.NET Core) with `EnableLogs = true`, NLog will never emit structured logs. The Serilog integration already handles this by reading `hub.GetSentryOptions()?.EnableLogs` instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NLog reads EnableLogs from wrong options source

High Severity

The NLog integration checks Options.EnableLogs (the target's own SentryNLogOptions) and passes Options to CaptureStructuredLog, but the Serilog integration explicitly reads from hub.GetSentryOptions() instead, with a detailed comment explaining why. When the SDK is initialized elsewhere (e.g., ASP.NET Core) and InitializeSdk is false, the NLog target's own Options.EnableLogs remains the default false, so structured logs silently won't be sent even though the user enabled them on the actual SDK options. The options passed to SetDefaultAttributes may also carry incorrect Environment/Release values.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f558baf. Configure here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks out - I think the same reasoning applies here:

// Read the options from the Hub, rather than the Sink's Serilog-Options, because 'EnableLogs' is declared in the base 'SentryOptions', rather than the derived 'SentrySerilogOptions'.
// In cases where Sentry's Serilog-Sink is added without a DSN (i.e., without initializing the SDK) and the SDK is initialized differently (e.g., through ASP.NET Core),
// then the 'EnableLogs' option of this Sink's Serilog-Options is default, but the Hub's Sentry-Options have the actual user-defined value configured.
var options = hub.GetSentryOptions();
if (options?.EnableLogs is true)
{
CaptureStructuredLog(hub, options, logEvent, formatted, template);
}

NB: More evidence to support #5245


if (logEvent.Level >= Options.MinimumEventLevel && !shouldOnlyLogExceptions)
{
var formatted = RenderLogEvent(Layout, logEvent);
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry/Sentry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@
<Compile Update="MeasurementUnit.Duration.cs;MeasurementUnit.Fraction.cs;MeasurementUnit.Information.cs">
<DependentUpon>MeasurementUnit.cs</DependentUpon>
</Compile>
<Compile Update="SentryLog.Factory.cs">
<DependentUpon>SentryLog.cs</DependentUpon>
</Compile>
<Compile Update="SentryMetric.Factory.cs;SentryMetric.Generic.cs">
<DependentUpon>SentryMetric.cs</DependentUpon>
</Compile>
Expand Down
18 changes: 18 additions & 0 deletions src/Sentry/SentryLog.Factory.cs

@jamescrosswell jamescrosswell Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big fan of splitting a single method out into a separate file (and the extra overhead that adds to the csproj file). My preference would be to simply add it to SentryLog.cs... the method name itself lets you know it's a factory method.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Sentry;

public sealed partial class SentryLog
{
internal static SentryLog Create(IHub hub, DateTimeOffset timestamp, SentryLogLevel level, string message, string? template, ImmutableArray<KeyValuePair<string, object>> parameters)
{
hub.GetTraceIdAndSpanId(out var traceId, out var spanId);

SentryLog log = new(timestamp, traceId, level, message)
{
Template = template,
Parameters = parameters,
SpanId = spanId,
};

return log;
}
}
2 changes: 1 addition & 1 deletion src/Sentry/SentryLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Sentry;
/// Sentry .NET SDK Docs: <see href="https://docs.sentry.io/platforms/dotnet/logs/"/>.
/// </remarks>
[DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")]
public sealed class SentryLog
public sealed partial class SentryLog
{
[SetsRequiredMembers]
internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel level, string message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
public NLog.Layouts.Layout? BreadcrumbCategory { get; set; }
public NLog.Layouts.Layout? BreadcrumbLayout { get; set; }
public NLog.Layouts.Layout? Dsn { get; set; }
public bool EnableLogs { get; set; }

Check warning on line 62 in test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt

View check run for this annotation

@sentry/warden / warden: find-bugs

[NPG-CG5] NLog integration ignores hub-level `EnableLogs` when SDK is initialized externally (additional location)

The structured-log guard reads only the NLog target's own `Options.EnableLogs` (defaults to `false`), so if the SDK is initialized elsewhere (e.g. ASP.NET Core) with `EnableLogs = true`, NLog will never emit structured logs. The Serilog integration already handles this by reading `hub.GetSentryOptions()?.EnableLogs` instead.

Check warning on line 62 in test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt

View check run for this annotation

@sentry/warden / warden: code-review

`SentryTarget` ignores hub's `EnableLogs` when SDK is initialized externally

When `InitializeSdk = false` (SDK is initialized elsewhere, e.g., ASP.NET Core, with `EnableLogs = true`), `SentryTarget.InnerWrite` only checks its own `Options.EnableLogs` (defaulting to `false`), so structured logs are silently dropped. The Serilog integration explicitly handles this case in `SentrySink.cs` (lines 168–172) by reading `EnableLogs` from `hub.GetSentryOptions()` with a comment explaining the rationale. The same fallback should be applied in `SentryTarget.cs`.
public NLog.Layouts.Layout? Environment { get; set; }
public int FlushTimeoutSeconds { get; set; }
public bool IgnoreEventsWithNoException { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
public NLog.Layouts.Layout? BreadcrumbCategory { get; set; }
public NLog.Layouts.Layout? BreadcrumbLayout { get; set; }
public NLog.Layouts.Layout? Dsn { get; set; }
public bool EnableLogs { get; set; }

Check warning on line 62 in test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt

View check run for this annotation

@sentry/warden / warden: code-review

[C3C-AZL] `SentryTarget` ignores hub's `EnableLogs` when SDK is initialized externally (additional location)

When `InitializeSdk = false` (SDK is initialized elsewhere, e.g., ASP.NET Core, with `EnableLogs = true`), `SentryTarget.InnerWrite` only checks its own `Options.EnableLogs` (defaulting to `false`), so structured logs are silently dropped. The Serilog integration explicitly handles this case in `SentrySink.cs` (lines 168–172) by reading `EnableLogs` from `hub.GetSentryOptions()` with a comment explaining the rationale. The same fallback should be applied in `SentryTarget.cs`.
public NLog.Layouts.Layout? Environment { get; set; }
public int FlushTimeoutSeconds { get; set; }
public bool IgnoreEventsWithNoException { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
public NLog.Layouts.Layout? BreadcrumbCategory { get; set; }
public NLog.Layouts.Layout? BreadcrumbLayout { get; set; }
public NLog.Layouts.Layout? Dsn { get; set; }
public bool EnableLogs { get; set; }
public NLog.Layouts.Layout? Environment { get; set; }

Check warning on line 63 in test/Sentry.NLog.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt

View check run for this annotation

@sentry/warden / warden: code-review

[C3C-AZL] `SentryTarget` ignores hub's `EnableLogs` when SDK is initialized externally (additional location)

When `InitializeSdk = false` (SDK is initialized elsewhere, e.g., ASP.NET Core, with `EnableLogs = true`), `SentryTarget.InnerWrite` only checks its own `Options.EnableLogs` (defaulting to `false`), so structured logs are silently dropped. The Serilog integration explicitly handles this case in `SentrySink.cs` (lines 168–172) by reading `EnableLogs` from `hub.GetSentryOptions()` with a comment explaining the rationale. The same fallback should be applied in `SentryTarget.cs`.
public int FlushTimeoutSeconds { get; set; }
public bool IgnoreEventsWithNoException { get; set; }
public bool IncludeEventDataOnBreadcrumbs { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ namespace Sentry.NLog
public NLog.Layouts.Layout? BreadcrumbCategory { get; set; }
public NLog.Layouts.Layout? BreadcrumbLayout { get; set; }
public NLog.Layouts.Layout? Dsn { get; set; }
public bool EnableLogs { get; set; }
public NLog.Layouts.Layout? Environment { get; set; }
public int FlushTimeoutSeconds { get; set; }
public bool IgnoreEventsWithNoException { get; set; }
Expand Down
6 changes: 6 additions & 0 deletions test/Sentry.NLog.Tests/Sentry.NLog.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@

</ItemGroup>

<ItemGroup>
<Compile Update="SentryTargetTests.Structured.cs">
<DependentUpon>SentryTargetTests.cs</DependentUpon>
</Compile>
</ItemGroup>

</Project>
Loading
Loading