Skip to content

Commit eadf5f2

Browse files
Flash0verbruno-garciajamescrosswellgetsentry-botgetsentry-bot
authored
feat: add Sentry Logs (experimental) (#4308)
* feat(logs): initial API for Sentry Logs (#4158) * feat(logs): add Buffering and Batching (#4310) * feat(logs): Logs for `Sentry.Extensions.Logging` and integrations for `Sentry.AspNetCore` and `Sentry.Maui` (#4193) Co-authored-by: Bruno Garcia <[email protected]> Co-authored-by: James Crosswell <[email protected]> Co-authored-by: Sentry Github Bot <[email protected]> Co-authored-by: getsentry-bot <[email protected]> Co-authored-by: getsentry-bot <[email protected]>
1 parent de422d3 commit eadf5f2

File tree

72 files changed

+5198
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+5198
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add _experimental_ support for [Sentry Structured Logging](https://docs.sentry.io/product/explore/logs/) ([#4308](https://github.com/getsentry/sentry-dotnet/pull/4308))
8+
- Structured-Logger API ([#4158](https://github.com/getsentry/sentry-dotnet/pull/4158))
9+
- Buffering and Batching ([#4310](https://github.com/getsentry/sentry-dotnet/pull/4310))
10+
- Integrations for `Sentry.Extensions.Logging`, `Sentry.AspNetCore` and `Sentry.Maui` ([#4193](https://github.com/getsentry/sentry-dotnet/pull/4193))
11+
512
### Fixes
613

714
- Update `sample_rate` of _Dynamic Sampling Context (DSC)_ when making sampling decisions ([#4374](https://github.com/getsentry/sentry-dotnet/pull/4374))
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
```
2+
3+
BenchmarkDotNet v0.13.12, macOS 15.5 (24F74) [Darwin 24.5.0]
4+
Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
5+
.NET SDK 9.0.301
6+
[Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
7+
DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
8+
9+
10+
```
11+
| Method | Mean | Error | StdDev | Gen0 | Allocated |
12+
|------- |---------:|--------:|--------:|-------:|----------:|
13+
| Log | 288.4 ns | 1.28 ns | 1.20 ns | 0.1163 | 976 B |
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
```
2+
3+
BenchmarkDotNet v0.13.12, macOS 15.5 (24F74) [Darwin 24.5.0]
4+
Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
5+
.NET SDK 9.0.301
6+
[Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
7+
DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
8+
9+
10+
```
11+
| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Gen0 | Allocated |
12+
|---------------- |----------- |-------------------- |------------:|---------:|---------:|-------:|----------:|
13+
| **EnqueueAndFlush** | **10** | **100** | **1,774.5 ns** | **7.57 ns** | **6.71 ns** | **0.6104** | **5 KB** |
14+
| **EnqueueAndFlush** | **10** | **200** | **3,468.5 ns** | **11.16 ns** | **10.44 ns** | **1.2207** | **10 KB** |
15+
| **EnqueueAndFlush** | **10** | **1000** | **17,259.7 ns** | **51.92 ns** | **46.02 ns** | **6.1035** | **50 KB** |
16+
| **EnqueueAndFlush** | **100** | **100** | **857.5 ns** | **4.21 ns** | **3.73 ns** | **0.1469** | **1.2 KB** |
17+
| **EnqueueAndFlush** | **100** | **200** | **1,681.4 ns** | **1.74 ns** | **1.63 ns** | **0.2937** | **2.41 KB** |
18+
| **EnqueueAndFlush** | **100** | **1000** | **8,302.2 ns** | **12.00 ns** | **10.64 ns** | **1.4648** | **12.03 KB** |
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#nullable enable
2+
3+
using BenchmarkDotNet.Attributes;
4+
using Microsoft.Extensions.Logging;
5+
using Sentry.Extensibility;
6+
using Sentry.Extensions.Logging;
7+
using Sentry.Internal;
8+
using Sentry.Testing;
9+
10+
namespace Sentry.Benchmarks.Extensions.Logging;
11+
12+
public class SentryStructuredLoggerBenchmarks
13+
{
14+
private Hub _hub = null!;
15+
private Sentry.Extensions.Logging.SentryStructuredLogger _logger = null!;
16+
private LogRecord _logRecord = null!;
17+
private SentryLog? _lastLog;
18+
19+
[GlobalSetup]
20+
public void Setup()
21+
{
22+
SentryLoggingOptions options = new()
23+
{
24+
Dsn = DsnSamples.ValidDsn,
25+
Experimental =
26+
{
27+
EnableLogs = true,
28+
},
29+
ExperimentalLogging =
30+
{
31+
MinimumLogLevel = LogLevel.Information,
32+
}
33+
};
34+
options.Experimental.SetBeforeSendLog((SentryLog log) =>
35+
{
36+
_lastLog = log;
37+
return null;
38+
});
39+
40+
MockClock clock = new(new DateTimeOffset(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2)));
41+
SdkVersion sdk = new()
42+
{
43+
Name = "SDK Name",
44+
Version = "SDK Version",
45+
};
46+
47+
_hub = new Hub(options, DisabledHub.Instance);
48+
_logger = new Sentry.Extensions.Logging.SentryStructuredLogger("CategoryName", options, _hub, clock, sdk);
49+
_logRecord = new LogRecord(LogLevel.Information, new EventId(2025, "EventName"), new InvalidOperationException("exception-message"), "Number={Number}, Text={Text}", 2018, "message");
50+
}
51+
52+
[Benchmark]
53+
public void Log()
54+
{
55+
_logger.Log(_logRecord.LogLevel, _logRecord.EventId, _logRecord.Exception, _logRecord.Message, _logRecord.Args);
56+
}
57+
58+
[GlobalCleanup]
59+
public void Cleanup()
60+
{
61+
_hub.Dispose();
62+
63+
if (_lastLog is null)
64+
{
65+
throw new InvalidOperationException("Last Log is null");
66+
}
67+
if (_lastLog.Message != "Number=2018, Text=message")
68+
{
69+
throw new InvalidOperationException($"Last Log with Message: '{_lastLog.Message}'");
70+
}
71+
}
72+
73+
private sealed class LogRecord
74+
{
75+
public LogRecord(LogLevel logLevel, EventId eventId, Exception? exception, string? message, params object?[] args)
76+
{
77+
LogLevel = logLevel;
78+
EventId = eventId;
79+
Exception = exception;
80+
Message = message;
81+
Args = args;
82+
}
83+
84+
public LogLevel LogLevel { get; }
85+
public EventId EventId { get; }
86+
public Exception? Exception { get; }
87+
public string? Message { get; }
88+
public object?[] Args { get; }
89+
}
90+
}

benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
<ItemGroup>
1616
<ProjectReference Include="..\..\src\Sentry\Sentry.csproj" />
17+
<ProjectReference Include="..\..\src\Sentry.Extensions.Logging\Sentry.Extensions.Logging.csproj" />
1718
<ProjectReference Include="..\..\src\Sentry.Profiling\Sentry.Profiling.csproj" />
1819
<ProjectReference Include="..\..\test\Sentry.Testing\Sentry.Testing.csproj" />
1920
</ItemGroup>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using BenchmarkDotNet.Attributes;
2+
using NSubstitute;
3+
using Sentry.Extensibility;
4+
using Sentry.Internal;
5+
6+
namespace Sentry.Benchmarks;
7+
8+
public class StructuredLogBatchProcessorBenchmarks
9+
{
10+
private Hub _hub;
11+
private StructuredLogBatchProcessor _batchProcessor;
12+
private SentryLog _log;
13+
14+
[Params(10, 100)]
15+
public int BatchCount { get; set; }
16+
17+
[Params(100, 200, 1_000)]
18+
public int OperationsPerInvoke { get; set; }
19+
20+
[GlobalSetup]
21+
public void Setup()
22+
{
23+
SentryOptions options = new()
24+
{
25+
Dsn = DsnSamples.ValidDsn,
26+
Experimental =
27+
{
28+
EnableLogs = true,
29+
},
30+
};
31+
32+
var batchInterval = Timeout.InfiniteTimeSpan;
33+
34+
var clientReportRecorder = Substitute.For<IClientReportRecorder>();
35+
clientReportRecorder
36+
.When(static recorder => recorder.RecordDiscardedEvent(Arg.Any<DiscardReason>(), Arg.Any<DataCategory>(), Arg.Any<int>()))
37+
.Throw<UnreachableException>();
38+
39+
var diagnosticLogger = Substitute.For<IDiagnosticLogger>();
40+
diagnosticLogger
41+
.When(static logger => logger.IsEnabled(Arg.Any<SentryLevel>()))
42+
.Throw<UnreachableException>();
43+
diagnosticLogger
44+
.When(static logger => logger.Log(Arg.Any<SentryLevel>(), Arg.Any<string>(), Arg.Any<Exception>(), Arg.Any<object[]>()))
45+
.Throw<UnreachableException>();
46+
47+
_hub = new Hub(options, DisabledHub.Instance);
48+
_batchProcessor = new StructuredLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, diagnosticLogger);
49+
_log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message");
50+
}
51+
52+
[Benchmark]
53+
public void EnqueueAndFlush()
54+
{
55+
for (var i = 0; i < OperationsPerInvoke; i++)
56+
{
57+
_batchProcessor.Enqueue(_log);
58+
}
59+
_batchProcessor.Flush();
60+
}
61+
62+
[GlobalCleanup]
63+
public void Cleanup()
64+
{
65+
_batchProcessor.Dispose();
66+
_hub.Dispose();
67+
}
68+
}

samples/Sentry.Samples.AspNetCore.Basic/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
// Log debug information about the Sentry SDK
1616
options.Debug = true;
1717
#endif
18+
19+
// This option enables Logs sent to Sentry.
20+
options.Experimental.EnableLogs = true;
1821
});
1922

2023
var app = builder.Build();

samples/Sentry.Samples.Console.Basic/Program.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* - Error Monitoring (both handled and unhandled exceptions)
44
* - Performance Tracing (Transactions / Spans)
55
* - Release Health (Sessions)
6+
* - Logs
67
* - MSBuild integration for Source Context (see the csproj)
78
*
89
* For more advanced features of the SDK, see Sentry.Samples.Console.Customized.
@@ -35,6 +36,20 @@
3536

3637
// This option tells Sentry to capture 100% of traces. You still need to start transactions and spans.
3738
options.TracesSampleRate = 1.0;
39+
40+
// This option enables Sentry Logs created via SentrySdk.Logger.
41+
options.Experimental.EnableLogs = true;
42+
options.Experimental.SetBeforeSendLog(static log =>
43+
{
44+
// A demonstration of how you can drop logs based on some attribute they have
45+
if (log.TryGetAttribute("suppress", out var attribute) && attribute is true)
46+
{
47+
return null;
48+
}
49+
50+
// Drop logs with level Info
51+
return log.Level is SentryLogLevel.Info ? null : log;
52+
});
3853
});
3954

4055
// This starts a new transaction and attaches it to the scope.
@@ -58,6 +73,7 @@ async Task FirstFunction()
5873
var httpClient = new HttpClient(messageHandler, true);
5974
var html = await httpClient.GetStringAsync("https://example.com/");
6075
WriteLine(html);
76+
SentrySdk.Experimental.Logger.LogInfo("HTTP Request completed.");
6177
}
6278

6379
async Task SecondFunction()
@@ -77,6 +93,8 @@ async Task SecondFunction()
7793
// This is an example of capturing a handled exception.
7894
SentrySdk.CaptureException(exception);
7995
span.Finish(exception);
96+
97+
SentrySdk.Experimental.Logger.LogError("Error with message: {0}", [exception.Message], static log => log.SetAttribute("method", nameof(SecondFunction)));
8098
}
8199

82100
span.Finish();
@@ -90,6 +108,8 @@ async Task ThirdFunction()
90108
// Simulate doing some work
91109
await Task.Delay(100);
92110

111+
SentrySdk.Experimental.Logger.LogFatal("Crash imminent!", [], static log => log.SetAttribute("suppress", true));
112+
93113
// This is an example of an unhandled exception. It will be captured automatically.
94114
throw new InvalidOperationException("Something happened that crashed the app!");
95115
}

samples/Sentry.Samples.ME.Logging/Program.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@
2323
// Optionally configure options: The default values are:
2424
options.MinimumBreadcrumbLevel = LogLevel.Information; // It requires at least this level to store breadcrumb
2525
options.MinimumEventLevel = LogLevel.Error; // This level or above will result in event sent to Sentry
26+
options.ExperimentalLogging.MinimumLogLevel = LogLevel.Trace; // This level or above will result in log sent to Sentry
2627

28+
// This option enables Logs sent to Sentry.
29+
options.Experimental.EnableLogs = true;
30+
options.Experimental.SetBeforeSendLog(static log =>
31+
{
32+
log.SetAttribute("attribute-key", "attribute-value");
33+
return log;
34+
});
35+
36+
// TODO: AddLogEntryFilter
2737
// Don't keep as a breadcrumb or send events for messages of level less than Critical with exception of type DivideByZeroException
2838
options.AddLogEntryFilter((_, level, _, exception) => level < LogLevel.Critical && exception is DivideByZeroException);
2939

samples/Sentry.Samples.Maui/MauiProgram.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static MauiApp CreateMauiApp()
3333
options.AttachScreenshot = true;
3434

3535
options.Debug = true;
36+
options.Experimental.EnableLogs = true;
3637
options.SampleRate = 1.0F;
3738

3839
// The Sentry MVVM Community Toolkit integration automatically creates traces for async relay commands,

0 commit comments

Comments
 (0)