Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
76035cd
feat(logs): add `log4net` integration
Flash0ver Apr 29, 2026
10916bf
test: fix
Flash0ver Apr 29, 2026
d5e3ab4
fix log4net sample app
Flash0ver Apr 29, 2026
d4e69a9
Format code
getsentry-bot Apr 29, 2026
8b180f1
ref: move to partial class
Flash0ver May 1, 2026
7a39c62
Merge branch 'main' into feat/logs-log4net
Flash0ver May 18, 2026
ca31e0b
test: add Properties-to-Attributes tests
Flash0ver May 18, 2026
7cc3bb1
Merge remote-tracking branch 'origin/main' into feat/logs-log4net
jamescrosswell Jul 15, 2026
441b655
fix(log4net): guard null RenderedMessage and clean up ThreadContext i…
jamescrosswell Jul 15, 2026
0a7f388
test(log4net): simplify structured log assertions with Sentry.Testing…
jamescrosswell Jul 15, 2026
575d2e6
feat(log4net): map LoggerName to category.name on structured logs
jamescrosswell Jul 16, 2026
56abb51
chore(log4net): drop redundant NETFRAMEWORK guard in sample
jamescrosswell Jul 16, 2026
b919a28
refactor(logs): inline SentryLog.Create instead of a separate partial…
jamescrosswell Jul 16, 2026
1f3468c
refactor(log4net): flatten CaptureStructuredLog with an early return
jamescrosswell Jul 16, 2026
c5d512d
fix(log4net): guard against null GetProperties() in structured logging
jamescrosswell Jul 16, 2026
13260a4
Refactor Appender to avoid a huge Append method
jamescrosswell Jul 16, 2026
2b31534
fix(log4net): honor appender settings and don't skip logs below Minim…
jamescrosswell Jul 16, 2026
4236d5f
Update src/Sentry.Log4Net/SentryAppender.cs
jamescrosswell Jul 16, 2026
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
2 changes: 1 addition & 1 deletion samples/Sentry.Samples.Log4Net/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal class Program
private static void Main()
{
// Set the user running the process the current principal
// Appender was configure to send the user with the event
// Appender was configured to send the user with the event
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

// The following anonymous object gets serialized and sent with log messages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net481</TargetFramework>
<Version>3.5.234</Version>
</PropertyGroup>

Expand Down
16 changes: 16 additions & 0 deletions src/Sentry.Log4Net/LevelMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,20 @@ internal static class LevelMapping
_ => null
};
}

public static SentryLogLevel? ToSentryLogLevel(this LoggingEvent loggingEvent)
{
return loggingEvent.Level switch
{
var level when level == Level.Off => null,
var level when level >= Level.Fatal => SentryLogLevel.Fatal,
var level when level >= Level.Error => SentryLogLevel.Error,
var level when level >= Level.Warn => SentryLogLevel.Warning,
var level when level >= Level.Info => SentryLogLevel.Info,
var level when level >= Level.Debug => SentryLogLevel.Debug,
var level when level >= Level.Trace => SentryLogLevel.Trace,
var level when level >= Level.All => SentryLogLevel.Trace,
_ => null,
};
Comment thread
cursor[bot] marked this conversation as resolved.
}
}
6 changes: 6 additions & 0 deletions src/Sentry.Log4Net/Sentry.Log4Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@
<InternalsVisibleTo Include="Sentry.Log4Net.Tests" PublicKey="$(SentryPublicKey)" />
</ItemGroup>

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

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

