Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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>
57 changes: 57 additions & 0 deletions src/Sentry.Log4Net/SentryAppender.Structured.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Sentry.Log4Net;

public partial class SentryAppender
{
private static void CaptureStructuredLog(IHub hub, SentryOptions options, LoggingEvent loggingEvent, string? environment, bool sendIdentity)
{
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");

// Honor the appender-level settings, overriding the scope/options defaults, to match the SentryEvent path.
if (!string.IsNullOrWhiteSpace(environment))
{
log.SetAttribute("sentry.environment", environment!);
}

if (sendIdentity && !string.IsNullOrEmpty(loggingEvent.Identity))
{
log.SetAttribute("user.id", loggingEvent.Identity);
}

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);
}
}
53 changes: 45 additions & 8 deletions src/Sentry.Log4Net/SentryAppender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Sentry.Log4Net;
/// <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 @@ public class SentryAppender : AppenderSkeleton
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 @@ -84,21 +90,39 @@ protected override void Append(LoggingEvent loggingEvent)
}
}

CaptureStructuredLogIfEnabled(loggingEvent);

var exception = loggingEvent.ExceptionObject ?? loggingEvent.MessageObject as Exception;

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;
Comment thread
cursor[bot] marked this conversation as resolved.
}

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

private void CaptureStructuredLogIfEnabled(LoggingEvent loggingEvent)
{
var options = _hub.GetSentryOptions();
if (options is not { EnableLogs: true })
{
return;
}

try
{
CaptureStructuredLog(_hub, options, loggingEvent, Environment, SendIdentity);
}
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 +163,19 @@ protected override void Append(LoggingEvent loggingEvent)
_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