Skip to content

Commit 8d03449

Browse files
committed
feat(logs): Sentry.Extensions.Logging
1 parent 9a51033 commit 8d03449

12 files changed

+240
-2
lines changed

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

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

24+
// This option enables the (experimental) Sentry Logs.
25+
options.EnableLogs = true;
26+
options.SetBeforeSendLog(static log =>
27+
{
28+
log.SetAttribute("attribute-key", "attribute-value");
29+
return log;
30+
});
31+
32+
// TODO: AddLogEntryFilter
2333
// Don't keep as a breadcrumb or send events for messages of level less than Critical with exception of type DivideByZeroException
2434
options.AddLogEntryFilter((_, level, _, exception) => level < LogLevel.Critical && exception is DivideByZeroException);
2535

src/Sentry.Extensions.Logging/BindableSentryLoggingOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ internal class BindableSentryLoggingOptions : BindableSentryOptions
77
{
88
public LogLevel? MinimumBreadcrumbLevel { get; set; }
99
public LogLevel? MinimumEventLevel { get; set; }
10+
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
11+
public LogLevel? MinimumLogLevel { get; set; }
1012
public bool? InitializeSdk { get; set; }
1113

1214
public void ApplyTo(SentryLoggingOptions options)
1315
{
1416
base.ApplyTo(options);
1517
options.MinimumBreadcrumbLevel = MinimumBreadcrumbLevel ?? options.MinimumBreadcrumbLevel;
1618
options.MinimumEventLevel = MinimumEventLevel ?? options.MinimumEventLevel;
19+
options.MinimumLogLevel = MinimumLogLevel ?? options.MinimumLogLevel;
1720
options.InitializeSdk = InitializeSdk ?? options.InitializeSdk;
1821
}
1922
}

src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,21 @@ internal static ILoggingBuilder AddSentry<TOptions>(
5151

5252
builder.Services.AddSingleton<IConfigureOptions<TOptions>, SentryLoggingOptionsSetup>();
5353
builder.Services.AddSingleton<ILoggerProvider, SentryLoggerProvider>();
54+
builder.Services.AddSingleton<ILoggerProvider, SentryStructuredLoggerProvider>();
5455
builder.Services.AddSentry<TOptions>();
5556

5657
// All logs should flow to the SentryLogger, regardless of level.
5758
// Filtering of events is handled in SentryLogger, using SentryOptions.MinimumEventLevel
5859
// Filtering of breadcrumbs is handled in SentryLogger, using SentryOptions.MinimumBreadcrumbLevel
5960
builder.AddFilter<SentryLoggerProvider>(_ => true);
6061

62+
// Logs from the SentryLogger should not flow to the SentryStructuredLogger as this may cause recursive invocations.
63+
// Filtering of logs is handled in SentryStructuredLogger, using SentryOptions.MinimumLogLevel
64+
builder.AddFilter<SentryStructuredLoggerProvider>(static (string? categoryName, LogLevel logLevel) =>
65+
{
66+
return categoryName is null || !categoryName.StartsWith("Sentry");
67+
});
68+
6169
return builder;
6270
}
6371
}

src/Sentry.Extensions.Logging/SentryLoggingOptions.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ public class SentryLoggingOptions : SentryOptions
1111
/// <summary>
1212
/// Gets or sets the minimum breadcrumb level.
1313
/// </summary>
14-
/// <remarks>Events with this level or higher will be stored as <see cref="Breadcrumb"/></remarks>
14+
/// <remarks>
15+
/// Events with this level or higher will be stored as <see cref="Breadcrumb"/>.
16+
/// </remarks>
1517
/// <value>
1618
/// The minimum breadcrumb level.
1719
/// </value>
@@ -21,13 +23,26 @@ public class SentryLoggingOptions : SentryOptions
2123
/// Gets or sets the minimum event level.
2224
/// </summary>
2325
/// <remarks>
24-
/// Events with this level or higher will be sent to Sentry
26+
/// Events with this level or higher will be sent to Sentry.
2527
/// </remarks>
2628
/// <value>
2729
/// The minimum event level.
2830
/// </value>
2931
public LogLevel MinimumEventLevel { get; set; } = LogLevel.Error;
3032

