Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d24d165
feat: add Batch Processor for Logs
Flash0ver Jun 26, 2025
aad0599
test: Batch Processor for Logs
Flash0ver Jun 26, 2025
76fcc1b
docs: Batch Processor for Logs
Flash0ver Jun 26, 2025
2ad33f6
test: fix unavailable API on TargetFramework=net48
Flash0ver Jun 27, 2025
38e1c04
test: run all Logs tests on full framework
Flash0ver Jun 27, 2025
f7a43b8
ref: remove usage of System.Threading.Lock
Flash0ver Jun 27, 2025
e6b0b74
ref: rename members for clarity
Flash0ver Jun 30, 2025
a84b78f
Merge branch 'feat/logs' into feat/logs-buffering
Flash0ver Jun 30, 2025
53c90ea
ref: delete Timer-Abstraction and change to System.Threading.Timer
Flash0ver Jul 1, 2025
6580632
ref: delete .ctor only called from tests
Flash0ver Jul 1, 2025
6e2ee9b
ref: switch Buffer-Processor to be lock-free but discarding
Flash0ver Jul 2, 2025
0774709
test: fix BatchBuffer and Tests
Flash0ver Jul 3, 2025
d9ae794
fix: flushing buffer on Timeout
Flash0ver Jul 8, 2025
7e1f5ea
feat: add Backpressure-ClientReport
Flash0ver Jul 10, 2025
365a2fb
ref: make BatchProcessor more resilient
Flash0ver Jul 11, 2025
c478391
Format code
getsentry-bot Jul 11, 2025
211beea
Merge branch 'feat/logs' into feat/logs-buffering
Flash0ver Jul 11, 2025
c699c2d
test: fix on .NET Framework
Flash0ver Jul 11, 2025
b21b537
fix: BatchBuffer flushed on Shutdown/Dispose
Flash0ver Jul 14, 2025
e8850db
ref: minimize locking
Flash0ver Jul 22, 2025
57f9ccc
Merge branch 'feat/logs' into feat/logs-buffering
Flash0ver Jul 22, 2025
c63bc53
ref: rename BatchProcessor to StructuredLogBatchProcessor
Flash0ver Jul 22, 2025
f28cc6d
ref: rename BatchBuffer to StructuredLogBatchBuffer
Flash0ver Jul 22, 2025
79ce02e
ref: remove internal options
Flash0ver Jul 23, 2025
0702796
test: ref
Flash0ver Jul 23, 2025
4e5f097
perf: update Benchmark result
Flash0ver Jul 23, 2025
1276725
ref: make SentryStructuredLogger. Flush abstract
Flash0ver Jul 24, 2025
3816fab
ref: guard an invariant of the Flush-Scope
Flash0ver Jul 24, 2025
28b6654
ref: remove unused values
Flash0ver Jul 24, 2025
1eef330
docs: improve comments
Flash0ver Jul 24, 2025
d72ca5c
perf: update Benchmark after signature change
Flash0ver Jul 25, 2025
49fefc1
ref: discard logs gracefully when Hub is (being) disposed
Flash0ver Jul 28, 2025
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
Prev Previous commit
Next Next commit
fix: BatchBuffer flushed on Shutdown/Dispose
  • Loading branch information
Flash0ver committed Jul 14, 2025
commit b21b537a8297867be6d643b10921ac4e4fbd0fb4
48 changes: 48 additions & 0 deletions benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using BenchmarkDotNet.Attributes;
using NSubstitute;
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Testing;

namespace Sentry.Benchmarks;

