Skip to content
Draft
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
4 changes: 3 additions & 1 deletion samples/Sentry.Samples.Log4Net/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ internal class Program

private static void Main()
{
#if NETFRAMEWORK
// 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);
#endif

// The following anonymous object gets serialized and sent with log messages
ThreadContext.Properties["inventory"] = new
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,
};
}
}
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>
33 changes: 33 additions & 0 deletions src/Sentry.Log4Net/SentryAppender.Structured.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Sentry.Log4Net;

public partial class SentryAppender
{
private static void CaptureStructuredLog(IHub hub, SentryOptions options, LoggingEvent loggingEvent)
{
var level = loggingEvent.ToSentryLogLevel();
if (level.HasValue)
{
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 log = SentryLog.Create(hub, timestamp, level.Value, loggingEvent.RenderedMessage, template, parameters);

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

foreach (var property in loggingEvent.GetProperties())
{
if (property is DictionaryEntry { Key: string key, Value: { } value })
{
if (key.Length != 0 && !key.StartsWith("log4net:", StringComparison.OrdinalIgnoreCase))
{
log.SetAttribute($"property.{key}", value);
}
}
}

hub.Logger.CaptureLog(log);
}
}
}
14 changes: 13 additions & 1 deletion 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,6 +90,12 @@ protected override void Append(LoggingEvent loggingEvent)
}
}

var options = _hub.GetSentryOptions();
if (options is { EnableLogs: true })
{
CaptureStructuredLog(_hub, options, loggingEvent);
}

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

