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
test: Batch Processor for Logs
  • Loading branch information
Flash0ver committed Jun 26, 2025
commit aad05997036ff11c801b07b9f0e3a74d85f808cd
22 changes: 18 additions & 4 deletions src/Sentry/Internal/BatchBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ internal sealed class BatchBuffer<T>

public BatchBuffer(int capacity)
{
ThrowIfNegativeOrZero(capacity, nameof(capacity));

_array = new T[capacity];
_count = 0;
}
Expand All @@ -18,11 +20,10 @@ public BatchBuffer(int capacity)

internal bool TryAdd(T item)
{
var count = Interlocked.Increment(ref _count);

if (count <= _array.Length)
if (_count < _array.Length)
{
_array[count - 1] = item;
_array[_count] = item;
_count++;
return true;
}

Expand Down Expand Up @@ -59,4 +60,17 @@ internal T[] ToArrayAndClear()
Clear();
return array;
}

private static void ThrowIfNegativeOrZero(int capacity, string paramName)
{
if (capacity <= 0)
{
ThrowNegativeOrZero(capacity, paramName);
}
}

private static void ThrowNegativeOrZero(int capacity, string paramName)
{
throw new ArgumentOutOfRangeException(paramName, capacity, "Argument must neither be negative nor zero.");
}
}
16 changes: 9 additions & 7 deletions src/Sentry/Internal/BatchProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Timers;
using Sentry.Protocol;
using Sentry.Protocol.Envelopes;

#if NET9_0_OR_GREATER
Expand All @@ -12,21 +13,22 @@ namespace Sentry.Internal;
internal sealed class BatchProcessor : IDisposable
{
private readonly IHub _hub;
private readonly System.Timers.Timer _timer;
private readonly BatchProcessorTimer _timer;
private readonly BatchBuffer<SentryLog> _logs;
private readonly Lock _lock;

private DateTime _lastFlush = DateTime.MinValue;

public BatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval)
: this(hub, batchCount, new TimersBatchProcessorTimer(batchInterval))
{
}

public BatchProcessor(IHub hub, int batchCount, BatchProcessorTimer timer)
{
_hub = hub;

_timer = new System.Timers.Timer(batchInterval.TotalMilliseconds)
{
AutoReset = false,
Enabled = false,
};
_timer = timer;
_timer.Elapsed += IntervalElapsed;

_logs = new BatchBuffer<SentryLog>(batchCount);
Expand Down Expand Up @@ -63,7 +65,7 @@ private void Flush()
_lastFlush = DateTime.UtcNow;

var logs = _logs.ToArrayAndClear();
_ = _hub.CaptureEnvelope(Envelope.FromLogs(logs));
_ = _hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(logs)));
}

private void IntervalElapsed(object? sender, ElapsedEventArgs e)
Expand Down
61 changes: 61 additions & 0 deletions src/Sentry/Internal/BatchProcessorTimer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Timers;

namespace Sentry.Internal;

internal abstract class BatchProcessorTimer : IDisposable
{
protected BatchProcessorTimer()
{
}

public abstract bool Enabled { get; set; }

public abstract event EventHandler<ElapsedEventArgs> Elapsed;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
}
}

internal sealed class TimersBatchProcessorTimer : BatchProcessorTimer
{
private readonly System.Timers.Timer _timer;

public TimersBatchProcessorTimer(TimeSpan interval)
{
_timer = new System.Timers.Timer(interval.TotalMilliseconds)
{
AutoReset = false,
Enabled = false,
};
_timer.Elapsed += OnElapsed;
}

public override bool Enabled
{
get => _timer.Enabled;
set => _timer.Enabled = value;
}

public override event EventHandler<ElapsedEventArgs>? Elapsed;

private void OnElapsed(object? sender, ElapsedEventArgs e)
{
Elapsed?.Invoke(sender, e);
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
_timer.Elapsed -= OnElapsed;
_timer.Dispose();
}
}
}
4 changes: 2 additions & 2 deletions src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -446,13 +446,13 @@ internal static Envelope FromClientReport(ClientReport clientReport)
}

[Experimental(DiagnosticId.ExperimentalFeature)]
internal static Envelope FromLogs(SentryLog[] logs)
internal static Envelope FromLog(StructuredLog log)
{
var header = DefaultHeader;

var items = new[]
{
EnvelopeItem.FromLogs(logs),
EnvelopeItem.FromLog(log),
};

return new Envelope(header, items);
Expand Down
5 changes: 2 additions & 3 deletions src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,16 +372,15 @@ internal static EnvelopeItem FromClientReport(ClientReport report)
}

