-
-
Notifications
You must be signed in to change notification settings - Fork 223
feat: add Serilog integration
#4462
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 12 commits
0abf66c
7c73bf1
cca7f57
922185b
962e435
9cec5f8
92a3afd
cfe1703
b0ec5a3
bf68d35
998c105
b3374d1
108caed
4f84ca7
ed13b35
972efc4
02defb5
83013af
7f886cc
d5cf509
c29c704
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| using Sentry.Internal.Extensions; | ||
| using Serilog.Parsing; | ||
|
|
||
| namespace Sentry.Serilog; | ||
|
|
||
| internal sealed partial class SentrySink | ||
| { | ||
| private void CaptureStructuredLog(IHub hub, LogEvent logEvent, string formatted, string? template) | ||
| { | ||
| var traceHeader = hub.GetTraceHeader() ?? SentryTraceHeader.Empty; | ||
| 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); | ||
seer-by-sentry[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| hub.Logger.CaptureLog(log); | ||
| } | ||
|
|
||
| private static void GetStructuredLoggingParametersAndAttributes(LogEvent logEvent, out ImmutableArray<KeyValuePair<string, object>> parameters, out List<KeyValuePair<string, object>> attributes) | ||
| { | ||
| var propertyNames = new HashSet<string>(); | ||
| foreach (var token in logEvent.MessageTemplate.Tokens) | ||
| { | ||
| if (token is PropertyToken property) | ||
| { | ||
| propertyNames.Add(property.PropertyName); | ||
| } | ||
| } | ||
|
|
||
| var @params = ImmutableArray.CreateBuilder<KeyValuePair<string, object>>(); | ||
| attributes = new List<KeyValuePair<string, object>>(); | ||
|
|
||
| foreach (var property in logEvent.Properties) | ||
| { | ||
| if (propertyNames.Contains(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) | ||
| { | ||
seer-by-sentry[bot] marked this conversation as resolved.
Show resolved
Hide 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()); | ||
| } | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Inconsistent Structure Handling in LoggingThe |
||
| else if (!property.Value.IsNull()) | ||
| { | ||
| yield return new KeyValuePair<string, object>(property.Key, property.Value); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,8 +13,8 @@ public static class SentrySinkExtensions | |
| /// </summary> | ||
| /// <param name="loggerConfiguration">The logger configuration .<seealso cref="LoggerSinkConfiguration"/></param> | ||
| /// <param name="dsn">The Sentry DSN (required). <seealso cref="SentryOptions.Dsn"/></param> | ||
| /// <param name="minimumEventLevel">Minimum log level to send an event. <seealso cref="SentrySerilogOptions.MinimumEventLevel"/></param> | ||
| /// <param name="minimumBreadcrumbLevel">Minimum log level to record a breadcrumb. <seealso cref="SentrySerilogOptions.MinimumBreadcrumbLevel"/></param> | ||
| /// <param name="minimumEventLevel">Minimum log level to send an event. <seealso cref="SentrySerilogOptions.MinimumEventLevel"/></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="sendDefaultPii">Whether to include default Personal Identifiable information. <seealso cref="SentryOptions.SendDefaultPii"/></param> | ||
|
|
@@ -35,6 +35,7 @@ public static class SentrySinkExtensions | |
| /// <param name="reportAssembliesMode">What mode to use for reporting referenced assemblies in each event sent to sentry. Defaults to <see cref="Sentry.ReportAssembliesMode.Version"/></param> | ||
| /// <param name="deduplicateMode">What modes to use for event automatic de-duplication. <seealso cref="SentryOptions.DeduplicateMode"/></param> | ||
| /// <param name="defaultTags">Default tags to add to all events. <seealso cref="SentryOptions.DefaultTags"/></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> | ||
|
|
@@ -50,7 +51,7 @@ public static class SentrySinkExtensions | |
| /// "dsn": "https://[email protected]", | ||
| /// "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}", | ||
| /// "sendDefaultPii": false, | ||
| /// "isEnvironmentUser": false, | ||
| /// "serverName": "MyServerName", | ||
|
|
@@ -71,7 +72,8 @@ public static class SentrySinkExtensions | |
| /// "defaultTags": { | ||
| /// "key-1", "value-1", | ||
| /// "key-2", "value-2" | ||
| /// } | ||
| /// }, | ||
| /// "experimentalEnableLogs": true | ||
| /// } | ||
| /// } | ||
| /// ] | ||
|
|
@@ -103,7 +105,8 @@ public static LoggerConfiguration Sentry( | |
| SentryLevel? diagnosticLevel = null, | ||
| ReportAssembliesMode? reportAssembliesMode = null, | ||
| DeduplicateMode? deduplicateMode = null, | ||
| Dictionary<string, string>? defaultTags = null) | ||
| Dictionary<string, string>? defaultTags = null, | ||
| bool? experimentalEnableLogs = null) | ||
| { | ||
| return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o, | ||
| dsn, | ||
|
|
@@ -128,7 +131,8 @@ public static LoggerConfiguration Sentry( | |
| diagnosticLevel, | ||
| reportAssembliesMode, | ||
| deduplicateMode, | ||
| defaultTags)); | ||
| defaultTags, | ||
| experimentalEnableLogs)); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -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> | ||
|
|
@@ -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 | ||
| /// } | ||
| /// } | ||
| /// ] | ||
|
|
@@ -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 | ||
jamescrosswell marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
| { | ||
| return loggerConfiguration.Sentry(o => ConfigureSentrySerilogOptions(o, | ||
| null, | ||
| minimumEventLevel, | ||
| minimumBreadcrumbLevel, | ||
| formatProvider, | ||
| textFormatter)); | ||
| textFormatter, | ||
| experimentalEnableLogs: experimentalEnableLogs)); | ||
| } | ||
|
|
||
| internal static void ConfigureSentrySerilogOptions( | ||
|
|
@@ -205,7 +213,8 @@ internal static void ConfigureSentrySerilogOptions( | |
| SentryLevel? diagnosticLevel = null, | ||
| ReportAssembliesMode? reportAssembliesMode = null, | ||
| DeduplicateMode? deduplicateMode = null, | ||
| Dictionary<string, string>? defaultTags = null) | ||
| Dictionary<string, string>? defaultTags = null, | ||
| bool? experimentalEnableLogs = null) | ||
| { | ||
| if (dsn is not null) | ||
| { | ||
|
|
@@ -317,6 +326,11 @@ internal static void ConfigureSentrySerilogOptions( | |
| sentrySerilogOptions.DeduplicateMode = deduplicateMode.Value; | ||
| } | ||
|
|
||
| if (experimentalEnableLogs.HasValue) | ||
| { | ||
| sentrySerilogOptions.Experimental.EnableLogs = experimentalEnableLogs.Value; | ||
| } | ||
|
|
||
| // Serilog-specific items | ||
| sentrySerilogOptions.InitializeSdk = dsn is not null; // Inferred from the Sentry overload that is used | ||
| if (defaultTags?.Count > 0) | ||
|
|
@@ -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); | ||
seer-by-sentry[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
seer-by-sentry[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the
ParentSpanIdrequired in the protocol for StructuredLogging? An empty SpanId probably isn't that useful otherwise eh?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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_idis 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.