From 6da7bca4fba884e0d34262458bbabd3f5212ec47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 15 Aug 2025 22:03:33 +0200 Subject: [PATCH 1/3] feat(logs): rework Logger overloads --- .../Sentry.Samples.Console.Basic/Program.cs | 8 +- src/Sentry/SentryStructuredLogger.Format.cs | 156 ++++++++++++++++++ src/Sentry/SentryStructuredLogger.cs | 80 +-------- ...piApprovalTests.Run.DotNet8_0.verified.txt | 24 ++- ...piApprovalTests.Run.DotNet9_0.verified.txt | 24 ++- .../ApiApprovalTests.Run.Net4_8.verified.txt | 18 +- .../SentryStructuredLoggerTests.Format.cs | 143 ++++++++++++++++ .../SentryStructuredLoggerTests.cs | 122 ++++---------- 8 files changed, 388 insertions(+), 187 deletions(-) create mode 100644 src/Sentry/SentryStructuredLogger.Format.cs create mode 100644 test/Sentry.Tests/SentryStructuredLoggerTests.Format.cs diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index f1ae39b6ff..6b1815bf93 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -37,7 +37,7 @@ // This option tells Sentry to capture 100% of traces. You still need to start transactions and spans. options.TracesSampleRate = 1.0; - // This option enables Sentry Logs created via SentrySdk.Logger. + // This option enables Sentry Logs created via SentrySdk.Experimental.Logger. options.Experimental.EnableLogs = true; options.Experimental.SetBeforeSendLog(static log => { @@ -94,7 +94,8 @@ async Task SecondFunction() SentrySdk.CaptureException(exception); span.Finish(exception); - SentrySdk.Experimental.Logger.LogError("Error with message: {0}", [exception.Message], static log => log.SetAttribute("method", nameof(SecondFunction))); + SentrySdk.Experimental.Logger.LogError(static log => log.SetAttribute("method", nameof(SecondFunction)), + "Error with message: {0}", exception.Message); } span.Finish(); @@ -108,7 +109,8 @@ async Task ThirdFunction() // Simulate doing some work await Task.Delay(100); - SentrySdk.Experimental.Logger.LogFatal("Crash imminent!", [], static log => log.SetAttribute("suppress", true)); + SentrySdk.Experimental.Logger.LogFatal(static log => log.SetAttribute("suppress", true), + "Crash imminent!"); // This is an example of an unhandled exception. It will be captured automatically. throw new InvalidOperationException("Something happened that crashed the app!"); diff --git a/src/Sentry/SentryStructuredLogger.Format.cs b/src/Sentry/SentryStructuredLogger.Format.cs new file mode 100644 index 0000000000..4575b5e0d9 --- /dev/null +++ b/src/Sentry/SentryStructuredLogger.Format.cs @@ -0,0 +1,156 @@ +using Sentry.Infrastructure; + +namespace Sentry; + +public abstract partial class SentryStructuredLogger +{ + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogTrace(string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Trace, template, parameters, null); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogTrace(Action configureLog, string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Trace, template, parameters, configureLog); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogDebug(string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Debug, template, parameters, null); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogDebug(Action configureLog, string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Debug, template, parameters, configureLog); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogInfo(string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Info, template, parameters, null); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogInfo(Action configureLog, string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Info, template, parameters, configureLog); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogWarning(string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Warning, template, parameters, null); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogWarning(Action configureLog, string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Warning, template, parameters, configureLog); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogError(string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Error, template, parameters, null); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogError(Action configureLog, string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Error, template, parameters, configureLog); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogFatal(string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Fatal, template, parameters, null); + } + + /// + /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. + /// This API is experimental and it may change in the future. + /// + /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. + /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. + /// The arguments to the . See System.String.Format. + [Experimental(DiagnosticId.ExperimentalFeature)] + public void LogFatal(Action configureLog, string template, params object[] parameters) + { + CaptureLog(SentryLogLevel.Fatal, template, parameters, configureLog); + } +} diff --git a/src/Sentry/SentryStructuredLogger.cs b/src/Sentry/SentryStructuredLogger.cs index 19842bbc72..8a0dd9da1b 100644 --- a/src/Sentry/SentryStructuredLogger.cs +++ b/src/Sentry/SentryStructuredLogger.cs @@ -8,7 +8,7 @@ namespace Sentry; /// This API is experimental and it may change in the future. /// [Experimental(DiagnosticId.ExperimentalFeature)] -public abstract class SentryStructuredLogger +public abstract partial class SentryStructuredLogger { internal static SentryStructuredLogger Create(IHub hub, SentryOptions options, ISystemClock clock) => Create(hub, options, clock, 100, TimeSpan.FromSeconds(5)); @@ -45,82 +45,4 @@ private protected SentryStructuredLogger() /// Clears all buffers for this logger and causes any buffered logs to be sent by the underlying . /// protected internal abstract void Flush(); - - /// - /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. - /// This API is experimental and it may change in the future. - /// - /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. - /// The arguments to the . See System.String.Format. - /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. - [Experimental(DiagnosticId.ExperimentalFeature)] - public void LogTrace(string template, object[]? parameters = null, Action? configureLog = null) - { - CaptureLog(SentryLogLevel.Trace, template, parameters, configureLog); - } - - /// - /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. - /// This API is experimental and it may change in the future. - /// - /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. - /// The arguments to the . See System.String.Format. - /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. - [Experimental(DiagnosticId.ExperimentalFeature)] - public void LogDebug(string template, object[]? parameters = null, Action? configureLog = null) - { - CaptureLog(SentryLogLevel.Debug, template, parameters, configureLog); - } - - /// - /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. - /// This API is experimental and it may change in the future. - /// - /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. - /// The arguments to the . See System.String.Format. - /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. - [Experimental(DiagnosticId.ExperimentalFeature)] - public void LogInfo(string template, object[]? parameters = null, Action? configureLog = null) - { - CaptureLog(SentryLogLevel.Info, template, parameters, configureLog); - } - - /// - /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. - /// This API is experimental and it may change in the future. - /// - /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. - /// The arguments to the . See System.String.Format. - /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. - [Experimental(DiagnosticId.ExperimentalFeature)] - public void LogWarning(string template, object[]? parameters = null, Action? configureLog = null) - { - CaptureLog(SentryLogLevel.Warning, template, parameters, configureLog); - } - - /// - /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. - /// This API is experimental and it may change in the future. - /// - /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. - /// The arguments to the . See System.String.Format. - /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. - [Experimental(DiagnosticId.ExperimentalFeature)] - public void LogError(string template, object[]? parameters = null, Action? configureLog = null) - { - CaptureLog(SentryLogLevel.Error, template, parameters, configureLog); - } - - /// - /// Creates and sends a structured log to Sentry, with severity , when enabled and sampled. - /// This API is experimental and it may change in the future. - /// - /// A formattable . When incompatible with the , the log will not be captured. See System.String.Format. - /// The arguments to the . See System.String.Format. - /// A delegate to set attributes on the . When the delegate throws an during invocation, the log will not be captured. - [Experimental(DiagnosticId.ExperimentalFeature)] - public void LogFatal(string template, object[]? parameters = null, Action? configureLog = null) - { - CaptureLog(SentryLogLevel.Fatal, template, parameters, configureLog); - } } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b94a74dc34..07c413e828 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -993,17 +993,29 @@ namespace Sentry protected abstract void CaptureLog(Sentry.SentryLog log); protected abstract void Flush(); [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogDebug(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogDebug(string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogError(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogDebug(System.Action configureLog, string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogFatal(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogError(string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogInfo(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogError(System.Action configureLog, string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogTrace(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogFatal(string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogWarning(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogFatal(System.Action configureLog, string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogInfo(string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogInfo(System.Action configureLog, string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogTrace(string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogTrace(System.Action configureLog, string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogWarning(string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogWarning(System.Action configureLog, string template, params object[] parameters) { } } public sealed class SentryThread : Sentry.ISentryJsonSerializable { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b94a74dc34..07c413e828 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -993,17 +993,29 @@ namespace Sentry protected abstract void CaptureLog(Sentry.SentryLog log); protected abstract void Flush(); [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogDebug(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogDebug(string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogError(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogDebug(System.Action configureLog, string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogFatal(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogError(string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogInfo(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogError(System.Action configureLog, string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogTrace(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogFatal(string template, params object[] parameters) { } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] - public void LogWarning(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogFatal(System.Action configureLog, string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogInfo(string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogInfo(System.Action configureLog, string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogTrace(string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogTrace(System.Action configureLog, string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogWarning(string template, params object[] parameters) { } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public void LogWarning(System.Action configureLog, string template, params object[] parameters) { } } public sealed class SentryThread : Sentry.ISentryJsonSerializable { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index aa006ffe82..2de7c68513 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -952,12 +952,18 @@ namespace Sentry { protected abstract void CaptureLog(Sentry.SentryLog log); protected abstract void Flush(); - public void LogDebug(string template, object[]? parameters = null, System.Action? configureLog = null) { } - public void LogError(string template, object[]? parameters = null, System.Action? configureLog = null) { } - public void LogFatal(string template, object[]? parameters = null, System.Action? configureLog = null) { } - public void LogInfo(string template, object[]? parameters = null, System.Action? configureLog = null) { } - public void LogTrace(string template, object[]? parameters = null, System.Action? configureLog = null) { } - public void LogWarning(string template, object[]? parameters = null, System.Action? configureLog = null) { } + public void LogDebug(string template, params object[] parameters) { } + public void LogDebug(System.Action configureLog, string template, params object[] parameters) { } + public void LogError(string template, params object[] parameters) { } + public void LogError(System.Action configureLog, string template, params object[] parameters) { } + public void LogFatal(string template, params object[] parameters) { } + public void LogFatal(System.Action configureLog, string template, params object[] parameters) { } + public void LogInfo(string template, params object[] parameters) { } + public void LogInfo(System.Action configureLog, string template, params object[] parameters) { } + public void LogTrace(string template, params object[] parameters) { } + public void LogTrace(System.Action configureLog, string template, params object[] parameters) { } + public void LogWarning(string template, params object[] parameters) { } + public void LogWarning(System.Action configureLog, string template, params object[] parameters) { } } public sealed class SentryThread : Sentry.ISentryJsonSerializable { diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.Format.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.Format.cs new file mode 100644 index 0000000000..87894fed75 --- /dev/null +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.Format.cs @@ -0,0 +1,143 @@ +#nullable enable + +namespace Sentry.Tests; + +public partial class SentryStructuredLoggerTests +{ + [Theory] + [InlineData(SentryLogLevel.Trace)] + [InlineData(SentryLogLevel.Debug)] + [InlineData(SentryLogLevel.Info)] + [InlineData(SentryLogLevel.Warning)] + [InlineData(SentryLogLevel.Error)] + [InlineData(SentryLogLevel.Fatal)] + public void Log_Enabled_CapturesEnvelope(SentryLogLevel level) + { + _fixture.Options.Experimental.EnableLogs = true; + var logger = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + logger.Log(level, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); + logger.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelopeWithoutAttributes(envelope, level); + } + + [Theory] + [InlineData(SentryLogLevel.Trace)] + [InlineData(SentryLogLevel.Debug)] + [InlineData(SentryLogLevel.Info)] + [InlineData(SentryLogLevel.Warning)] + [InlineData(SentryLogLevel.Error)] + [InlineData(SentryLogLevel.Fatal)] + public void Log_Disabled_DoesNotCaptureEnvelope(SentryLogLevel level) + { + _fixture.Options.Experimental.EnableLogs.Should().BeFalse(); + var logger = _fixture.GetSut(); + + logger.Log(level, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); + logger.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + } + + [Theory] + [InlineData(SentryLogLevel.Trace)] + [InlineData(SentryLogLevel.Debug)] + [InlineData(SentryLogLevel.Info)] + [InlineData(SentryLogLevel.Warning)] + [InlineData(SentryLogLevel.Error)] + [InlineData(SentryLogLevel.Fatal)] + public void Log_ConfigureLog_Enabled_CapturesEnvelope(SentryLogLevel level) + { + _fixture.Options.Experimental.EnableLogs = true; + var logger = _fixture.GetSut(); + + Envelope envelope = null!; + _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); + + logger.Log(level, ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); + logger.Flush(); + + _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); + _fixture.AssertEnvelope(envelope, level); + } + + [Theory] + [InlineData(SentryLogLevel.Trace)] + [InlineData(SentryLogLevel.Debug)] + [InlineData(SentryLogLevel.Info)] + [InlineData(SentryLogLevel.Warning)] + [InlineData(SentryLogLevel.Error)] + [InlineData(SentryLogLevel.Fatal)] + public void Log_ConfigureLog_Disabled_DoesNotCaptureEnvelope(SentryLogLevel level) + { + _fixture.Options.Experimental.EnableLogs.Should().BeFalse(); + var logger = _fixture.GetSut(); + + logger.Log(level, ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); + logger.Flush(); + + _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); + } +} + +file static class SentryStructuredLoggerExtensions +{ + public static void Log(this SentryStructuredLogger logger, SentryLogLevel level, string template, params object[] parameters) + { + switch (level) + { + case SentryLogLevel.Trace: + logger.LogTrace(template, parameters); + break; + case SentryLogLevel.Debug: + logger.LogDebug(template, parameters); + break; + case SentryLogLevel.Info: + logger.LogInfo(template, parameters); + break; + case SentryLogLevel.Warning: + logger.LogWarning(template, parameters); + break; + case SentryLogLevel.Error: + logger.LogError(template, parameters); + break; + case SentryLogLevel.Fatal: + logger.LogFatal(template, parameters); + break; + default: + throw new ArgumentOutOfRangeException(nameof(level), level, null); + } + } + + public static void Log(this SentryStructuredLogger logger, SentryLogLevel level, Action configureLog, string template, params object[] parameters) + { + switch (level) + { + case SentryLogLevel.Trace: + logger.LogTrace(configureLog, template, parameters); + break; + case SentryLogLevel.Debug: + logger.LogDebug(configureLog, template, parameters); + break; + case SentryLogLevel.Info: + logger.LogInfo(configureLog, template, parameters); + break; + case SentryLogLevel.Warning: + logger.LogWarning(configureLog, template, parameters); + break; + case SentryLogLevel.Error: + logger.LogError(configureLog, template, parameters); + break; + case SentryLogLevel.Fatal: + logger.LogFatal(configureLog, template, parameters); + break; + default: + throw new ArgumentOutOfRangeException(nameof(level), level, null); + } + } +} diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index b64b258e69..3281fa367c 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -5,7 +5,7 @@ namespace Sentry.Tests; /// /// /// -public class SentryStructuredLoggerTests : IDisposable +public partial class SentryStructuredLoggerTests : IDisposable { internal sealed class Fixture { @@ -28,6 +28,11 @@ public Fixture() var traceHeader = new SentryTraceHeader(TraceId, ParentSpanId.Value, null); Hub.GetTraceHeader().Returns(traceHeader); + + ExpectedAttributes = new Dictionary(1) + { + { "attribute-key", "attribute-value" }, + }; } public InMemoryDiagnosticLogger DiagnosticLogger { get; } @@ -39,6 +44,8 @@ public Fixture() public SentryId TraceId { get; private set; } public SpanId? ParentSpanId { get; private set; } + public Dictionary ExpectedAttributes { get; } + public void WithoutTraceHeader() { Hub.GetTraceHeader().Returns((SentryTraceHeader?)null); @@ -85,45 +92,6 @@ public void Create_Disabled_CachedDisabledInstance() instance.Should().BeSameAs(other); } - [Theory] - [InlineData(SentryLogLevel.Trace)] - [InlineData(SentryLogLevel.Debug)] - [InlineData(SentryLogLevel.Info)] - [InlineData(SentryLogLevel.Warning)] - [InlineData(SentryLogLevel.Error)] - [InlineData(SentryLogLevel.Fatal)] - public void Log_Enabled_CapturesEnvelope(SentryLogLevel level) - { - _fixture.Options.Experimental.EnableLogs = true; - var logger = _fixture.GetSut(); - - Envelope envelope = null!; - _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); - - logger.Log(level, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); - logger.Flush(); - - _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); - envelope.AssertEnvelope(_fixture, level); - } - - [Theory] - [InlineData(SentryLogLevel.Trace)] - [InlineData(SentryLogLevel.Debug)] - [InlineData(SentryLogLevel.Info)] - [InlineData(SentryLogLevel.Warning)] - [InlineData(SentryLogLevel.Error)] - [InlineData(SentryLogLevel.Fatal)] - public void Log_Disabled_DoesNotCaptureEnvelope(SentryLogLevel level) - { - _fixture.Options.Experimental.EnableLogs.Should().BeFalse(); - var logger = _fixture.GetSut(); - - logger.Log(level, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); - - _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); - } - [Fact] public void Log_WithoutTraceHeader_CapturesEnvelope() { @@ -134,11 +102,11 @@ public void Log_WithoutTraceHeader_CapturesEnvelope() Envelope envelope = null!; _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); logger.Flush(); _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); - envelope.AssertEnvelope(_fixture, SentryLogLevel.Trace); + _fixture.AssertEnvelope(envelope, SentryLogLevel.Trace); } [Fact] @@ -156,12 +124,12 @@ public void Log_WithBeforeSendLog_InvokesCallback() }); var logger = _fixture.GetSut(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); logger.Flush(); _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); - configuredLog.AssertLog(_fixture, SentryLogLevel.Trace); + _fixture.AssertLog(configuredLog, SentryLogLevel.Trace); } [Fact] @@ -177,7 +145,7 @@ public void Log_WhenBeforeSendLogReturnsNull_DoesNotCaptureEnvelope() }); var logger = _fixture.GetSut(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); @@ -205,7 +173,7 @@ public void Log_InvalidConfigureLog_DoesNotCaptureEnvelope() _fixture.Options.Experimental.EnableLogs = true; var logger = _fixture.GetSut(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], static (SentryLog log) => throw new InvalidOperationException()); + logger.LogTrace(static (SentryLog log) => throw new InvalidOperationException(), "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); @@ -245,13 +213,13 @@ public void Flush_AfterLog_CapturesEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); envelope.Should().BeNull(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); envelope.Should().BeNull(); logger.Flush(); _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); - envelope.AssertEnvelope(_fixture, SentryLogLevel.Trace); + _fixture.AssertEnvelope(envelope, SentryLogLevel.Trace); } [Fact] @@ -262,7 +230,7 @@ public void Dispose_BeforeLog_DoesNotCaptureEnvelope() var defaultLogger = logger.Should().BeOfType().Which; defaultLogger.Dispose(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); @@ -278,15 +246,15 @@ private static void ConfigureLog(SentryLog log) } } -file static class AssertionExtensions +internal static class AssertionExtensions { - public static void AssertEnvelope(this Envelope envelope, SentryStructuredLoggerTests.Fixture fixture, SentryLogLevel level) + public static void AssertEnvelope(this SentryStructuredLoggerTests.Fixture fixture, Envelope envelope, SentryLogLevel level) { envelope.Header.Should().ContainSingle().Which.Key.Should().Be("sdk"); var item = envelope.Items.Should().ContainSingle().Which; var log = item.Payload.Should().BeOfType().Which.Source.Should().BeOfType().Which; - AssertLog(log, fixture, level); + AssertLog(fixture, log, level); Assert.Collection(item.Header, element => Assert.Equal(CreateHeader("type", "log"), element), @@ -294,14 +262,20 @@ public static void AssertEnvelope(this Envelope envelope, SentryStructuredLogger element => Assert.Equal(CreateHeader("content_type", "application/vnd.sentry.items.log+json"), element)); } - public static void AssertLog(this StructuredLog log, SentryStructuredLoggerTests.Fixture fixture, SentryLogLevel level) + public static void AssertEnvelopeWithoutAttributes(this SentryStructuredLoggerTests.Fixture fixture, Envelope envelope, SentryLogLevel level) + { + fixture.ExpectedAttributes.Clear(); + AssertEnvelope(fixture, envelope, level); + } + + public static void AssertLog(this SentryStructuredLoggerTests.Fixture fixture, StructuredLog log, SentryLogLevel level) { var items = log.Items; items.Length.Should().Be(1); - AssertLog(items[0], fixture, level); + AssertLog(fixture, items[0], level); } - public static void AssertLog(this SentryLog log, SentryStructuredLoggerTests.Fixture fixture, SentryLogLevel level) + public static void AssertLog(this SentryStructuredLoggerTests.Fixture fixture, SentryLog log, SentryLogLevel level) { log.Timestamp.Should().Be(fixture.Clock.GetUtcNow()); log.TraceId.Should().Be(fixture.TraceId); @@ -310,8 +284,12 @@ public static void AssertLog(this SentryLog log, SentryStructuredLoggerTests.Fix log.Template.Should().Be("Template string with arguments: {0}, {1}, {2}, {3}"); log.Parameters.Should().BeEquivalentTo(new KeyValuePair[] { new("0", "string"), new("1", true), new("2", 1), new("3", 2.2), }); log.ParentSpanId.Should().Be(fixture.ParentSpanId); - log.TryGetAttribute("attribute-key", out string? value).Should().BeTrue(); - value.Should().Be("attribute-value"); + + foreach (var expectedAttribute in fixture.ExpectedAttributes) + { + log.TryGetAttribute(expectedAttribute.Key, out string? value).Should().BeTrue(); + value.Should().Be(expectedAttribute.Value); + } } private static KeyValuePair CreateHeader(string name, object? value) @@ -319,33 +297,3 @@ public static void AssertLog(this SentryLog log, SentryStructuredLoggerTests.Fix return new KeyValuePair(name, value); } } - -file static class SentryStructuredLoggerExtensions -{ - public static void Log(this SentryStructuredLogger logger, SentryLogLevel level, string template, object[]? parameters, Action? configureLog) - { - switch (level) - { - case SentryLogLevel.Trace: - logger.LogTrace(template, parameters, configureLog); - break; - case SentryLogLevel.Debug: - logger.LogDebug(template, parameters, configureLog); - break; - case SentryLogLevel.Info: - logger.LogInfo(template, parameters, configureLog); - break; - case SentryLogLevel.Warning: - logger.LogWarning(template, parameters, configureLog); - break; - case SentryLogLevel.Error: - logger.LogError(template, parameters, configureLog); - break; - case SentryLogLevel.Fatal: - logger.LogFatal(template, parameters, configureLog); - break; - default: - throw new ArgumentOutOfRangeException(nameof(level), level, null); - } - } -} From 4d0ff815f2e6665cefe32b855868efd3dc3529a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Sat, 16 Aug 2025 14:24:04 +0200 Subject: [PATCH 2/3] docs: update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1c1ba100..7dc0592f14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Experimental _Structured Logs_: + - Redesign SDK Logger APIs to allow usage of `params` ([#4451](https://github.com/getsentry/sentry-dotnet/pull/4451)) - Shorten the `key` names of `Microsoft.Extensions.Logging` attributes ([#4450](https://github.com/getsentry/sentry-dotnet/pull/4450)) ### Fixes From 0d19dea8994bf1cc63cf3800be8d8f7928a8a0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:33:24 +0200 Subject: [PATCH 3/3] style: unwrap Param-Array arguments --- test/Sentry.Tests/SentryStructuredLoggerTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 3281fa367c..b0a2e6e3a5 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -102,7 +102,7 @@ public void Log_WithoutTraceHeader_CapturesEnvelope() Envelope envelope = null!; _fixture.Hub.CaptureEnvelope(Arg.Do(arg => envelope = arg)); - logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); logger.Flush(); _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); @@ -124,7 +124,7 @@ public void Log_WithBeforeSendLog_InvokesCallback() }); var logger = _fixture.GetSut(); - logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); logger.Flush(); _fixture.Hub.Received(1).CaptureEnvelope(Arg.Any()); @@ -145,7 +145,7 @@ public void Log_WhenBeforeSendLogReturnsNull_DoesNotCaptureEnvelope() }); var logger = _fixture.GetSut(); - logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); invocations.Should().Be(1); @@ -157,7 +157,7 @@ public void Log_InvalidFormat_DoesNotCaptureEnvelope() _fixture.Options.Experimental.EnableLogs = true; var logger = _fixture.GetSut(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}, {4}", ["string", true, 1, 2.2]); + logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}, {4}", "string", true, 1, 2.2); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); @@ -173,7 +173,7 @@ public void Log_InvalidConfigureLog_DoesNotCaptureEnvelope() _fixture.Options.Experimental.EnableLogs = true; var logger = _fixture.GetSut(); - logger.LogTrace(static (SentryLog log) => throw new InvalidOperationException(), "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace(static (SentryLog log) => throw new InvalidOperationException(), "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); @@ -190,7 +190,7 @@ public void Log_InvalidBeforeSendLog_DoesNotCaptureEnvelope() _fixture.Options.Experimental.SetBeforeSendLog(static (SentryLog log) => throw new InvalidOperationException()); var logger = _fixture.GetSut(); - logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); @@ -213,7 +213,7 @@ public void Flush_AfterLog_CapturesEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); envelope.Should().BeNull(); - logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); envelope.Should().BeNull(); @@ -230,7 +230,7 @@ public void Dispose_BeforeLog_DoesNotCaptureEnvelope() var defaultLogger = logger.Should().BeOfType().Which; defaultLogger.Dispose(); - logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2]); + logger.LogTrace(ConfigureLog, "Template string with arguments: {0}, {1}, {2}, {3}", "string", true, 1, 2.2); _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue();