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
23 changes: 12 additions & 11 deletions samples/Sentry.Samples.AspNetCore.Serilog/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Serilog;
using Serilog;
using Serilog.Events;

Expand All @@ -14,28 +15,28 @@ public static WebApplication BuildWebApp(string[] args)
c.Enrich.FromLogContext()
.MinimumLevel.Debug()
.WriteTo.Console()
// Add Sentry integration with Serilog
// Configure Serilog to send logs to Sentry. This only configures the sink - Sentry is initialised below.
.WriteTo.Sentry(s =>
{
// Sets the minimum log level required to add a log message as breadcrumb
s.MinimumBreadcrumbLevel = LogEventLevel.Debug;
// Set the minimum level for messages to be sent out as events to Sentry
s.MinimumEventLevel = LogEventLevel.Error;
// When configuring Sentry's Serilog integration in combination with other integrations that
// initialize the Sentry SDK (like ASP.NET Core or MAUI) we need to tell it not to reinitialize
// Sentry... we just want it to set up the Serilog sink
s.InitializeSdk = false;
}));

// Add Sentry integration
// It can be defined via configuration (including `appsettings.json`)
// or coded explicitly, via parameter like:
// .UseSentry("dsn") or .UseSentry(o => o.Dsn = ""; o.Release = "1.0"; ...)
// Add the Sentry integration.
// Most options can be defined via binding configuration (including `appsettings.json` as we do here)
// or coded explicitly, in the options callback below (as we do with the DSN and Serilog log context)
builder.WebHost.UseSentry(o =>
{
#if !SENTRY_DSN_DEFINED_IN_ENV
builder.WebHost.UseSentry(SamplesShared.Dsn);
o.Dsn = SamplesShared.Dsn;
#else
builder.WebHost.UseSentry(EnvironmentVariables.Dsn);
o.Dsn = EnvironmentVariables.Dsn;
#endif
// Apply properties from the Serilog LogContext to Sentry events
o.UseSerilog();
});

