diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22c5c80917..4e76e86d14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
## Unreleased
+### Features
+
+- Add experimental support for [Sentry Structured Logging](https://docs.sentry.io/product/explore/logs/) API ([#4158](https://github.com/getsentry/sentry-dotnet/pull/4158), [#4310](https://github.com/getsentry/sentry-dotnet/pull/4310))
+ - Add experimental integrations for `Sentry.Extensions.Logging`, `Sentry.AspNetCore` and `Sentry.Maui` ([#4193](https://github.com/getsentry/sentry-dotnet/pull/4193))
+
### Fixes
- Native AOT: don't load SentryNative on unsupported platforms ([#4347](https://github.com/getsentry/sentry-dotnet/pull/4347))
@@ -20,7 +25,6 @@
### Features
-- Add experimental support for [Sentry Structured Logging](https://docs.sentry.io/product/explore/logs/) via `SentrySdk.Experimental.Logger` ([#4158](https://github.com/getsentry/sentry-dotnet/pull/4158))
- Added StartSpan and GetTransaction methods to the SentrySdk ([#4303](https://github.com/getsentry/sentry-dotnet/pull/4303))
### Fixes
diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Extensions.Logging.SentryStructuredLoggerBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Extensions.Logging.SentryStructuredLoggerBenchmarks-report-github.md
new file mode 100644
index 0000000000..172b688e55
--- /dev/null
+++ b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.Extensions.Logging.SentryStructuredLoggerBenchmarks-report-github.md
@@ -0,0 +1,13 @@
+```
+
+BenchmarkDotNet v0.13.12, macOS 15.5 (24F74) [Darwin 24.5.0]
+Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
+.NET SDK 9.0.301
+ [Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
+ DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
+
+
+```
+| Method | Mean | Error | StdDev | Gen0 | Allocated |
+|------- |---------:|--------:|--------:|-------:|----------:|
+| Log | 288.4 ns | 1.28 ns | 1.20 ns | 0.1163 | 976 B |
diff --git a/benchmarks/Sentry.Benchmarks/Extensions.Logging/SentryStructuredLoggerBenchmarks.cs b/benchmarks/Sentry.Benchmarks/Extensions.Logging/SentryStructuredLoggerBenchmarks.cs
new file mode 100644
index 0000000000..3e18747a65
--- /dev/null
+++ b/benchmarks/Sentry.Benchmarks/Extensions.Logging/SentryStructuredLoggerBenchmarks.cs
@@ -0,0 +1,90 @@
+#nullable enable
+
+using BenchmarkDotNet.Attributes;
+using Microsoft.Extensions.Logging;
+using Sentry.Extensibility;
+using Sentry.Extensions.Logging;
+using Sentry.Internal;
+using Sentry.Testing;
+
+namespace Sentry.Benchmarks.Extensions.Logging;
+
+public class SentryStructuredLoggerBenchmarks
+{
+ private Hub _hub = null!;
+ private Sentry.Extensions.Logging.SentryStructuredLogger _logger = null!;
+ private LogRecord _logRecord = null!;
+ private SentryLog? _lastLog;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ SentryLoggingOptions options = new()
+ {
+ Dsn = DsnSamples.ValidDsn,
+ Experimental =
+ {
+ EnableLogs = true,
+ },
+ ExperimentalLogging =
+ {
+ MinimumLogLevel = LogLevel.Information,
+ }
+ };
+ options.Experimental.SetBeforeSendLog((SentryLog log) =>
+ {
+ _lastLog = log;
+ return null;
+ });
+
+ MockClock clock = new(new DateTimeOffset(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2)));
+ SdkVersion sdk = new()
+ {
+ Name = "SDK Name",
+ Version = "SDK Version",
+ };
+
+ _hub = new Hub(options, DisabledHub.Instance);
+ _logger = new Sentry.Extensions.Logging.SentryStructuredLogger("CategoryName", options, _hub, clock, sdk);
+ _logRecord = new LogRecord(LogLevel.Information, new EventId(2025, "EventName"), new InvalidOperationException("exception-message"), "Number={Number}, Text={Text}", 2018, "message");
+ }
+
+ [Benchmark]
+ public void Log()
+ {
+ _logger.Log(_logRecord.LogLevel, _logRecord.EventId, _logRecord.Exception, _logRecord.Message, _logRecord.Args);
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _hub.Dispose();
+
+ if (_lastLog is null)
+ {
+ throw new InvalidOperationException("Last Log is null");
+ }
+ if (_lastLog.Message != "Number=2018, Text=message")
+ {
+ throw new InvalidOperationException($"Last Log with Message: '{_lastLog.Message}'");
+ }
+ }
+
+ private sealed class LogRecord
+ {
+ public LogRecord(LogLevel logLevel, EventId eventId, Exception? exception, string? message, params object?[] args)
+ {
+ LogLevel = logLevel;
+ EventId = eventId;
+ Exception = exception;
+ Message = message;
+ Args = args;
+ }
+
+ public LogLevel LogLevel { get; }
+ public EventId EventId { get; }
+ public Exception? Exception { get; }
+ public string? Message { get; }
+ public object?[] Args { get; }
+ }
+}
diff --git a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
index bdb8ed918a..48231469f2 100644
--- a/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
+++ b/benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
@@ -14,6 +14,7 @@
+
diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs
index cb8a4da994..221a10293b 100644
--- a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs
+++ b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs
@@ -15,6 +15,9 @@
// Log debug information about the Sentry SDK
options.Debug = true;
#endif
+
+ // This option enables Logs sent to Sentry.
+ options.Experimental.EnableLogs = true;
});
var app = builder.Build();
diff --git a/samples/Sentry.Samples.ME.Logging/Program.cs b/samples/Sentry.Samples.ME.Logging/Program.cs
index 809db165b4..0178235c03 100644
--- a/samples/Sentry.Samples.ME.Logging/Program.cs
+++ b/samples/Sentry.Samples.ME.Logging/Program.cs
@@ -23,7 +23,17 @@
// Optionally configure options: The default values are:
options.MinimumBreadcrumbLevel = LogLevel.Information; // It requires at least this level to store breadcrumb
options.MinimumEventLevel = LogLevel.Error; // This level or above will result in event sent to Sentry
+ options.ExperimentalLogging.MinimumLogLevel = LogLevel.Trace; // This level or above will result in log sent to Sentry
+ // This option enables Logs sent to Sentry.
+ options.Experimental.EnableLogs = true;
+ options.Experimental.SetBeforeSendLog(static log =>
+ {
+ log.SetAttribute("attribute-key", "attribute-value");
+ return log;
+ });
+
+ // TODO: AddLogEntryFilter
// Don't keep as a breadcrumb or send events for messages of level less than Critical with exception of type DivideByZeroException
options.AddLogEntryFilter((_, level, _, exception) => level < LogLevel.Critical && exception is DivideByZeroException);
diff --git a/samples/Sentry.Samples.Maui/MauiProgram.cs b/samples/Sentry.Samples.Maui/MauiProgram.cs
index 625632ecc2..909851b7d4 100644
--- a/samples/Sentry.Samples.Maui/MauiProgram.cs
+++ b/samples/Sentry.Samples.Maui/MauiProgram.cs
@@ -33,6 +33,7 @@ public static MauiApp CreateMauiApp()
options.AttachScreenshot = true;
options.Debug = true;
+ options.Experimental.EnableLogs = true;
options.SampleRate = 1.0F;
// The Sentry MVVM Community Toolkit integration automatically creates traces for async relay commands,
diff --git a/src/Sentry.AspNetCore/SentryAspNetCoreStructuredLoggerProvider.cs b/src/Sentry.AspNetCore/SentryAspNetCoreStructuredLoggerProvider.cs
new file mode 100644
index 0000000000..90cb033375
--- /dev/null
+++ b/src/Sentry.AspNetCore/SentryAspNetCoreStructuredLoggerProvider.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Sentry.Extensions.Logging;
+using Sentry.Infrastructure;
+
+namespace Sentry.AspNetCore;
+
+///
+/// Structured Logger Provider for Sentry.
+///
+[ProviderAlias("SentryLogs")]
+internal sealed class SentryAspNetCoreStructuredLoggerProvider : SentryStructuredLoggerProvider
+{
+ public SentryAspNetCoreStructuredLoggerProvider(IOptions options, IHub hub)
+ : this(options.Value, hub, SystemClock.Clock, CreateSdkVersion())
+ {
+ }
+
+ internal SentryAspNetCoreStructuredLoggerProvider(SentryAspNetCoreOptions options, IHub hub, ISystemClock clock, SdkVersion sdk)
+ : base(options, hub, clock, sdk)
+ {
+ }
+
+ private static SdkVersion CreateSdkVersion()
+ {
+ return new SdkVersion
+ {
+ Name = Constants.SdkName,
+ Version = SentryMiddleware.NameAndVersion.Version,
+ };
+ }
+}
diff --git a/src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs b/src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs
index c00217368b..2b5f74bf4d 100644
--- a/src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs
+++ b/src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs
@@ -93,10 +93,16 @@ public static IWebHostBuilder UseSentry(
_ = logging.Services
.AddSingleton, SentryAspNetCoreOptionsSetup>();
_ = logging.Services.AddSingleton();
+ _ = logging.Services.AddSingleton();
_ = logging.AddFilter(
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
LogLevel.None);
+ _ = logging.AddFilter(static (string? categoryName, LogLevel logLevel) =>
+ {
+ return categoryName is null
+ || (categoryName != "Sentry.ISentryClient" && categoryName != "Sentry.AspNetCore.SentryMiddleware");
+ });
var sentryBuilder = logging.Services.AddSentry();
configureSentry?.Invoke(context, sentryBuilder);
diff --git a/src/Sentry.Extensions.Logging/BindableSentryLoggingOptions.cs b/src/Sentry.Extensions.Logging/BindableSentryLoggingOptions.cs
index 299e61a21c..7843ad9975 100644
--- a/src/Sentry.Extensions.Logging/BindableSentryLoggingOptions.cs
+++ b/src/Sentry.Extensions.Logging/BindableSentryLoggingOptions.cs
@@ -9,11 +9,20 @@ internal class BindableSentryLoggingOptions : BindableSentryOptions
public LogLevel? MinimumEventLevel { get; set; }
public bool? InitializeSdk { get; set; }
+ public BindableSentryLoggingExperimentalOptions ExperimentalLogging { get; set; } = new();
+
+ internal sealed class BindableSentryLoggingExperimentalOptions
+ {
+ public LogLevel? MinimumLogLevel { get; set; }
+ }
+
public void ApplyTo(SentryLoggingOptions options)
{
base.ApplyTo(options);
options.MinimumBreadcrumbLevel = MinimumBreadcrumbLevel ?? options.MinimumBreadcrumbLevel;
options.MinimumEventLevel = MinimumEventLevel ?? options.MinimumEventLevel;
options.InitializeSdk = InitializeSdk ?? options.InitializeSdk;
+
+ options.ExperimentalLogging.MinimumLogLevel = ExperimentalLogging.MinimumLogLevel ?? options.ExperimentalLogging.MinimumLogLevel;
}
}
diff --git a/src/Sentry.Extensions.Logging/LogLevelExtensions.cs b/src/Sentry.Extensions.Logging/LogLevelExtensions.cs
index e3f862de77..9f1d2f85e2 100644
--- a/src/Sentry.Extensions.Logging/LogLevelExtensions.cs
+++ b/src/Sentry.Extensions.Logging/LogLevelExtensions.cs
@@ -45,4 +45,19 @@ public static SentryLevel ToSentryLevel(this LogLevel level)
_ => SentryLevel.Debug
};
}
+
+ public static SentryLogLevel ToSentryLogLevel(this LogLevel logLevel)
+ {
+ return logLevel switch
+ {
+ LogLevel.Trace => SentryLogLevel.Trace,
+ LogLevel.Debug => SentryLogLevel.Debug,
+ LogLevel.Information => SentryLogLevel.Info,
+ LogLevel.Warning => SentryLogLevel.Warning,
+ LogLevel.Error => SentryLogLevel.Error,
+ LogLevel.Critical => SentryLogLevel.Fatal,
+ LogLevel.None => default,
+ _ => default,
+ };
+ }
}
diff --git a/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs b/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs
index 9b79803de0..f2a4957c11 100644
--- a/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs
+++ b/src/Sentry.Extensions.Logging/LoggingBuilderExtensions.cs
@@ -51,6 +51,7 @@ internal static ILoggingBuilder AddSentry(
builder.Services.AddSingleton, SentryLoggingOptionsSetup>();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddSentry();
// All logs should flow to the SentryLogger, regardless of level.
@@ -58,6 +59,14 @@ internal static ILoggingBuilder AddSentry(
// Filtering of breadcrumbs is handled in SentryLogger, using SentryOptions.MinimumBreadcrumbLevel
builder.AddFilter(_ => true);
+ // Logs from the SentryLogger should not flow to the SentryStructuredLogger as this may cause recursive invocations.
+ // Filtering of logs is handled in SentryStructuredLogger, using SentryOptions.MinimumLogLevel
+ builder.AddFilter(static (string? categoryName, LogLevel logLevel) =>
+ {
+ return categoryName is null
+ || categoryName != "Sentry.ISentryClient";
+ });
+
return builder;
}
}
diff --git a/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj b/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj
index e011099d02..4d52cd091c 100644
--- a/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj
+++ b/src/Sentry.Extensions.Logging/Sentry.Extensions.Logging.csproj
@@ -40,6 +40,7 @@
+
diff --git a/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs b/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs
index 52bd4a0260..d181b645bf 100644
--- a/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs
+++ b/src/Sentry.Extensions.Logging/SentryLoggingOptions.cs
@@ -11,7 +11,9 @@ public class SentryLoggingOptions : SentryOptions
///
/// Gets or sets the minimum breadcrumb level.
///
- /// Events with this level or higher will be stored as
+ ///
+ /// Events with this level or higher will be stored as .
+ ///
///
/// The minimum breadcrumb level.
///
@@ -21,7 +23,7 @@ public class SentryLoggingOptions : SentryOptions
/// Gets or sets the minimum event level.
///
///
- /// Events with this level or higher will be sent to Sentry
+ /// Events with this level or higher will be sent to Sentry.
///
///
/// The minimum event level.
@@ -48,4 +50,39 @@ public class SentryLoggingOptions : SentryOptions
/// List of callbacks to be invoked when initializing the SDK
///
internal Action[] ConfigureScopeCallbacks { get; set; } = Array.Empty>();
+
+ ///
+ /// Experimental Sentry Logging features.
+ ///
+ ///
+ /// This and related experimental APIs may change in the future.
+ ///
+ [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
+ public SentryLoggingExperimentalOptions ExperimentalLogging { get; set; } = new();
+
+ ///
+ /// Experimental Sentry Logging options.
+ ///
+ ///
+ /// This and related experimental APIs may change in the future.
+ ///
+ [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
+ public sealed class SentryLoggingExperimentalOptions
+ {
+ internal SentryLoggingExperimentalOptions()
+ {
+ }
+
+ ///
+ /// Gets or sets the minimum log level.
+ /// This API is experimental and it may change in the future.
+ ///
+ ///
+ /// Logs with this level or higher will be stored as .
+ ///
+ ///
+ /// The minimum log level.
+ ///
+ public LogLevel MinimumLogLevel { get; set; } = LogLevel.Trace;
+ }
}
diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs
new file mode 100644
index 0000000000..87a0daae49
--- /dev/null
+++ b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs
@@ -0,0 +1,121 @@
+using Microsoft.Extensions.Logging;
+using Sentry.Extensibility;
+using Sentry.Infrastructure;
+
+namespace Sentry.Extensions.Logging;
+
+internal sealed class SentryStructuredLogger : ILogger
+{
+ private readonly string? _categoryName;
+ private readonly SentryLoggingOptions _options;
+ private readonly IHub _hub;
+ private readonly ISystemClock _clock;
+ private readonly SdkVersion _sdk;
+
+ internal SentryStructuredLogger(string categoryName, SentryLoggingOptions options, IHub hub, ISystemClock clock, SdkVersion sdk)
+ {
+ _categoryName = categoryName;
+ _options = options;
+ _clock = clock;
+ _hub = hub;
+ _sdk = sdk;
+ }
+
+ public IDisposable? BeginScope(TState state) where TState : notnull
+ {
+ return NullDisposable.Instance;
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return _hub.IsEnabled
+ && _options.Experimental.EnableLogs
+ && logLevel != LogLevel.None
+ && logLevel >= _options.ExperimentalLogging.MinimumLogLevel;
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ var timestamp = _clock.GetUtcNow();
+ var traceHeader = _hub.GetTraceHeader() ?? SentryTraceHeader.Empty;
+
+ var level = logLevel.ToSentryLogLevel();
+ Debug.Assert(level != default);
+
+ string message;
+ try
+ {
+ message = formatter.Invoke(state, exception);
+ }
+ catch (FormatException e)
+ {
+ _options.DiagnosticLogger?.LogError(e, "Template string does not match the provided argument. The Log will be dropped.");
+ return;
+ }
+
+ string? template = null;
+ var parameters = ImmutableArray.CreateBuilder>();
+ // see Microsoft.Extensions.Logging.FormattedLogValues
+ if (state is IReadOnlyList> formattedLogValues)
+ {
+ if (formattedLogValues.Count != 0)
+ {
+ parameters.Capacity = formattedLogValues.Count - 1;
+ }
+
+ foreach (var formattedLogValue in formattedLogValues)
+ {
+ if (formattedLogValue.Key == "{OriginalFormat}" && formattedLogValue.Value is string formattedString)
+ {
+ template = formattedString;
+ }
+ else if (formattedLogValue.Value is not null)
+ {
+ parameters.Add(new KeyValuePair(formattedLogValue.Key, formattedLogValue.Value));
+ }
+ }
+ }
+
+ SentryLog log = new(timestamp, traceHeader.TraceId, level, message)
+ {
+ Template = template,
+ Parameters = parameters.DrainToImmutable(),
+ ParentSpanId = traceHeader.SpanId,
+ };
+
+ log.SetDefaultAttributes(_options, _sdk);
+
+ if (_categoryName is not null)
+ {
+ log.SetAttribute("microsoft.extensions.logging.category_name", _categoryName);
+ }
+ if (eventId.Name is not null || eventId.Id != 0)
+ {
+ log.SetAttribute("microsoft.extensions.logging.event.id", eventId.Id);
+ }
+ if (eventId.Name is not null)
+ {
+ log.SetAttribute("microsoft.extensions.logging.event.name", eventId.Name);
+ }
+
+ _hub.Logger.CaptureLog(log);
+ }
+}
+
+file sealed class NullDisposable : IDisposable
+{
+ public static NullDisposable Instance { get; } = new NullDisposable();
+
+ private NullDisposable()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+}
diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLoggerProvider.cs b/src/Sentry.Extensions.Logging/SentryStructuredLoggerProvider.cs
new file mode 100644
index 0000000000..fdfe0a527c
--- /dev/null
+++ b/src/Sentry.Extensions.Logging/SentryStructuredLoggerProvider.cs
@@ -0,0 +1,48 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Sentry.Infrastructure;
+
+namespace Sentry.Extensions.Logging;
+
+///
+/// Sentry Structured Logger Provider.
+///
+[ProviderAlias("SentryLogs")]
+internal class SentryStructuredLoggerProvider : ILoggerProvider
+{
+ private readonly SentryLoggingOptions _options;
+ private readonly IHub _hub;
+ private readonly ISystemClock _clock;
+ private readonly SdkVersion _sdk;
+
+ public SentryStructuredLoggerProvider(IOptions options, IHub hub)
+ : this(options.Value, hub, SystemClock.Clock, CreateSdkVersion())
+ {
+ }
+
+ internal SentryStructuredLoggerProvider(SentryLoggingOptions options, IHub hub, ISystemClock clock, SdkVersion sdk)
+ {
+ _options = options;
+ _hub = hub;
+ _clock = clock;
+ _sdk = sdk;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new SentryStructuredLogger(categoryName, _options, _hub, _clock, _sdk);
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private static SdkVersion CreateSdkVersion()
+ {
+ return new SdkVersion
+ {
+ Name = Constants.SdkName,
+ Version = SentryLoggerProvider.NameAndVersion.Version,
+ };
+ }
+}
diff --git a/src/Sentry.Maui/Internal/SentryMauiStructuredLoggerProvider.cs b/src/Sentry.Maui/Internal/SentryMauiStructuredLoggerProvider.cs
new file mode 100644
index 0000000000..d0525a4b49
--- /dev/null
+++ b/src/Sentry.Maui/Internal/SentryMauiStructuredLoggerProvider.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Sentry.Extensions.Logging;
+using Sentry.Infrastructure;
+
+namespace Sentry.Maui.Internal;
+
+[ProviderAlias("SentryLogs")]
+internal sealed class SentryMauiStructuredLoggerProvider : SentryStructuredLoggerProvider
+{
+ public SentryMauiStructuredLoggerProvider(IOptions options, IHub hub)
+ : this(options.Value, hub, SystemClock.Clock, CreateSdkVersion())
+ {
+ }
+
+ internal SentryMauiStructuredLoggerProvider(SentryMauiOptions options, IHub hub, ISystemClock clock, SdkVersion sdk)
+ : base(options, hub, clock, sdk)
+ {
+ }
+
+ private static SdkVersion CreateSdkVersion()
+ {
+ return new SdkVersion
+ {
+ Name = Constants.SdkName,
+ Version = Constants.SdkVersion,
+ };
+ }
+}
diff --git a/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs b/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
index 7d71288365..674a7c6535 100644
--- a/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
+++ b/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
@@ -52,10 +52,17 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
services.AddLogging();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton, SentryMauiOptionsSetup>();
services.AddSingleton();
+ builder.Logging.AddFilter(static (string? categoryName, LogLevel logLevel) =>
+ {
+ return categoryName is null
+ || categoryName != "Sentry.ISentryClient";
+ });
+
// Resolve the configured options and register any element event binders from these
var options = new SentryMauiOptions();
configureOptions?.Invoke(options);
diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs
index acf6d1de0a..9ca3847e1e 100644
--- a/src/Sentry/BindableSentryOptions.cs
+++ b/src/Sentry/BindableSentryOptions.cs
@@ -53,10 +53,8 @@ internal partial class BindableSentryOptions
public bool? EnableSpotlight { get; set; }
public string? SpotlightUrl { get; set; }
- [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public BindableSentryExperimentalOptions Experimental { get; set; } = new();
- [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
internal sealed class BindableSentryExperimentalOptions
{
public bool? EnableLogs { get; set; }
diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
index 75a8cee778..37b03babbd 100644
--- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
+++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
@@ -40,10 +40,21 @@ private protected override void CaptureLog(SentryLogLevel level, string template
return;
}
+ ImmutableArray> @params = default;
+ if (parameters is { Length: > 0 })
+ {
+ var builder = ImmutableArray.CreateBuilder>(parameters.Length);
+ for (var index = 0; index < parameters.Length; index++)
+ {
+ builder.Add(new KeyValuePair(index.ToString(), parameters[index]));
+ }
+ @params = builder.DrainToImmutable();
+ }
+
SentryLog log = new(timestamp, traceHeader.TraceId, level, message)
{
Template = template,
- Parameters = ImmutableArray.Create(parameters),
+ Parameters = @params,
ParentSpanId = traceHeader.SpanId,
};
@@ -60,7 +71,14 @@ private protected override void CaptureLog(SentryLogLevel level, string template
var scope = _hub.GetScope();
log.SetDefaultAttributes(_options, scope?.Sdk ?? SdkVersion.Instance);
+ CaptureLog(log);
+ }
+
+ ///
+ protected internal override void CaptureLog(SentryLog log)
+ {
var configuredLog = log;
+
if (_options.Experimental.BeforeSendLogInternal is { } beforeSendLog)
{
try
diff --git a/src/Sentry/Internal/DisabledSentryStructuredLogger.cs b/src/Sentry/Internal/DisabledSentryStructuredLogger.cs
index efe0e65ad2..02fb6fc8f1 100644
--- a/src/Sentry/Internal/DisabledSentryStructuredLogger.cs
+++ b/src/Sentry/Internal/DisabledSentryStructuredLogger.cs
@@ -14,6 +14,12 @@ private protected override void CaptureLog(SentryLogLevel level, string template
// disabled
}
+ ///
+ protected internal override void CaptureLog(SentryLog log)
+ {
+ // disabled
+ }
+
///
protected internal override void Flush()
{
diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs
index bfec633662..96e6a08b2b 100644
--- a/src/Sentry/Internal/Hub.cs
+++ b/src/Sentry/Internal/Hub.cs
@@ -847,6 +847,5 @@ public void Dispose()
public SentryId LastEventId => CurrentScope.LastEventId;
- [Experimental(DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger { get; }
}
diff --git a/src/Sentry/Polyfilling/ImmutableCollectionsPolyfill.cs b/src/Sentry/Polyfilling/ImmutableCollectionsPolyfill.cs
new file mode 100644
index 0000000000..68b44d5dcc
--- /dev/null
+++ b/src/Sentry/Polyfilling/ImmutableCollectionsPolyfill.cs
@@ -0,0 +1,20 @@
+// ReSharper disable CheckNamespace
+namespace System.Collections.Immutable;
+
+internal static class ImmutableCollectionsPolyfill
+{
+#if !NET8_0_OR_GREATER
+ internal static ImmutableArray DrainToImmutable(this ImmutableArray.Builder builder)
+ {
+ if (builder.Capacity == builder.Count)
+ {
+ return builder.MoveToImmutable();
+ }
+
+ var result = builder.ToImmutable();
+ builder.Count = 0;
+ builder.Capacity = 0;
+ return result;
+ }
+#endif
+}
diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs
index 77fcbe83f4..45193ea097 100644
--- a/src/Sentry/Protocol/Envelopes/Envelope.cs
+++ b/src/Sentry/Protocol/Envelopes/Envelope.cs
@@ -451,7 +451,6 @@ internal static Envelope FromClientReport(ClientReport clientReport)
internal static Envelope FromAttachment(SentryId eventId, SentryAttachment attachment, IDiagnosticLogger? logger = null) =>
new(eventId, CreateHeader(eventId), [EnvelopeItem.FromAttachment(attachment)]);
- [Experimental(DiagnosticId.ExperimentalFeature)]
internal static Envelope FromLog(StructuredLog log)
{
var header = DefaultHeader;
diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
index 7528a14d63..5409ebc0a6 100644
--- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
+++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
@@ -371,7 +371,6 @@ internal static EnvelopeItem FromClientReport(ClientReport report)
return new EnvelopeItem(header, new JsonSerializable(report));
}
- [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
internal static EnvelopeItem FromLog(StructuredLog log)
{
var header = new Dictionary(3, StringComparer.Ordinal)
diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs
index dab0813c2e..b506b9da6c 100644
--- a/src/Sentry/SentryLog.cs
+++ b/src/Sentry/SentryLog.cs
@@ -67,7 +67,7 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le
/// This API is experimental and it may change in the future.
///
[Experimental(DiagnosticId.ExperimentalFeature)]
- public ImmutableArray