diff --git a/CHANGELOG.md b/CHANGELOG.md index a126d9bd35..9ca0f0410f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 100 logs per batch (#6768) + ## 9.0.0-alpha.1 ### Breaking Changes diff --git a/Sources/Swift/Tools/SentryLogBatcher.swift b/Sources/Swift/Tools/SentryLogBatcher.swift index 72d52fbe1b..5151907c0e 100644 --- a/Sources/Swift/Tools/SentryLogBatcher.swift +++ b/Sources/Swift/Tools/SentryLogBatcher.swift @@ -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 @@ -24,7 +25,7 @@ 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 (100), and buffer size. /// - Parameters: /// - options: The Sentry configuration options /// - dispatchQueue: A **serial** dispatch queue wrapper for thread-safe access to mutable state @@ -32,6 +33,8 @@ import Foundation /// /// - 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: Setting `maxLogCount` to 100. While Replay hard limit is 1000, we keep this lower, as it's hard to lower once released. @_spi(Private) public convenience init( options: Options, dispatchQueue: SentryDispatchQueueWrapper, @@ -40,7 +43,8 @@ import Foundation self.init( options: options, flushTimeout: 5, - maxBufferSizeBytes: 1_024 * 1_024, // 1MB + maxLogCount: 100, // Maximum 100 logs per batch + maxBufferSizeBytes: 1_024 * 1_024, // 1MB buffer size dispatchQueue: dispatchQueue, delegate: delegate ) @@ -50,21 +54,26 @@ import Foundation /// - Parameters: /// - options: The Sentry configuration options /// - flushTimeout: The timeout interval after which buffered logs will be flushed + /// - maxLogCount: Maximum number of logs to batch before triggering an immediate flush. /// - 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` or `maxBufferSizeBytes` limit is reached. @_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 @@ -189,7 +198,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() diff --git a/Tests/SentryTests/SentryLogBatcherTests.swift b/Tests/SentryTests/SentryLogBatcherTests.swift index bc315004f9..962eba5238 100644 --- a/Tests/SentryTests/SentryLogBatcherTests.swift +++ b/Tests/SentryTests/SentryLogBatcherTests.swift @@ -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 ) @@ -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 @@ -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 { @@ -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) @@ -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 @@ -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