public partial class SentryAppender
{
private static void CaptureStructuredLog(IHub hub, SentryOptions options, LoggingEvent loggingEvent)
{
if (loggingEvent.ToSentryLogLevel() is not { } level)
{
return;
}

DateTimeOffset timestamp = new(loggingEvent.TimeStampUtc);
const string? template = null; // cannot get format-string from `log4net.Util.SystemStringFormat` via `LoggingEvent.MessageObject`
var parameters = ImmutableArray<KeyValuePair<string, object>>.Empty; // cannot get arguments from `log4net.Util.SystemStringFormat` via `LoggingEvent.MessageObject`

var message = !string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage) ? loggingEvent.RenderedMessage : string.Empty;
var log = SentryLog.Create(hub, timestamp, level, message, template, parameters);

var scope = hub.GetScope();
log.SetDefaultAttributes(options, scope, Sdk);
log.SetOrigin("auto.log.log4net");

if (loggingEvent.LoggerName is { } loggerName)
{
log.SetAttribute("category.name", loggerName);
}

// GetProperties() is non-null in current log4net, but the sibling GetLoggingEventProperties guards
// against null, so we do too rather than rely on log4net's internals.
if (loggingEvent.GetProperties() is { } properties)
{
foreach (var property in properties)
{
if (property is DictionaryEntry { Key: string key, Value: { } value })
{
if (key.Length != 0 && !key.StartsWith("log4net:", StringComparison.OrdinalIgnoreCase) && !Guid.TryParse(key, out _))
{
log.SetAttribute($"property.{key}", value);
}
}
}
}
Comment thread
cursor[bot] marked this conversation as resolved.

hub.Logger.CaptureLog(log);
}
}
48 changes: 40 additions & 8 deletions src/Sentry.Log4Net/SentryAppender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
/// <summary>
/// Sentry appender for log4net.
/// </summary>
public class SentryAppender : AppenderSkeleton
public partial class SentryAppender : AppenderSkeleton
{
private readonly Func<string, IDisposable> _initAction;
private volatile IDisposable? _sdkHandle;
Expand All @@ -15,6 +15,12 @@
internal static readonly SdkVersion NameAndVersion
= typeof(SentryAppender).Assembly.GetNameAndVersion();

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

private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name;

private readonly IHub _hub;
Expand Down Expand Up @@ -88,17 +94,30 @@

if (MinimumEventLevel is not null && loggingEvent.Level < MinimumEventLevel)
{
var message = !string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage) ? loggingEvent.RenderedMessage : string.Empty;
var category = loggingEvent.LoggerName;
var level = loggingEvent.ToBreadcrumbLevel();
IDictionary<string, string> data = GetLoggingEventProperties(loggingEvent)
.Where(kvp => kvp.Value != null)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value!.ToString() ?? "");
AddBreadcrumbFromLoggingEvent(loggingEvent);
return;

Check failure on line 98 in src/Sentry.Log4Net/SentryAppender.cs

View check run for this annotation

@sentry/warden / warden: code-review

Structured logs not captured for events below MinimumEventLevel due to early return

The `CaptureStructuredLog` call is placed after the early return at line 97-98, so structured logs are never captured for events below `MinimumEventLevel`. The test `DoAppend_StructuredLoggingWithoutException_LeavesBreadcrumb` expects structured logs to be captured even for breadcrumb-only events, but this code path returns before reaching `CaptureStructuredLog`.

Check failure on line 98 in src/Sentry.Log4Net/SentryAppender.cs

View check run for this annotation

@sentry/warden / warden: find-bugs

Structured logs not captured when log level is below MinimumEventLevel

When `EnableLogs` is true and the log level is below `MinimumEventLevel`, structured logs are not captured due to the early `return` on line 98. The test `DoAppend_StructuredLoggingWithoutException_LeavesBreadcrumb` expects structured logs to be captured in this scenario, and Serilog's implementation captures structured logs independently of event/breadcrumb thresholds. Add `CaptureStructuredLog` call before the early return, or refactor to call it at the end regardless of the event/breadcrumb path.
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.

_hub.AddBreadcrumb(message, category, type: null, data, level ?? default);
CreateSentryEvent(loggingEvent, exception);

var options = _hub.GetSentryOptions();
if (options is not { EnableLogs: true })
{
return;
}

try
{
CaptureStructuredLog(_hub, options, loggingEvent);
}
catch (Exception ex)
{
options.DiagnosticLogger?.LogError(ex, "Failed to capture structured log. The log will be dropped.");
}
}

private void CreateSentryEvent(LoggingEvent loggingEvent, Exception? exception)
{
var evt = new SentryEvent(exception)
{
Logger = loggingEvent.LoggerName,
Expand Down Expand Up @@ -139,6 +158,19 @@
_hub.CaptureEvent(evt);
}

private void AddBreadcrumbFromLoggingEvent(LoggingEvent loggingEvent)
{
var message = !string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage) ? loggingEvent.RenderedMessage : string.Empty;
var category = loggingEvent.LoggerName;
var level = loggingEvent.ToBreadcrumbLevel();
IDictionary<string, string> data = GetLoggingEventProperties(loggingEvent)
.Where(kvp => kvp.Value != null)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value!.ToString() ?? "");

_hub.AddBreadcrumb(message, category, type: null, data, level ?? default);
return;
}

private static IEnumerable<KeyValuePair<string, object?>> GetLoggingEventProperties(LoggingEvent loggingEvent)
{
var properties = loggingEvent.GetProperties();
Expand Down
14 changes: 14 additions & 0 deletions src/Sentry/SentryLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le
Parameters = ImmutableArray<KeyValuePair<string, object>>.Empty;
}

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

/// <summary>
/// The timestamp of the log.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions test/Sentry.Log4Net.Tests/Sentry.Log4Net.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@
<Using Include="Sentry.Log4Net" />
</ItemGroup>

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

</Project>
Loading
Loading