Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Features

- Add _experimental_ support for [Sentry Structured Logging](https://docs.sentry.io/product/explore/logs/) ([#4308](https://github.com/getsentry/sentry-dotnet/pull/4308))
- Structured-Logger API ([#4158](https://github.com/getsentry/sentry-dotnet/pull/4158))
- Buffering and Batching ([#4310](https://github.com/getsentry/sentry-dotnet/pull/4310))
- Integrations for `Sentry.Extensions.Logging`, `Sentry.AspNetCore` and `Sentry.Maui` ([#4193](https://github.com/getsentry/sentry-dotnet/pull/4193))

### Fixes

- Update `sample_rate` of _Dynamic Sampling Context (DSC)_ when making sampling decisions ([#4374](https://github.com/getsentry/sentry-dotnet/pull/4374))
Expand Down
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
```

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 | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Gen0 | Allocated |
|---------------- |----------- |-------------------- |------------:|---------:|---------:|-------:|----------:|
| **EnqueueAndFlush** | **10** | **100** | **1,774.5 ns** | **7.57 ns** | **6.71 ns** | **0.6104** | **5 KB** |
| **EnqueueAndFlush** | **10** | **200** | **3,468.5 ns** | **11.16 ns** | **10.44 ns** | **1.2207** | **10 KB** |
| **EnqueueAndFlush** | **10** | **1000** | **17,259.7 ns** | **51.92 ns** | **46.02 ns** | **6.1035** | **50 KB** |
| **EnqueueAndFlush** | **100** | **100** | **857.5 ns** | **4.21 ns** | **3.73 ns** | **0.1469** | **1.2 KB** |
| **EnqueueAndFlush** | **100** | **200** | **1,681.4 ns** | **1.74 ns** | **1.63 ns** | **0.2937** | **2.41 KB** |
| **EnqueueAndFlush** | **100** | **1000** | **8,302.2 ns** | **12.00 ns** | **10.64 ns** | **1.4648** | **12.03 KB** |
Original file line number Diff line number Diff line change
@@ -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; }
}
}
1 change: 1 addition & 0 deletions benchmarks/Sentry.Benchmarks/Sentry.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Sentry\Sentry.csproj" />
<ProjectReference Include="..\..\src\Sentry.Extensions.Logging\Sentry.Extensions.Logging.csproj" />
<ProjectReference Include="..\..\src\Sentry.Profiling\Sentry.Profiling.csproj" />
<ProjectReference Include="..\..\test\Sentry.Testing\Sentry.Testing.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using BenchmarkDotNet.Attributes;
using NSubstitute;
using Sentry.Extensibility;
using Sentry.Internal;

namespace Sentry.Benchmarks;

public class StructuredLogBatchProcessorBenchmarks
{
private Hub _hub;
private StructuredLogBatchProcessor _batchProcessor;
private SentryLog _log;

[Params(10, 100)]
public int BatchCount { get; set; }

[Params(100, 200, 1_000)]
public int OperationsPerInvoke { get; set; }

[GlobalSetup]
public void Setup()
{
SentryOptions options = new()
{
Dsn = DsnSamples.ValidDsn,
Experimental =
{
EnableLogs = true,
},
};

var batchInterval = Timeout.InfiniteTimeSpan;

var clientReportRecorder = Substitute.For<IClientReportRecorder>();
clientReportRecorder
.When(static recorder => recorder.RecordDiscardedEvent(Arg.Any<DiscardReason>(), Arg.Any<DataCategory>(), Arg.Any<int>()))
.Throw<UnreachableException>();

var diagnosticLogger = Substitute.For<IDiagnosticLogger>();
diagnosticLogger
.When(static logger => logger.IsEnabled(Arg.Any<SentryLevel>()))
.Throw<UnreachableException>();
diagnosticLogger
.When(static logger => logger.Log(Arg.Any<SentryLevel>(), Arg.Any<string>(), Arg.Any<Exception>(), Arg.Any<object[]>()))
.Throw<UnreachableException>();

_hub = new Hub(options, DisabledHub.Instance);
_batchProcessor = new StructuredLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, diagnosticLogger);
_log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message");
}

[Benchmark]
public void EnqueueAndFlush()
{
for (var i = 0; i < OperationsPerInvoke; i++)
{
_batchProcessor.Enqueue(_log);
}
_batchProcessor.Flush();
}

[GlobalCleanup]
public void Cleanup()
{
_batchProcessor.Dispose();
_hub.Dispose();
}
}
3 changes: 3 additions & 0 deletions samples/Sentry.Samples.AspNetCore.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
20 changes: 20 additions & 0 deletions samples/Sentry.Samples.Console.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* - Error Monitoring (both handled and unhandled exceptions)
* - Performance Tracing (Transactions / Spans)
* - Release Health (Sessions)
* - Logs
* - MSBuild integration for Source Context (see the csproj)
*
* For more advanced features of the SDK, see Sentry.Samples.Console.Customized.
Expand Down Expand Up @@ -35,6 +36,20 @@

// 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.
options.Experimental.EnableLogs = true;
options.Experimental.SetBeforeSendLog(static log =>
{
// A demonstration of how you can drop logs based on some attribute they have
if (log.TryGetAttribute("suppress", out var attribute) && attribute is true)
{
return null;
}

// Drop logs with level Info
return log.Level is SentryLogLevel.Info ? null : log;
});
});

// This starts a new transaction and attaches it to the scope.
Expand All @@ -58,6 +73,7 @@ async Task FirstFunction()
var httpClient = new HttpClient(messageHandler, true);
var html = await httpClient.GetStringAsync("https://example.com/");
WriteLine(html);
SentrySdk.Experimental.Logger.LogInfo("HTTP Request completed.");
}

async Task SecondFunction()
Expand All @@ -77,6 +93,8 @@ async Task SecondFunction()
// This is an example of capturing a handled exception.
SentrySdk.CaptureException(exception);
span.Finish(exception);

SentrySdk.Experimental.Logger.LogError("Error with message: {0}", [exception.Message], static log => log.SetAttribute("method", nameof(SecondFunction)));
}

span.Finish();
Expand All @@ -90,6 +108,8 @@ async Task ThirdFunction()
// Simulate doing some work
await Task.Delay(100);

SentrySdk.Experimental.Logger.LogFatal("Crash imminent!", [], static log => log.SetAttribute("suppress", true));

// This is an example of an unhandled exception. It will be captured automatically.
throw new InvalidOperationException("Something happened that crashed the app!");
}
Expand Down
10 changes: 10 additions & 0 deletions samples/Sentry.Samples.ME.Logging/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions samples/Sentry.Samples.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Sentry.Extensions.Logging;
using Sentry.Infrastructure;

namespace Sentry.AspNetCore;

/// <summary>
/// Structured Logger Provider for Sentry.
/// </summary>
[ProviderAlias("SentryLogs")]
internal sealed class SentryAspNetCoreStructuredLoggerProvider : SentryStructuredLoggerProvider
{
public SentryAspNetCoreStructuredLoggerProvider(IOptions<SentryAspNetCoreOptions> 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,
};
}
}
6 changes: 6 additions & 0 deletions src/Sentry.AspNetCore/SentryWebHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,16 @@ public static IWebHostBuilder UseSentry(
_ = logging.Services
.AddSingleton<IConfigureOptions<SentryAspNetCoreOptions>, SentryAspNetCoreOptionsSetup>();
_ = logging.Services.AddSingleton<ILoggerProvider, SentryAspNetCoreLoggerProvider>();
_ = logging.Services.AddSingleton<ILoggerProvider, SentryAspNetCoreStructuredLoggerProvider>();

_ = logging.AddFilter<SentryAspNetCoreLoggerProvider>(
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware",
LogLevel.None);
_ = logging.AddFilter<SentryAspNetCoreStructuredLoggerProvider>(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
15 changes: 15 additions & 0 deletions src/Sentry.Extensions.Logging/LogLevelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Loading
Loading