Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- [HTTP Client errors](https://docs.sentry.io/platforms/apple/guides/ios/configuration/http-client-errors/) now mark sessions as errored (#6633)

### Fixes

- Limit log batching to maximum 1000 logs per batch (#6768)

## 9.0.0-alpha.1

### Breaking Changes
Expand Down
17 changes: 14 additions & 3 deletions Sources/Swift/Tools/SentryLogBatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation

private let options: Options
private let flushTimeout: TimeInterval
private let maxLogCount: Int
private let maxBufferSizeBytes: Int
private let dispatchQueue: SentryDispatchQueueWrapper

Expand All @@ -24,14 +25,16 @@ import Foundation

private weak var delegate: SentryLogBatcherDelegate?

/// Convenience initializer with default flush timeout and buffer size.
/// Convenience initializer with default flush timeout, max log count (1000), and buffer size.
/// - Parameters:
/// - options: The Sentry configuration options
/// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state
/// - delegate: The delegate to handle captured log batches
///
/// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety.
/// Passing a concurrent queue will result in undefined behavior and potential data races.
///
/// - Note: This initializer sets `maxLogCount` to 1000, ensuring we never send more than 1000 logs in a single batch.
@_spi(Private) public convenience init(
options: Options,
dispatchQueue: SentryDispatchQueueWrapper,
Expand All @@ -40,7 +43,8 @@ import Foundation
self.init(
options: options,
flushTimeout: 5,
maxBufferSizeBytes: 1_024 * 1_024, // 1MB
maxLogCount: 1_000, // Maximum 1000 logs per batch
maxBufferSizeBytes: 1_024 * 1_024, // 1MB buffer size
dispatchQueue: dispatchQueue,
delegate: delegate
)
Expand All @@ -50,21 +54,27 @@ import Foundation
/// - Parameters:
/// - options: The Sentry configuration options
/// - flushTimeout: The timeout interval after which buffered logs will be flushed
/// - maxLogCount: The maximum number of logs to batch before triggering an immediate flush (default: 1000)
/// - maxBufferSizeBytes: The maximum buffer size in bytes before triggering an immediate flush
/// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state
/// - delegate: The delegate to handle captured log batches
///
/// - Important: The `dispatchQueue` parameter MUST be a serial queue to ensure thread safety.
/// Passing a concurrent queue will result in undefined behavior and potential data races.
///
/// - Note: Logs are flushed when either `maxLogCount` (1000) or `maxBufferSizeBytes` limit is reached,
/// ensuring we never send more than 1000 logs in a single batch.
@_spi(Private) public init(
options: Options,
flushTimeout: TimeInterval,
maxLogCount: Int,
maxBufferSizeBytes: Int,
dispatchQueue: SentryDispatchQueueWrapper,
delegate: SentryLogBatcherDelegate
) {
self.options = options
self.flushTimeout = flushTimeout
self.maxLogCount = maxLogCount
self.maxBufferSizeBytes = maxBufferSizeBytes
self.dispatchQueue = dispatchQueue
self.delegate = delegate
Expand Down Expand Up @@ -189,7 +199,8 @@ import Foundation
encodedLogs.append(encodedLog)
encodedLogsSize += encodedLog.count

if encodedLogsSize >= maxBufferSizeBytes {
// Flush when we reach max log count or max buffer size
if encodedLogs.count >= maxLogCount || encodedLogsSize >= maxBufferSizeBytes {
performCaptureLogs()
} else if encodedLogsWereEmpty && timerWorkItem == nil {
startTimer()
Expand Down
30 changes: 27 additions & 3 deletions Tests/SentryTests/SentryLogBatcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ final class SentryLogBatcherTests: XCTestCase {
sut = SentryLogBatcher(
options: options,
flushTimeout: 0.1, // Very small timeout for testing
maxBufferSizeBytes: 800, // byte limit for testing (log with attributes ~390 bytes)
maxLogCount: 10, // Maximum 10 logs per batch
maxBufferSizeBytes: 8_000, // byte limit for testing (log with attributes ~390 bytes)
dispatchQueue: testDispatchQueue,
delegate: testDelegate
)
Expand Down Expand Up @@ -69,7 +70,7 @@ final class SentryLogBatcherTests: XCTestCase {

func testBufferReachesMaxSize_FlushesImmediately() throws {
// Arrange
let largeLogBody = String(repeating: "A", count: 600) // Larger than 500 byte limit
let largeLogBody = String(repeating: "A", count: 8_000) // Larger than 8000 byte limit
let largeLog = createTestLog(body: largeLogBody)

// Act
Expand All @@ -84,6 +85,27 @@ final class SentryLogBatcherTests: XCTestCase {
XCTAssertEqual(capturedLogs[0].body, largeLogBody)
}

// MARK: - Max Log Count Tests

func testMaxLogCount_FlushesWhenReached() throws {
// Act - Add exactly maxLogCount logs
for i in 0..<9 {
let log = createTestLog(body: "Log \(i + 1)")
sut.addLog(log, scope: scope)
}

XCTAssertEqual(testDelegate.captureLogsDataInvocations.count, 0)

let log = createTestLog(body: "Log \(10)") // Reached 10 max logs limit
sut.addLog(log, scope: scope)

// Assert - Should have flushed once when reaching maxLogCount
XCTAssertEqual(testDelegate.captureLogsDataInvocations.count, 1)

let capturedLogs = testDelegate.getCapturedLogs()
XCTAssertEqual(capturedLogs.count, 10, "Should have captured exactly \(10) logs")
}

// MARK: - Timeout Tests

func testTimeout_FlushesAfterDelay() throws {
Expand Down Expand Up @@ -185,7 +207,7 @@ final class SentryLogBatcherTests: XCTestCase {

func testScheduledFlushAfterBufferAlreadyFlushed_DoesNothing() throws {
// Arrange
let largeLogBody = String(repeating: "B", count: 300)
let largeLogBody = String(repeating: "B", count: 4_000)
let log1 = createTestLog(body: largeLogBody)
let log2 = createTestLog(body: largeLogBody)

Expand Down Expand Up @@ -235,6 +257,7 @@ final class SentryLogBatcherTests: XCTestCase {
let sutWithRealQueue = SentryLogBatcher(
options: options,
flushTimeout: 5,
maxLogCount: 1_000, // Maximum 1000 logs per batch
maxBufferSizeBytes: 10_000,
dispatchQueue: SentryDispatchQueueWrapper(),
delegate: testDelegate
Expand Down Expand Up @@ -264,6 +287,7 @@ final class SentryLogBatcherTests: XCTestCase {
let sutWithRealQueue = SentryLogBatcher(
options: options,
flushTimeout: 0.2,
maxLogCount: 1_000, // Maximum 1000 logs per batch
maxBufferSizeBytes: 10_000,
dispatchQueue: SentryDispatchQueueWrapper(),
delegate: testDelegate
Expand Down
Loading