// The App:
var webApplication = builder.Build();
Expand Down
31 changes: 19 additions & 12 deletions samples/Sentry.Samples.Serilog/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Serilog;
using Serilog;
using Serilog.Context;
using Serilog.Events;
Expand All @@ -7,29 +8,35 @@ internal static class Program
{
private static void Main()
{
// Initialise Sentry SDK itself
using var _ = SentrySdk.Init(options =>
{
#if !SENTRY_DSN_DEFINED_IN_ENV
// A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable.
// See https://docs.sentry.io/product/sentry-basics/dsn-explainer/
options.Dsn = SamplesShared.Dsn;
#endif

options.AttachStacktrace = true;
// send PII like the username of the user logged in to the device
options.SendDefaultPii = true;
// Apply properties from the Serilog LogContext (like MyTaskId below) to Sentry events
options.UseSerilog();
});

Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug()
.WriteTo.Console()
// Other overloads exist, for example, configure the SDK with only the DSN or no parameters at all.
// Configure Serilog to send logs to Sentry
.WriteTo.Sentry(options =>
{
#if !SENTRY_DSN_DEFINED_IN_ENV
// A DSN is required. You can set here in code, or you can set it in the SENTRY_DSN environment variable.
// See https://docs.sentry.io/product/sentry-basics/dsn-explainer/
options.Dsn = SamplesShared.Dsn;
#endif

// Debug and higher are stored as breadcrumbs (default os Information)
options.MinimumBreadcrumbLevel = LogEventLevel.Debug;
// Error and higher is sent as event (default is Error)
// Error and higher are sent as events (default is Error)
options.MinimumEventLevel = LogEventLevel.Error;
options.AttachStacktrace = true;
// send PII like the username of the user logged in to the device
options.SendDefaultPii = true;
// Optional Serilog text formatter used to format LogEvent to string. If TextFormatter is set, FormatProvider is ignored.
options.TextFormatter = new MessageTemplateTextFormatter("[{MyTaskId}] {Message}");
// Other configuration
})
.CreateLogger();

Expand Down
25 changes: 17 additions & 8 deletions src/Sentry.Serilog/SentryOptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ namespace Sentry.Serilog;
public static class SentryOptionExtensions
{
/// <summary>
/// Ensures Serilog scope properties get applied to Sentry events. If you are not initialising Sentry when
/// configuring the Sentry sink for Serilog then you should call this method in the options callback for whichever
/// Sentry integration you are using to initialise Sentry.
/// Enables the Serilog integration, so that properties from the Serilog <c>LogContext</c> get applied to all Sentry
/// events.
/// </summary>
/// <param name="options"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T ApplySerilogScopeToEvents<T>(this T options) where T : SentryOptions
/// <remarks>
/// Call this in the options callback of whichever method you use to initialise Sentry (for example
/// <c>SentrySdk.Init</c> or <c>UseSentry</c>). The Sentry sink for Serilog does not initialise Sentry, so it cannot
/// do this for you. Calling this more than once has no additional effect.
/// </remarks>
/// <param name="options">The options used to initialise Sentry.</param>
public static void UseSerilog(this SentryOptions options)
{
if (options.HasSerilogScopeEventProcessor())
{
return;
}

options.AddEventProcessor(new SerilogScopeEventProcessor(options));
return options;
}

internal static bool HasSerilogScopeEventProcessor(this SentryOptions options)
=> options.EventProcessors.Exists(processor => processor.Type == typeof(SerilogScopeEventProcessor));
}
14 changes: 6 additions & 8 deletions src/Sentry.Serilog/SentrySerilogOptions.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
namespace Sentry.Serilog;

/// <summary>
/// Sentry Options for Serilog logging
/// Options for the Sentry sink for Serilog.
/// </summary>
/// <inheritdoc />
public class SentrySerilogOptions : SentryOptions
/// <remarks>
/// These options only configure the sink. The Sentry SDK itself is configured and initialised separately, using
/// <c>SentrySdk.Init</c> or another Sentry integration (such as ASP.NET Core or MAUI).
/// </remarks>
public class SentrySerilogOptions
{
/// <summary>
/// Whether to initialize this SDK through this integration
/// </summary>
public bool InitializeSdk { get; set; } = true;

/// <summary>
/// Minimum log level to send an event.
/// </summary>
Expand Down
43 changes: 27 additions & 16 deletions src/Sentry.Serilog/SentrySink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ namespace Sentry.Serilog;
/// <summary>
/// Sentry Sink for Serilog
/// </summary>
/// <inheritdoc cref="IDisposable" />
/// <inheritdoc cref="ILogEventSink" />
internal sealed partial class SentrySink : ILogEventSink, IDisposable
internal sealed partial class SentrySink : ILogEventSink
{
private readonly IDisposable? _sdkDisposable;
private readonly SentrySerilogOptions _options;

internal static readonly SdkVersion NameAndVersion
Expand All @@ -29,27 +27,24 @@ internal static readonly SdkVersion NameAndVersion
private readonly Func<IHub> _hubAccessor;
private readonly ISystemClock _clock;

public SentrySink(
SentrySerilogOptions options,
IDisposable? sdkDisposable)
private volatile bool _checkedUseSerilog;

public SentrySink(SentrySerilogOptions options)
: this(
options,
() => HubAdapter.Instance,
sdkDisposable,
SystemClock.Clock)
{
}

internal SentrySink(
SentrySerilogOptions options,
Func<IHub> hubAccessor,
IDisposable? sdkDisposable,
ISystemClock clock)
{
_options = options;
_hubAccessor = hubAccessor;
_clock = clock;
_sdkDisposable = sdkDisposable;
}

private static AsyncLocal<bool> isReentrant = new();
Expand All @@ -58,7 +53,7 @@ public void Emit(LogEvent logEvent)
{
if (isReentrant.Value)
{
_options.DiagnosticLogger?.LogError($"Reentrant log event detected. Logging when inside the scope of another log event can cause a StackOverflowException. LogEventInfo.Message: {logEvent.MessageTemplate.Text}");
_hubAccessor()?.GetSentryOptions()?.DiagnosticLogger?.LogError($"Reentrant log event detected. Logging when inside the scope of another log event can cause a StackOverflowException. LogEventInfo.Message: {logEvent.MessageTemplate.Text}");
return;
}

Expand Down Expand Up @@ -88,6 +83,12 @@ private void InnerEmit(LogEvent logEvent)
return;
}

var options = hub.GetSentryOptions();
if (options is not null)
{
WarnIfUseSerilogNotCalled(options);
}

var exception = logEvent.Exception;
var template = logEvent.MessageTemplate.Text;
var formatted = FormatLogEvent(logEvent);
Expand Down Expand Up @@ -151,16 +152,28 @@ private void InnerEmit(LogEvent logEvent)
level: logEvent.Level.ToBreadcrumbLevel());
}

// Read the options from the Hub, rather than the Sink's Serilog-Options. In cases where Sentry's Serilog-Sink is
// added without a DSN (i.e., without initializing the SDK) and the SDK is initialized differently (e.g., through
// ASP.NET Core), only the Hub's Sentry-Options have the actual user-defined values configured.
var options = hub.GetSentryOptions();
if (options is not null)
{
CaptureStructuredLog(hub, options, logEvent, formatted, template);
}
}

private void WarnIfUseSerilogNotCalled(SentryOptions options)
{
if (_checkedUseSerilog)
{
return;
}

_checkedUseSerilog = true;
if (!options.HasSerilogScopeEventProcessor())
{
options.LogWarning(
"The Sentry sink for Serilog is in use, but UseSerilog() was not called on the options used to initialise Sentry. " +
"Properties from the Serilog LogContext will not be applied to Sentry events.");
}
}

private string FormatLogEvent(LogEvent logEvent)
{
if (_options.TextFormatter is { } formatter)
Expand Down Expand Up @@ -188,6 +201,4 @@ private string FormatLogEvent(LogEvent logEvent)
}
}
}

public void Dispose() => _sdkDisposable?.Dispose();
}
Loading
Loading