Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0abf66c
feat(logs): add Serilog integration
Flash0ver Aug 20, 2025
7c73bf1
test: and fix
Flash0ver Aug 21, 2025
cca7f57
Format code
getsentry-bot Aug 21, 2025
922185b
docs: update CHANGELOG.md
Flash0ver Aug 21, 2025
962e435
test: flush structured logger
Flash0ver Aug 21, 2025
9cec5f8
perf: change lookup from List to HashSet
Flash0ver Aug 22, 2025
92a3afd
Merge branch 'main' into feat/logs-serilog
jamescrosswell Aug 25, 2025
cfe1703
ref(logs): reorder parameters
Flash0ver Aug 27, 2025
b0ec5a3
ref(logs): inline method
Flash0ver Aug 27, 2025
bf68d35
test: update API approvals
Flash0ver Aug 27, 2025
998c105
test(logs): flush client as well
Flash0ver Aug 27, 2025
b3374d1
Merge branch 'main' into feat/logs-serilog
jamescrosswell Sep 5, 2025
108caed
Merge branch 'main' into feat/logs-serilog
Flash0ver Sep 10, 2025
4f84ca7
ref: remove option from overload not initializing the SDK
Flash0ver Sep 10, 2025
ed13b35
fix: Event and Breadcrumbs disabled but Logs enabled
Flash0ver Sep 10, 2025
972efc4
fix: TraceId and ParentSpanId
Flash0ver Sep 10, 2025
02defb5
fix: use Hub-Options over Sink-Options to allow SDK-Init via Serilog …
Flash0ver Sep 10, 2025
83013af
Merge branch 'main' into feat/logs-serilog
Flash0ver Sep 11, 2025
7f886cc
docs: update CHANGELOG after release
Flash0ver Sep 11, 2025
d5cf509
Merge branch 'main' into feat/logs-serilog
Flash0ver Sep 16, 2025
c29c704
docs: update CHANGELOG after merge
Flash0ver Sep 16, 2025
1a4b3c7
Merge branch 'main' into feat/logs-serilog
Flash0ver Sep 24, 2025
5136730
docs: add comment about options
Flash0ver Sep 24, 2025
91f5013
ref: remove redundant null-check
Flash0ver Sep 24, 2025
cdf0318
ref: remove another redundant null-check
Flash0ver Sep 24, 2025
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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Experimental _Structured Logs_:
- Add integration for `Serilog` ([#4462](https://github.com/getsentry/sentry-dotnet/pull/4462))
- Shorten the `key` names of `Microsoft.Extensions.Logging` attributes ([#4450](https://github.com/getsentry/sentry-dotnet/pull/4450))

### Fixes
Expand All @@ -17,8 +18,8 @@
### Dependencies

- Reapply "Bump Cocoa SDK from v8.39.0 to v8.46.0 (#4103)" ([#4442](https://github.com/getsentry/sentry-dotnet/pull/4442))
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8460)
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.39.0...8.46.0)
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8460)
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.39.0...8.46.0)
- Bump Native SDK from v0.9.1 to v0.10.0 ([#4436](https://github.com/getsentry/sentry-dotnet/pull/4436))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0100)
- [diff](https://github.com/getsentry/sentry-native/compare/0.9.1...0.10.0)
Expand Down
2 changes: 2 additions & 0 deletions samples/Sentry.Samples.Serilog/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ private static void Main()
// Error and higher is sent as event (default is Error)
options.MinimumEventLevel = LogEventLevel.Error;
options.AttachStacktrace = true;
// send structured logs to Sentry
options.Experimental.EnableLogs = 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.
Expand Down
14 changes: 14 additions & 0 deletions src/Sentry.Serilog/LogLevelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,18 @@ public static BreadcrumbLevel ToBreadcrumbLevel(this LogEventLevel level)
_ => (BreadcrumbLevel)level
};
}

public static SentryLogLevel ToSentryLogLevel(this LogEventLevel level)
{
return level switch
{
LogEventLevel.Verbose => SentryLogLevel.Trace,
LogEventLevel.Debug => SentryLogLevel.Debug,
LogEventLevel.Information => SentryLogLevel.Info,
LogEventLevel.Warning => SentryLogLevel.Warning,
LogEventLevel.Error => SentryLogLevel.Error,
LogEventLevel.Fatal => SentryLogLevel.Fatal,
_ => (SentryLogLevel)level,
};
}
}
115 changes: 115 additions & 0 deletions src/Sentry.Serilog/SentrySink.Structured.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Sentry.Internal.Extensions;
using Serilog.Parsing;

namespace Sentry.Serilog;

internal sealed partial class SentrySink
{
private static readonly SdkVersion Sdk = CreateSdkVersion();

private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template)
{
var traceHeader = hub.GetTraceHeader() ?? SentryTraceHeader.Empty;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is the ParentSpanId required in the protocol for StructuredLogging? An empty SpanId probably isn't that useful otherwise eh?

Copy link
Copy Markdown
Member Author

@Flash0ver Flash0ver Aug 25, 2025

Choose a reason for hiding this comment

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

No ... it's an optional "Default Attribute" ... the trace_id is required.

Oops ... I made this mistake in the Sentry-SDK and Microsoft.Extensions.Logging integrations, too.

I'll create a follow-up PR to fix this issue in all integrations.

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.

follow-up for both the SDK-Logger and the Microsoft.Extensions.Logging integration: #4565

GetStructuredLoggingParametersAndAttributes(logEvent, out var parameters, out var attributes);

SentryLog log = new(logEvent.Timestamp, traceHeader.TraceId, logEvent.Level.ToSentryLogLevel(), formatted)
{
Template = template,
Parameters = parameters,
ParentSpanId = traceHeader.SpanId,
};

log.SetDefaultAttributes(_options, Sdk);

foreach (var attribute in attributes)
{
log.SetAttribute(attribute.Key, attribute.Value);
Comment thread
seer-by-sentry[bot] marked this conversation as resolved.
Outdated
}

hub.Logger.CaptureLog(log);
}

private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray<KeyValuePair<string, object>> parameters, out List<KeyValuePair<string, object>> attributes)
{
var propertyTokens = new List<PropertyToken>();
foreach (var token in logEvent.MessageTemplate.Tokens)
{
if (token is PropertyToken property)
{
propertyTokens.Add(property);
}
}

Comment thread
seer-by-sentry[bot] marked this conversation as resolved.
var @params = ImmutableArray.CreateBuilder<KeyValuePair<string, object>>();
attributes = new List<KeyValuePair<string, object>>();

foreach (var property in logEvent.Properties)
{
if (propertyTokens.Exists(prop => prop.PropertyName == property.Key))
{
foreach (var parameter in GetLogEventProperties(property))
{
@params.Add(parameter);
}
}
else
{
foreach (var attribute in GetLogEventProperties(property))
{
attributes.Add(new KeyValuePair<string, object>($"property.{attribute.Key}", attribute.Value));
}
}
}

parameters = @params.DrainToImmutable();
return;

static IEnumerable<KeyValuePair<string, object>> GetLogEventProperties(KeyValuePair<string, LogEventPropertyValue> property)
{
if (property.Value is ScalarValue scalarValue)
{
if (scalarValue.Value is not null)
{
yield return new KeyValuePair<string, object>(property.Key, scalarValue.Value);
}
}
else if (property.Value is SequenceValue sequenceValue)
{
if (sequenceValue.Elements.Count != 0)
{
yield return new KeyValuePair<string, object>(property.Key, sequenceValue.ToString());
}
}
else if (property.Value is DictionaryValue dictionaryValue)
{
if (dictionaryValue.Elements.Count != 0)
{
yield return new KeyValuePair<string, object>(property.Key, dictionaryValue.ToString());
}
}
else if (property.Value is StructureValue structureValue)
{
Comment thread
seer-by-sentry[bot] marked this conversation as resolved.
foreach (var prop in structureValue.Properties)
{
if (LogEventProperty.IsValidName(prop.Name))
{
yield return new KeyValuePair<string, object>($"{property.Key}.{prop.Name}", prop.Value.ToString());
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Inconsistent Structure Handling in Logging

The GetLogEventProperties method handles StructureValue inconsistently with other complex types like SequenceValue and DictionaryValue. It flattens structure properties into multiple key-value pairs using dot notation, rather than capturing the entire structure as a single string-represented value. This causes incorrect structured logging and test failures.

Fix in Cursor Fix in Web

else if (!property.Value.IsNull())
{
yield return new KeyValuePair<string, object>(property.Key, property.Value);
}
}
}

private static SdkVersion CreateSdkVersion()
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
{
return new SdkVersion
{
Name = SdkName,
Version = NameAndVersion.Version,
};
}
}
43 changes: 23 additions & 20 deletions src/Sentry.Serilog/SentrySink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Sentry.Serilog;
/// </summary>
/// <inheritdoc cref="IDisposable" />
/// <inheritdoc cref="ILogEventSink" />
internal sealed class SentrySink : ILogEventSink, IDisposable
internal sealed partial class SentrySink : ILogEventSink, IDisposable
{
private readonly IDisposable? _sdkDisposable;
private readonly SentrySerilogOptions _options;
Expand Down Expand Up @@ -122,30 +122,33 @@ private void InnerEmit(LogEvent logEvent)
}
}

if (logEvent.Level < _options.MinimumBreadcrumbLevel)
if (logEvent.Level >= _options.MinimumBreadcrumbLevel)
{
return;
Dictionary<string, string>? data = null;
if (exception != null && !string.IsNullOrWhiteSpace(formatted))
{
// Exception.Message won't be used as Breadcrumb message
// Avoid losing it by adding as data:
data = new Dictionary<string, string>
{
{ "exception_message", exception.Message }
};
}

hub.AddBreadcrumb(
_clock,
string.IsNullOrWhiteSpace(formatted)
? exception?.Message ?? ""
: formatted,
context,
data: data,
level: logEvent.Level.ToBreadcrumbLevel());
}

Dictionary<string, string>? data = null;
if (exception != null && !string.IsNullOrWhiteSpace(formatted))
if (_options.Experimental.EnableLogs)
{
// Exception.Message won't be used as Breadcrumb message
// Avoid losing it by adding as data:
data = new Dictionary<string, string>
{
{"exception_message", exception.Message}
};
CaptureStructuredLog(hub, logEvent, formatted, template);
}

hub.AddBreadcrumb(
_clock,
string.IsNullOrWhiteSpace(formatted)
? exception?.Message ?? ""
: formatted,
context,
data: data,
level: logEvent.Level.ToBreadcrumbLevel());
}
Comment thread
seer-by-sentry[bot] marked this conversation as resolved.

private static bool IsSentryContext(string context) =>
Expand Down
33 changes: 27 additions & 6 deletions src/Sentry.Serilog/SentrySinkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static class SentrySinkExtensions
/// <param name="minimumBreadcrumbLevel">Minimum log level to record a breadcrumb. <seealso cref="SentrySerilogOptions.MinimumBreadcrumbLevel"/></param>
/// <param name="formatProvider">The Serilog format provider. <seealso cref="IFormatProvider"/></param>
/// <param name="textFormatter">The Serilog text formatter. <seealso cref="ITextFormatter"/></param>
/// <param name="experimentalEnableLogs">Whether to send structured logs. <seealso cref="SentryOptions.SentryExperimentalOptions.EnableLogs"/></param>
/// <param name="sendDefaultPii">Whether to include default Personal Identifiable information. <seealso cref="SentryOptions.SendDefaultPii"/></param>
/// <param name="isEnvironmentUser">Whether to report the <see cref="System.Environment.UserName"/> as the User affected in the event. <seealso cref="SentryOptions.IsEnvironmentUser"/></param>
/// <param name="serverName">Gets or sets the name of the server running the application. <seealso cref="SentryOptions.ServerName"/></param>
Expand Down Expand Up @@ -50,7 +51,8 @@ public static class SentrySinkExtensions
/// "dsn": "https://MY-DSN@sentry.io",
/// "minimumBreadcrumbLevel": "Verbose",
/// "minimumEventLevel": "Error",
/// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"///
/// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}",
/// "experimentalEnableLogs": true,
/// "sendDefaultPii": false,
/// "isEnvironmentUser": false,
/// "serverName": "MyServerName",
Expand Down Expand Up @@ -86,6 +88,7 @@ public static LoggerConfiguration Sentry(
LogEventLevel? minimumEventLevel = null,
IFormatProvider? formatProvider = null,
ITextFormatter? textFormatter = null,
bool? experimentalEnableLogs = null,
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
bool? sendDefaultPii = null,
bool? isEnvironmentUser = null,
string? serverName = null,
Expand All @@ -111,6 +114,7 @@ public static LoggerConfiguration Sentry(
minimumBreadcrumbLevel,
formatProvider,
textFormatter,
experimentalEnableLogs,
sendDefaultPii,
isEnvironmentUser,
serverName,
Expand Down Expand Up @@ -143,6 +147,7 @@ public static LoggerConfiguration Sentry(
/// <param name="minimumBreadcrumbLevel">Minimum log level to record a breadcrumb. <seealso cref="SentrySerilogOptions.MinimumBreadcrumbLevel"/></param>
/// <param name="formatProvider">The Serilog format provider. <seealso cref="IFormatProvider"/></param>
/// <param name="textFormatter">The Serilog text formatter. <seealso cref="ITextFormatter"/></param>
/// <param name="experimentalEnableLogs">Whether to send structured logs. <seealso cref="SentryOptions.SentryExperimentalOptions.EnableLogs"/></param>
/// <returns><see cref="LoggerConfiguration"/></returns>
/// <example>This sample shows how each item may be set from within a configuration file:
/// <code>
Expand All @@ -157,7 +162,8 @@ public static LoggerConfiguration Sentry(
/// "Args": {
/// "minimumEventLevel": "Error",
/// "minimumBreadcrumbLevel": "Verbose",
/// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}"///
/// "outputTemplate": "{Timestamp:o} [{Level:u3}] ({Application}/{MachineName}/{ThreadId}) {Message}{NewLine}{Exception}",
/// "experimentalEnableLogs": true
/// }
/// }
/// ]
Expand All @@ -170,15 +176,17 @@ public static LoggerConfiguration Sentry(
LogEventLevel? minimumEventLevel = null,
LogEventLevel? minimumBreadcrumbLevel = null,
IFormatProvider? formatProvider = null,
ITextFormatter? textFormatter = null
ITextFormatter? textFormatter = null,
bool? experimentalEnableLogs = null
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
)
{
return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o,
null,
minimumEventLevel,
minimumBreadcrumbLevel,
formatProvider,
textFormatter));
textFormatter,
experimentalEnableLogs));
}

internal static void ConfigureSentrySerilogOptions(
Expand All @@ -188,6 +196,7 @@ internal static void ConfigureSentrySerilogOptions(
LogEventLevel? minimumBreadcrumbLevel = null,
IFormatProvider? formatProvider = null,
ITextFormatter? textFormatter = null,
bool? experimentalEnableLogs = null,
bool? sendDefaultPii = null,
bool? isEnvironmentUser = null,
string? serverName = null,
Expand Down Expand Up @@ -232,6 +241,11 @@ internal static void ConfigureSentrySerilogOptions(
sentrySerilogOptions.TextFormatter = textFormatter;
}

if (experimentalEnableLogs.HasValue)
{
sentrySerilogOptions.Experimental.EnableLogs = experimentalEnableLogs.Value;
}

if (sendDefaultPii.HasValue)
{
sentrySerilogOptions.SendDefaultPii = sendDefaultPii.Value;
Expand Down Expand Up @@ -354,7 +368,14 @@ public static LoggerConfiguration Sentry(
sdkDisposable = SentrySdk.Init(options);
}

var minimumOverall = (LogEventLevel)Math.Min((int)options.MinimumBreadcrumbLevel, (int)options.MinimumEventLevel);
return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), minimumOverall);
if (options.Experimental.EnableLogs)
{
return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable));
}
else
{
var minimumOverall = (LogEventLevel)Math.Min((int)options.MinimumBreadcrumbLevel, (int)options.MinimumEventLevel);
return loggerConfiguration.Sink(new SentrySink(options, sdkDisposable), minimumOverall);
Comment thread
seer-by-sentry[bot] marked this conversation as resolved.
Outdated
}
Comment thread
seer-by-sentry[bot] marked this conversation as resolved.
Outdated
}
}
1 change: 1 addition & 0 deletions src/Sentry/SentryLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Sentry;
/// <para>This API is experimental and it may change in the future.</para>
/// </summary>
[Experimental(DiagnosticId.ExperimentalFeature)]
[DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")]
public sealed class SentryLog
{
private readonly Dictionary<string, SentryAttribute> _attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ namespace Serilog
public static class SentrySinkExtensions
{
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action<Sentry.Serilog.SentrySerilogOptions> configureOptions) { }
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { }
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { }
public static Serilog.LoggerConfiguration Sentry(
this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
string dsn,
Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default,
Serilog.Events.LogEventLevel? minimumEventLevel = default,
System.IFormatProvider? formatProvider = null,
Serilog.Formatting.ITextFormatter? textFormatter = null,
bool? experimentalEnableLogs = default,
bool? sendDefaultPii = default,
bool? isEnvironmentUser = default,
string? serverName = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ namespace Serilog
public static class SentrySinkExtensions
{
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, System.Action<Sentry.Serilog.SentrySerilogOptions> configureOptions) { }
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null) { }
public static Serilog.LoggerConfiguration Sentry(this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration, Serilog.Events.LogEventLevel? minimumEventLevel = default, Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default, System.IFormatProvider? formatProvider = null, Serilog.Formatting.ITextFormatter? textFormatter = null, bool? experimentalEnableLogs = default) { }
public static Serilog.LoggerConfiguration Sentry(
this Serilog.Configuration.LoggerSinkConfiguration loggerConfiguration,
string dsn,
Serilog.Events.LogEventLevel? minimumBreadcrumbLevel = default,
Serilog.Events.LogEventLevel? minimumEventLevel = default,
System.IFormatProvider? formatProvider = null,
Serilog.Formatting.ITextFormatter? textFormatter = null,
bool? experimentalEnableLogs = default,
bool? sendDefaultPii = default,
bool? isEnvironmentUser = default,
string? serverName = null,
Expand Down
Loading
Loading