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();