[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
internal static EnvelopeItem FromLogs(SentryLog[] logs)
internal static EnvelopeItem FromLog(StructuredLog log)
{
var header = new Dictionary<string, object?>(3, StringComparer.Ordinal)
{
[TypeKey] = TypeValueLog,
["item_count"] = logs.Length,
["item_count"] = log.Length,
["content_type"] = "application/vnd.sentry.items.log+json",
};

var log = new StructuredLog(logs);
return new EnvelopeItem(header, new JsonSerializable(log));
}

Expand Down
6 changes: 6 additions & 0 deletions src/Sentry/Protocol/StructuredLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ internal sealed class StructuredLog : ISentryJsonSerializable
{
private readonly SentryLog[] _items;

public StructuredLog(SentryLog log)
{
_items = [log];
}

public StructuredLog(SentryLog[] logs)
{
_items = logs;
}

public int Length => _items.Length;
public ReadOnlySpan<SentryLog> Items => _items;

public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
Expand Down
175 changes: 175 additions & 0 deletions test/Sentry.Tests/Internals/BatchBufferTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
namespace Sentry.Tests.Internals;

public class BatchBufferTests
{
[Fact]
public void Ctor_CapacityIsNegative_Throws()
{
var ctor = () => new BatchBuffer<string>(-1);

Assert.Throws<ArgumentOutOfRangeException>("capacity", ctor);
}

[Fact]
public void Ctor_CapacityIsZero_Throws()
{
var ctor = () => new BatchBuffer<string>(0);

Assert.Throws<ArgumentOutOfRangeException>("capacity", ctor);
}

[Fact]
public void TryAdd_CapacityOne_CanAddOnce()
{
var buffer = new BatchBuffer<string>(1);
AssertProperties(buffer, 0, 1, true, false);

buffer.TryAdd("one").Should().BeTrue();
AssertProperties(buffer, 1, 1, false, true);

buffer.TryAdd("two").Should().BeFalse();
AssertProperties(buffer, 1, 1, false, true);
}

[Fact]
public void TryAdd_CapacityTwo_CanAddTwice()
{
var buffer = new BatchBuffer<string>(2);
AssertProperties(buffer, 0, 2, true, false);

buffer.TryAdd("one").Should().BeTrue();
AssertProperties(buffer, 1, 2, false, false);

buffer.TryAdd("two").Should().BeTrue();
AssertProperties(buffer, 2, 2, false, true);

buffer.TryAdd("three").Should().BeFalse();
AssertProperties(buffer, 2, 2, false, true);
}

[Fact]
public void ToArray_IsEmpty_EmptyArray()
{
var buffer = new BatchBuffer<string>(3);

var array = buffer.ToArray();

Assert.Empty(array);
AssertProperties(buffer, 0, 3, true, false);
}

[Fact]
public void ToArray_IsNotEmptyNorFull_PartialArray()
{
var buffer = new BatchBuffer<string>(3);
buffer.TryAdd("one").Should().BeTrue();
buffer.TryAdd("two").Should().BeTrue();

var array = buffer.ToArray();

Assert.Collection(array,
item => Assert.Equal("one", item),
item => Assert.Equal("two", item));
AssertProperties(buffer, 2, 3, false, false);
}

[Fact]
public void ToArray_IsFull_FullArray()
{
var buffer = new BatchBuffer<string>(3);
buffer.TryAdd("one").Should().BeTrue();
buffer.TryAdd("two").Should().BeTrue();
buffer.TryAdd("three").Should().BeTrue();

var array = buffer.ToArray();

Assert.Collection(array,
item => Assert.Equal("one", item),
item => Assert.Equal("two", item),
item => Assert.Equal("three", item));
AssertProperties(buffer, 3, 3, false, true);
}

[Fact]
public void Clear_IsEmpty_NoOp()
{
var buffer = new BatchBuffer<string>(2);

AssertProperties(buffer, 0, 2, true, false);
buffer.Clear();
AssertProperties(buffer, 0, 2, true, false);
}

[Fact]
public void Clear_IsNotEmptyNorFull_ClearArray()
{
var buffer = new BatchBuffer<string>(2);
buffer.TryAdd("one").Should().BeTrue();

AssertProperties(buffer, 1, 2, false, false);
buffer.Clear();
AssertProperties(buffer, 0, 2, true, false);
}

[Fact]
public void Clear_IsFull_ClearArray()
{
var buffer = new BatchBuffer<string>(2);
buffer.TryAdd("one").Should().BeTrue();
buffer.TryAdd("two").Should().BeTrue();

AssertProperties(buffer, 2, 2, false, true);
buffer.Clear();
AssertProperties(buffer, 0, 2, true, false);
}

[Fact]
public void ToArrayAndClear_IsEmpty_EmptyArray()
{
var buffer = new BatchBuffer<string>(2);

AssertProperties(buffer, 0, 2, true, false);
var array = buffer.ToArrayAndClear();
AssertProperties(buffer, 0, 2, true, false);
Assert.Empty(array);
}

[Fact]
public void ToArrayAndClear_IsNotEmptyNorFull_PartialArray()
{
var buffer = new BatchBuffer<string>(2);
buffer.TryAdd("one").Should().BeTrue();

AssertProperties(buffer, 1, 2, false, false);
var array = buffer.ToArrayAndClear();
AssertProperties(buffer, 0, 2, true, false);
Assert.Collection(array,
item => Assert.Equal("one", item));
}

[Fact]
public void ToArrayAndClear_IsFull_FullArray()
{
var buffer = new BatchBuffer<string>(2);
buffer.TryAdd("one").Should().BeTrue();
buffer.TryAdd("two").Should().BeTrue();

AssertProperties(buffer, 2, 2, false, true);
var array = buffer.ToArrayAndClear();
AssertProperties(buffer, 0, 2, true, false);
Assert.Collection(array,
item => Assert.Equal("one", item),
item => Assert.Equal("two", item));
}

private static void AssertProperties<T>(BatchBuffer<T> buffer, int count, int capacity, bool empty, bool full)
{
using (new AssertionScope())
{
buffer.Count.Should().Be(count);
buffer.Capacity.Should().Be(capacity);
buffer.IsEmpty.Should().Be(empty);
buffer.IsFull.Should().Be(full);
}
}
}
Loading