33+
/// <summary>
34+
/// Gets or sets the minimum log level.
35+
/// <para>This API is experimental and it may change in the future.</para>
36+
/// </summary>
37+
/// <remarks>
38+
/// Logs with this level or higher will be stored as <see cref="SentryLog"/>.
39+
/// </remarks>
40+
/// <value>
41+
/// The minimum log level.
42+
/// </value>
43+
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
44+
public LogLevel MinimumLogLevel { get; set; } = LogLevel.Trace;
45+
3146
/// <summary>
3247
/// Whether to initialize this SDK through this integration
3348
/// </summary>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace Sentry.Extensions.Logging;
4+
5+
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
6+
internal sealed class SentryStructuredLogger : ILogger
7+
{
8+
private readonly string _categoryName;
9+
private readonly SentryLoggingOptions _options;
10+
private readonly IHub _hub;
11+
12+
internal SentryStructuredLogger(string categoryName, SentryLoggingOptions options, IHub hub)
13+
{
14+
_categoryName = categoryName;
15+
_options = options;
16+
_hub = hub;
17+
}
18+
19+
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
20+
{
21+
return NullDisposable.Instance;
22+
}
23+
24+
public bool IsEnabled(LogLevel logLevel)
25+
{
26+
return _hub.IsEnabled
27+
&& _options.EnableLogs
28+
&& logLevel != LogLevel.None
29+
&& logLevel >= _options.MinimumLogLevel;
30+
}
31+
32+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
33+
{
34+
if (!IsEnabled(logLevel))
35+
{
36+
return;
37+
}
38+
39+
// not quite ideal as this is a boxing allocation from Microsoft.Extensions.Logging.FormattedLogValues
40+
/*
41+
string? template = null;
42+
object[]? parameters = null;
43+
if (state is IReadOnlyList<KeyValuePair<string, object?>> formattedLogValues)
44+
{
45+
foreach (var formattedLogValue in formattedLogValues)
46+
{
47+
if (formattedLogValue.Key == "{OriginalFormat}" && formattedLogValue.Value is string formattedString)
48+
{
49+
template = formattedString;
50+
break;
51+
}
52+
}
53+
}
54+
*/
55+
56+
string message = formatter.Invoke(state, exception);
57+
58+
switch (logLevel)
59+
{
60+
case LogLevel.Trace:
61+
_hub.Logger.LogTrace(message);
62+
break;
63+
case LogLevel.Debug:
64+
_hub.Logger.LogDebug(message);
65+
break;
66+
case LogLevel.Information:
67+
_hub.Logger.LogInfo(message);
68+
break;
69+
case LogLevel.Warning:
70+
_hub.Logger.LogWarning(message);
71+
break;
72+
case LogLevel.Error:
73+
_hub.Logger.LogError(message);
74+
break;
75+
case LogLevel.Critical:
76+
_hub.Logger.LogFatal(message);
77+
break;
78+
case LogLevel.None:
79+
default:
80+
break;
81+
}
82+
}
83+
}
84+
85+
file sealed class NullDisposable : IDisposable
86+
{
87+
public static NullDisposable Instance { get; } = new NullDisposable();
88+
89+
private NullDisposable()
90+
{
91+
}
92+
93+
public void Dispose()
94+
{
95+
}
96+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Options;
3+
4+
namespace Sentry.Extensions.Logging;
5+
6+
/// <summary>
7+
/// Sentry Structured Logger Provider.
8+
/// </summary>
9+
[ProviderAlias("SentryLogs")]
10+
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
11+
internal sealed class SentryStructuredLoggerProvider : ILoggerProvider
12+
{
13+
private readonly IOptions<SentryLoggingOptions> _options;
14+
private readonly IHub _hub;
15+
16+
// TODO: convert this comment into an automated test
17+
// Constructor must be public for Microsoft.Extensions.DependencyInjection
18+
public SentryStructuredLoggerProvider(IOptions<SentryLoggingOptions> options, IHub hub)
19+
{
20+
_options = options;
21+
_hub = hub;
22+
}
23+
24+
public ILogger CreateLogger(string categoryName)
25+
{
26+
return new SentryStructuredLogger(categoryName, _options.Value, _hub);
27+
}
28+
29+
public void Dispose()
30+
{
31+
}
32+
}

test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ namespace Sentry.Extensions.Logging
4141
public bool InitializeSdk { get; set; }
4242
public Microsoft.Extensions.Logging.LogLevel MinimumBreadcrumbLevel { get; set; }
4343
public Microsoft.Extensions.Logging.LogLevel MinimumEventLevel { get; set; }
44+
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
45+
public Microsoft.Extensions.Logging.LogLevel MinimumLogLevel { get; set; }
4446
public void ConfigureScope(System.Action<Sentry.Scope> action) { }
4547
}
4648
public static class SentryLoggingOptionsExtensions