if (MinimumEventLevel is not null && loggingEvent.Level < MinimumEventLevel)
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry/Sentry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,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
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
{
private readonly Dictionary<string, SentryAttribute> _attributes;

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>
203 changes: 203 additions & 0 deletions test/Sentry.Log4Net.Tests/SentryAppenderTests.Structured.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#nullable enable

using log4net.Util;

namespace Sentry.Log4Net.Tests;

public partial class SentryAppenderTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DoAppend_StructuredLogging_IsEnabled(bool isEnabled)
{
InMemorySentryStructuredLogger capturer = new();
_fixture.Hub.Logger.Returns(capturer);
_fixture.Options.EnableLogs = isEnabled;

var sut = _fixture.GetSut();

sut.DoAppend(CreateLoggingEvent(Level.Info, "Message"));

capturer.Logs.Should().HaveCount(isEnabled ? 1 : 0);
}

public static TheoryData<Level, SentryLogLevel> LogLevelData => new()
{
{ Level.All, SentryLogLevel.Trace },
{ Level.Finest, SentryLogLevel.Trace },
{ Level.Verbose, SentryLogLevel.Trace },
{ Level.Finer, SentryLogLevel.Trace },
{ Level.Trace, SentryLogLevel.Trace },
{ Level.Fine, SentryLogLevel.Debug },
{ Level.Debug, SentryLogLevel.Debug },
{ Level.Info, SentryLogLevel.Info },
{ Level.Notice, SentryLogLevel.Info },
{ Level.Warn, SentryLogLevel.Warning },
{ Level.Error, SentryLogLevel.Error },
{ Level.Severe, SentryLogLevel.Error },
{ Level.Critical, SentryLogLevel.Error },
{ Level.Alert, SentryLogLevel.Error },
{ Level.Fatal, SentryLogLevel.Fatal },
{ Level.Emergency, SentryLogLevel.Fatal },
{ Level.Log4Net_Debug, SentryLogLevel.Fatal },
{ new Level(0, "DEFAULT"), SentryLogLevel.Trace },
{ new Level(-1, "CUSTOM"), SentryLogLevel.Trace },
};

[Theory]
[MemberData(nameof(LogLevelData))]
public void DoAppend_StructuredLogging_LogLevel(Level level, SentryLogLevel expected)
{
InMemorySentryStructuredLogger capturer = new();
_fixture.Hub.Logger.Returns(capturer);
_fixture.Options.EnableLogs = true;

var sut = _fixture.GetSut();

sut.DoAppend(CreateLoggingEvent(level, "Message"));

capturer.Logs.Should().ContainSingle().Which.Level.Should().Be(expected);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void DoAppend_StructuredLogging_LogEvent(bool withActiveSpan)
{
InMemorySentryStructuredLogger capturer = new();
_fixture.Hub.Logger.Returns(capturer);
_fixture.Options.EnableLogs = true;
_fixture.Options.Environment = "test-environment";
_fixture.Options.Release = "test-release";

if (withActiveSpan)
{
var span = Substitute.For<ISpan>();
span.TraceId.Returns(SentryId.Create());
span.SpanId.Returns(SpanId.Create());
_fixture.Hub.GetSpan().Returns(span);
}
else
{
_fixture.Hub.GetSpan().Returns((ISpan?)null);
}

var sut = _fixture.GetSut();
ThreadContext.Properties["Text-Property"] = "4";
ThreadContext.Properties["Number-Property"] = 4;
ThreadContext.Properties["Collection-Property"] = new[] { 3, 4, 5 };
ThreadContext.Properties["Object-Property"] = (Number: 4, Text: "4");

sut.DoAppend(CreateLoggingEvent(Level.Info, "{0}, {1}, {2}", [0, 1, 2]));

var log = capturer.Logs.Should().ContainSingle().Which;
log.Timestamp.Should().BeOnOrBefore(DateTimeOffset.Now);
log.TraceId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.TraceId : _fixture.Scope.PropagationContext.TraceId);
log.Level.Should().Be(SentryLogLevel.Info);
log.Message.Should().Be("0, 1, 2");
log.Template.Should().BeNull();
log.Parameters.Should().BeEmpty();
log.SpanId.Should().Be(withActiveSpan ? _fixture.Hub.GetSpan()!.SpanId : null);

log.TryGetAttribute("sentry.environment", out object? environment).Should().BeTrue();
environment.Should().Be("test-environment");
log.TryGetAttribute("sentry.release", out object? release).Should().BeTrue();
release.Should().Be("test-release");
log.TryGetAttribute("sentry.origin", out object? origin).Should().BeTrue();
origin.Should().Be("auto.log.log4net");
log.TryGetAttribute("sentry.sdk.name", out object? sdkName).Should().BeTrue();
sdkName.Should().Be(SentryAppender.SdkName);
log.TryGetAttribute("sentry.sdk.version", out object? sdkVersion).Should().BeTrue();
sdkVersion.Should().Be(SentryAppender.NameAndVersion.Version);

log.TryGetAttribute("property.Text-Property", out object? text).Should().BeTrue();
text.Should().Be("4");
log.TryGetAttribute("property.Number-Property", out object? number).Should().BeTrue();
number.Should().Be(4);
log.TryGetAttribute("property.Collection-Property", out object? collection).Should().BeTrue();
collection.Should().BeEquivalentTo(new[] { 3, 4, 5 });
log.TryGetAttribute("property.Object-Property", out object? obj).Should().BeTrue();
obj.Should().Be((Number: 4, Text: "4"));
}

[Fact]
public void DoAppend_StructuredLogging_Properties()
{
InMemorySentryStructuredLogger capturer = new();
_fixture.Hub.Logger.Returns(capturer);
_fixture.Options.EnableLogs = true;

var sut = _fixture.GetSut();

LoggingEventData data = new()
{
LoggerName = "TestLogger",
Level = Level.Info,
Message = "Test Message",
ThreadName = "1",
LocationInfo = new LocationInfo(null),
UserName = "TestUser",
Identity = "TestIdentity",
ExceptionString = "Exception",
Domain = "TestDomain",
Properties = new PropertiesDictionary(),
TimeStampUtc = DateTime.UtcNow,
};
LoggingEvent loggingEvent = new(data);
sut.DoAppend(loggingEvent);

var log = capturer.Logs.Should().ContainSingle().Which;
log.Level.Should().Be(SentryLogLevel.Info);
log.Message.Should().Be("Test Message");

//TODO: assert Count/Length of Attributes
//requires: https://github.com/getsentry/sentry-dotnet/pull/4936
//should not contain "log4net:.." properties
Comment on lines +154 to +156
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

todo: add missing assertions

}

[Fact]
public void DoAppend_StructuredLoggingWithException_NoBreadcrumb()
{
InMemorySentryStructuredLogger capturer = new();
_fixture.Hub.Logger.Returns(capturer);
_fixture.Options.EnableLogs = true;

var sut = _fixture.GetSut();
sut.MinimumEventLevel = Level.Error;

sut.DoAppend(CreateLoggingEvent(Level.Error, "Message", new Exception("expected message")));

capturer.Logs.Should().ContainSingle().Which.Message.Should().Be("Message");
_fixture.Scope.Breadcrumbs.Should().BeEmpty();
_ = _fixture.Hub.Received(1).CaptureEvent(Arg.Any<SentryEvent>());
}

[Fact]
public void DoAppend_StructuredLoggingWithoutException_LeavesBreadcrumb()
{
InMemorySentryStructuredLogger capturer = new();
_fixture.Hub.Logger.Returns(capturer);
_fixture.Options.EnableLogs = true;

var sut = _fixture.GetSut();
sut.MinimumEventLevel = Level.Fatal;

sut.DoAppend(CreateLoggingEvent(Level.Error, "Message"));

capturer.Logs.Should().ContainSingle().Which.Message.Should().Be("Message");
_fixture.Scope.Breadcrumbs.Should().ContainSingle().Which.Message.Should().Be("Message");
_ = _fixture.Hub.Received(0).CaptureEvent(Arg.Any<SentryEvent>());
}

private static LoggingEvent CreateLoggingEvent(Level level, string message, Exception? exception = null)
{
return new LoggingEvent(null, null, "TestLogger", level, message, exception);
}

private static LoggingEvent CreateLoggingEvent(Level level, string format, object[] args)
{
var message = new SystemStringFormat(CultureInfo.InvariantCulture, format, args);
return new LoggingEvent(null, null, "TestLogger", level, message, null);
}
}
Loading
Loading