public class BatchProcessorBenchmarks
{
private BatchProcessor _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()
{
var hub = DisabledHub.Instance;
var batchInterval = Timeout.InfiniteTimeSpan;
var clock = new MockClock();
var clientReportRecorder = Substitute.For<IClientReportRecorder>();
var diagnosticLogger = Substitute.For<IDiagnosticLogger>();
_batchProcessor = new BatchProcessor(hub, BatchCount, batchInterval, clock, 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();
}
}
49 changes: 39 additions & 10 deletions src/Sentry/Internal/BatchBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Sentry.Internal;
/// Not all members are thread-safe.
/// See individual members for notes on thread safety.
/// </remarks>
[DebuggerDisplay("Name = {Name}, Capacity = {Capacity}, IsEmpty = {IsEmpty}, IsFull = {IsFull}, IsAddInProgress = {IsAddInProgress}")]
[DebuggerDisplay("Name = {Name}, Capacity = {Capacity}, IsEmpty = {IsEmpty}, IsFull = {IsFull}, AddCount = {AddCount}")]
internal sealed class BatchBuffer<T> : IDisposable
{
private readonly T[] _array;
Expand Down Expand Up @@ -67,24 +67,50 @@ public BatchBuffer(int capacity, string? name = null)
/// </remarks>
internal bool IsFull => _additions >= _array.Length;

internal FlushScope EnterFlushScope()
/// <summary>
/// Number of <see cref="TryAdd"/> operations in progress.
/// </summary>
/// <remarks>
/// This property is used for debugging only.
/// </remarks>
private int AddCount => _addCounter.Count;

/// <summary>
/// Enters a <see cref="FlushScope"/> used to ensure that only a single flush operation is in progress.
/// </summary>
/// <returns>A <see cref="FlushScope"/> that must be disposed to exit.</returns>
/// <remarks>
/// This method is thread-safe.
/// </remarks>
internal FlushScope TryEnterFlushScope(out bool lockTaken)
{
if (_addLock.TryEnter())
{
lockTaken = true;
return new FlushScope(this);
}

Debug.Fail("The FlushScope should not have been entered again, before the previously entered FlushScope has exited.");
lockTaken = false;
return new FlushScope();
}

/// <summary>
/// Exits the <see cref="FlushScope"/> through <see cref="FlushScope.Dispose"/>.
/// </summary>
/// <remarks>
/// This method is thread-safe.
/// </remarks>
private void ExitFlushScope()
{
_addLock.Exit();
}

internal bool IsAddInProgress => !_addCounter.IsSet;

/// <summary>
/// Blocks the current thread until all <see cref="TryAdd"/> operations have completed.
/// </summary>
/// <remarks>
/// This method is thread-safe.
/// </remarks>
internal void WaitAddCompleted()
{
_addCounter.Wait();
Expand Down Expand Up @@ -177,6 +203,12 @@ private void Clear(int length)
Array.Clear(_array, 0, length);
}

/// <inheritdoc />
public void Dispose()
{
_addCounter.Dispose();
}

private static void ThrowIfLessThanTwo(int capacity, string paramName)
{
if (capacity < 2)
Expand All @@ -190,11 +222,6 @@ private static void ThrowLessThanTwo(int capacity, string paramName)
throw new ArgumentOutOfRangeException(paramName, capacity, "Argument must be at least two.");
}

public void Dispose()
{
_addCounter.Dispose();
}

internal ref struct FlushScope : IDisposable
{
private BatchBuffer<T>? _lockObj;
Expand All @@ -204,6 +231,8 @@ internal FlushScope(BatchBuffer<T> lockObj)
_lockObj = lockObj;
}

internal bool IsEntered => _lockObj is not null;

public void Dispose()
{
var lockObj = _lockObj;
Expand Down
13 changes: 11 additions & 2 deletions src/Sentry/Internal/BatchProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ internal void Enqueue(SentryLog log)
}
}

internal void Flush()
{
DisableTimer();
Flush(_buffer1);
Flush(_buffer2);
}

private bool TryEnqueue(BatchBuffer<SentryLog> buffer, SentryLog log)
{
if (buffer.TryAdd(log, out var count))
Expand All @@ -75,7 +82,7 @@ private bool TryEnqueue(BatchBuffer<SentryLog> buffer, SentryLog log)

if (count == buffer.Capacity) // is buffer full
{
using var flushScope = buffer.EnterFlushScope();
using var flushScope = buffer.TryEnterFlushScope(out var lockTaken);
DisableTimer();

var currentActiveBuffer = _activeBuffer;
Expand Down Expand Up @@ -113,7 +120,9 @@ internal void OnIntervalElapsed(object? state)
{
var currentActiveBuffer = _activeBuffer;

if (!currentActiveBuffer.IsEmpty && _clock.GetUtcNow() > _lastFlush)
using var scope = currentActiveBuffer.TryEnterFlushScope(out var lockTaken);

if (lockTaken && !currentActiveBuffer.IsEmpty && _clock.GetUtcNow() > _lastFlush)
{
_ = TrySwapBuffer(currentActiveBuffer);
Flush(currentActiveBuffer);
Expand Down
8 changes: 8 additions & 0 deletions src/Sentry/Internal/DefaultSentryStructuredLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private static TimeSpan ClampBatchInterval(TimeSpan batchInterval)
: batchInterval;
}

/// <inheritdoc />
private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
{
var timestamp = _clock.GetUtcNow();
Expand Down Expand Up @@ -94,6 +95,13 @@ private protected override void CaptureLog(SentryLogLevel level, string template
}
}

/// <inheritdoc />
protected internal override void Flush()
{
_batchProcessor.Flush();
}

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Internal/DisabledSentryStructuredLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ internal DisabledSentryStructuredLogger()
{
}

/// <inheritdoc />
private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
{
// disabled
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ public void Dispose()
_memoryMonitor?.Dispose();
#endif

Logger.Flush();
Logger.Dispose();

try
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/SentryStructuredLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ private protected SentryStructuredLogger()
{
}

/// <summary>
/// Buffers a <see href="https://develop.sentry.dev/sdk/telemetry/logs">Sentry Log</see> message
/// via the associated <see href="https://develop.sentry.dev/sdk/telemetry/spans/batch-processor">Batch Processor</see>.
/// </summary>
/// <param name="level">The severity level of the log.</param>
/// <param name="template">The parameterized template string.</param>
/// <param name="parameters">The parameters to the <paramref name="template"/> string.</param>
/// <param name="configureLog">A configuration callback. Will be removed in a future version.</param>
private protected abstract void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog);

/// <summary>
Expand Down Expand Up @@ -101,6 +109,14 @@ public void LogFatal(string template, object[]? parameters = null, Action<Sentry
CaptureLog(SentryLogLevel.Fatal, template, parameters, configureLog);
}

/// <summary>
/// When overridden in a derived <see langword="class"/>,
/// clears all buffers for this logger and causes any buffered logs to be sent by the underlying <see cref="ISentryClient"/>.
/// </summary>
protected internal virtual void Flush()
{
}

/// <inheritdoc />
public void Dispose()
{
Expand Down
1 change: 1 addition & 0 deletions test/Sentry.Testing/InMemorySentryStructuredLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public sealed class InMemorySentryStructuredLogger : SentryStructuredLogger
{
public List<LogEntry> Entries { get; } = new();

/// <inheritdoc />
private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
{
Entries.Add(LogEntry.Create(level, template, parameters));
Expand Down
6 changes: 4 additions & 2 deletions test/Sentry.Tests/Internals/BatchProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,17 @@ public void Enqueue_BothTimeoutAndSizeReached_CaptureEnvelopes()
AssertEnvelopes(["one"], ["two", "three"]);
}

[Fact(Skip = "TODO")]
[Fact]
public async Task Enqueue_Concurrency_CaptureEnvelopes()
{
const int batchCount = 3;
const int maxDegreeOfParallelism = 5;
const int logsPerTask = 100;

using var processor = new BatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, _clock, _clientReportRecorder, _diagnosticLogger);
using var sync = new ManualResetEvent(false);

var tasks = new Task[5];
var tasks = new Task[maxDegreeOfParallelism];
for (var i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Factory.StartNew(static state =>
Expand All @@ -137,6 +138,7 @@ public async Task Enqueue_Concurrency_CaptureEnvelopes()

sync.Set();
await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(5));
processor.Flush();
_capturedEnvelopes.CompleteAdding();

var capturedLogs = _capturedEnvelopes
Expand Down
Loading