test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ namespace Sentry.Extensions.Logging
4141
public bool InitializeSdk { get; set; }
4242
public Microsoft.Extensions.Logging.LogLevel MinimumBreadcrumbLevel { get; set; }
4343
public Microsoft.Extensions.Logging.LogLevel MinimumEventLevel { get; set; }
44+
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
45+
public Microsoft.Extensions.Logging.LogLevel MinimumLogLevel { get; set; }
4446
public void ConfigureScope(System.Action<Sentry.Scope> action) { }
4547
}
4648
public static class SentryLoggingOptionsExtensions

test/Sentry.Extensions.Logging.Tests/ApiApprovalTests.Run.Net4_8.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ namespace Sentry.Extensions.Logging
4141
public bool InitializeSdk { get; set; }
4242
public Microsoft.Extensions.Logging.LogLevel MinimumBreadcrumbLevel { get; set; }
4343
public Microsoft.Extensions.Logging.LogLevel MinimumEventLevel { get; set; }
44+
public Microsoft.Extensions.Logging.LogLevel MinimumLogLevel { get; set; }
4445
public void ConfigureScope(System.Action<Sentry.Scope> action) { }
4546
}
4647
public static class SentryLoggingOptionsExtensions

test/Sentry.Extensions.Logging.Tests/SentryLoggingOptionsSetupTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public void Configure_BindsConfigurationToOptions()
2525
Distribution = "FakeDistribution",
2626
Environment = "Test",
2727
Dsn = "https://[email protected]:65535/2147483647",
28+
EnableLogs = true,
2829
MaxQueueItems = 8,
2930
MaxCacheItems = 9,
3031
ShutdownTimeout = TimeSpan.FromSeconds(13),
@@ -55,6 +56,7 @@ public void Configure_BindsConfigurationToOptions()
5556

5657
MinimumBreadcrumbLevel = LogLevel.Debug,
5758
MinimumEventLevel = LogLevel.Error,
59+
MinimumLogLevel = LogLevel.None,
5860
InitializeSdk = true
5961
};
6062
var config = new ConfigurationBuilder()
@@ -74,6 +76,7 @@ public void Configure_BindsConfigurationToOptions()
7476
["Distribution"] = expected.Distribution,
7577
["Environment"] = expected.Environment,
7678
["Dsn"] = expected.Dsn,
79+
["EnableLogs"] = expected.EnableLogs.ToString(),
7780
["MaxQueueItems"] = expected.MaxQueueItems.ToString(),
7881
["MaxCacheItems"] = expected.MaxCacheItems.ToString(),
7982
["ShutdownTimeout"] = expected.ShutdownTimeout.ToString(),
@@ -105,6 +108,7 @@ public void Configure_BindsConfigurationToOptions()
105108
["JsonPreserveReferences"] = expected.JsonPreserveReferences.ToString(),
106109
["MinimumBreadcrumbLevel"] = expected.MinimumBreadcrumbLevel.ToString(),
107110
["MinimumEventLevel"] = expected.MinimumEventLevel.ToString(),
111+
["MinimumLogLevel"] = expected.MinimumLogLevel.ToString(),
108112
["InitializeSdk"] = expected.InitializeSdk.ToString(),
109113
})
110114
.Build();
@@ -134,6 +138,7 @@ public void Configure_BindsConfigurationToOptions()
134138
actual.Distribution.Should().Be(expected.Distribution);
135139
actual.Environment.Should().Be(expected.Environment);
136140
actual.Dsn.Should().Be(expected.Dsn);
141+
actual.EnableLogs.Should().Be(expected.EnableLogs);
137142
actual.MaxQueueItems.Should().Be(expected.MaxQueueItems);
138143
actual.MaxCacheItems.Should().Be(expected.MaxCacheItems);
139144
actual.ShutdownTimeout.Should().Be(expected.ShutdownTimeout);
@@ -162,6 +167,7 @@ public void Configure_BindsConfigurationToOptions()
162167

163168
actual.MinimumBreadcrumbLevel.Should().Be(expected.MinimumBreadcrumbLevel);
164169
actual.MinimumEventLevel.Should().Be(expected.MinimumEventLevel);
170+
actual.MinimumLogLevel.Should().Be(expected.MinimumLogLevel);
165171
actual.InitializeSdk.Should().Be(expected.InitializeSdk);
166172
}
167173
}

0 commit comments

Comments
 (0)