From 833ee243013613d4cf0985c364e3552f7046a72d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 16 Oct 2025 14:26:52 +0200 Subject: [PATCH 1/4] Add automatic stdout log integration when `enableLogs` is set to true --- CHANGELOG.md | 1 + Sentry.xcodeproj/project.pbxproj | 29 ++- SentryTestUtils/TestDispatchFactory.swift | 5 +- Sources/Sentry/SentryStdoutLogIntegration.m | 171 ++++++++++++++++++ Sources/Sentry/SentyOptionsInternal.m | 3 +- .../include/SentryStdOutLogIntegration.h | 21 +++ .../SentryStdOutLogIntegrationTests.swift | 131 ++++++++++++++ .../SentryTests/SentryTests-Bridging-Header.h | 1 + 8 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 Sources/Sentry/SentryStdoutLogIntegration.m create mode 100644 Sources/Sentry/include/SentryStdOutLogIntegration.h create mode 100644 Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e8655ac2e..a7ae8dbf3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Features - Add SentryDistribution as Swift Package Manager target (#6149) +- Add automatic stdout log integration when `enableLogs` is set to true (#XXXX) ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 3f38d3b94f9..93a8b6c8bf8 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -734,7 +734,10 @@ 92235CAC2E15369900865983 /* SentryLogBatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAB2E15369900865983 /* SentryLogBatcher.swift */; }; 92235CAE2E15549C00865983 /* SentryLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAD2E15549C00865983 /* SentryLogger.swift */; }; 92235CB02E155B2600865983 /* SentryLoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */; }; + 9229D1462E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9229D1452E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift */; }; 925824C22CB5897700C9B20B /* SentrySessionReplayIntegration-Hybrid.h in Headers */ = {isa = PBXBuildFile; fileRef = D80382BE2C09C6FD0090E048 /* SentrySessionReplayIntegration-Hybrid.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 925B67CC2EA11970005B2D3B /* SentryStdOutLogIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 925B67CB2EA11970005B2D3B /* SentryStdOutLogIntegration.h */; }; + 925B67D02EA11B0E005B2D3B /* SentryStdoutLogIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 925B67CF2EA11B0E005B2D3B /* SentryStdoutLogIntegration.m */; }; 9264E1EB2E2E385E00B077CF /* SentryLogMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */; }; 9264E1ED2E2E397C00B077CF /* SentryLogMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */; }; 92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -2075,6 +2078,9 @@ 92235CAB2E15369900865983 /* SentryLogBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogBatcher.swift; sourceTree = ""; }; 92235CAD2E15549C00865983 /* SentryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogger.swift; sourceTree = ""; }; 92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = ""; }; + 9229D1452E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStdOutLogIntegrationTests.swift; sourceTree = ""; }; + 925B67CB2EA11970005B2D3B /* SentryStdOutLogIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStdOutLogIntegration.h; path = include/SentryStdOutLogIntegration.h; sourceTree = ""; }; + 925B67CF2EA11B0E005B2D3B /* SentryStdoutLogIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryStdoutLogIntegration.m; sourceTree = ""; }; 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = ""; }; 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = ""; }; 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = ""; }; @@ -2913,6 +2919,7 @@ D85596EF280580BE0041FF8B /* Screenshot */, 0A9BF4E028A114690068D266 /* ViewHierarchy */, D80CD8D52B752FD9002F710B /* SessionReplay */, + 925B67892EA118EA005B2D3B /* StdOutLog */, FA034AC72DD3DB4900FE3107 /* SentryIntegrationProtocol.h */, 7BA235622600B61200E12865 /* SentryInternalNotificationNames.h */, 0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */, @@ -3510,6 +3517,7 @@ 7BE0DC3F272AE9F0004FA8B7 /* Session */, 7BE0DC3E272AE9DC004FA8B7 /* SentryCrash */, D80694C12B7CC85800B820E6 /* SessionReplay */, + 9292AA712EA1110E005DF5E2 /* StdOutLog */, 7B59398324AB481B0003AAD2 /* NotificationCenterTestCase.swift */, 0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */, ); @@ -4217,6 +4225,23 @@ name = Transaction; sourceTree = ""; }; + 925B67892EA118EA005B2D3B /* StdOutLog */ = { + isa = PBXGroup; + children = ( + 925B67CB2EA11970005B2D3B /* SentryStdOutLogIntegration.h */, + 925B67CF2EA11B0E005B2D3B /* SentryStdoutLogIntegration.m */, + ); + name = StdOutLog; + sourceTree = ""; + }; + 9292AA712EA1110E005DF5E2 /* StdOutLog */ = { + isa = PBXGroup; + children = ( + 9229D1452E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift */, + ); + path = StdOutLog; + sourceTree = ""; + }; D4009EA02D77196F0007AF30 /* ViewCapture */ = { isa = PBXGroup; children = ( @@ -5199,6 +5224,7 @@ 7BA61CC8247D125400C130A8 /* SentryDefaultThreadInspector.h in Headers */, 63FE713320DA4C1100CDBAE8 /* SentryCrashCPU.h in Headers */, 6271ADF32BA06D9B0098D2E9 /* SentryInternalSerializable.h in Headers */, + 925B67CC2EA11970005B2D3B /* SentryStdOutLogIntegration.h in Headers */, D8853C842833EABC00700D64 /* SentryANRTrackerV1.h in Headers */, 63FE715B20DA4C1100CDBAE8 /* SentryCrashSignalInfo.h in Headers */, 63FE70E520DA4C1000CDBAE8 /* SentryCrashMonitor_CPPException.h in Headers */, @@ -5670,6 +5696,7 @@ 84DBC62C2CE82F12000C4904 /* SentryFeedback.swift in Sources */, F41362132E1C566100B84443 /* SentryScopePersistentStore+User.swift in Sources */, 63B818FA1EC34639002FDF4C /* SentryDebugMeta.m in Sources */, + 925B67D02EA11B0E005B2D3B /* SentryStdoutLogIntegration.m in Sources */, 7B98D7D325FB65AE00C5A389 /* SentryWatchdogTerminationTracker.m in Sources */, 8E564AE8267AF22600FE117D /* SentryNetworkTrackingIntegration.m in Sources */, 63AA75EF1EB8B3C400D153DE /* SentryClient.m in Sources */, @@ -5683,7 +5710,6 @@ FAF1201A2E70C0EE006E1DA3 /* SentryEnvelopeHeaderHelper.m in Sources */, F49D419E2DEA3D0600D9244E /* SentryCrashExceptionApplicationHelper.m in Sources */, D4F56C5D2E9CF38900D57DAB /* SentryXcodeVersion.swift in Sources */, - 7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */, D8ACE3C82762187200F5A213 /* SentryFileIOTrackerHelper.m in Sources */, D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */, FA7206E12E0B37C80072FDD4 /* SentryProfileCollector.mm in Sources */, @@ -6115,6 +6141,7 @@ 8431EE5B29ADB8EA00D8DC56 /* SentryTimeTests.m in Sources */, 7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */, 7B5B94332657A816002E474B /* SentryAppStartTrackingIntegrationTests.swift in Sources */, + 9229D1462E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift in Sources */, 62278CA82E30B21A0022ABC6 /* SentryHttpTransportFlushIntegrationTests.swift in Sources */, 0A5370A128A3EC2400B2DCDE /* SentryViewHierarchyProviderTests.swift in Sources */, D8FFE50C2703DBB400607131 /* SwizzlingCallTests.swift in Sources */, diff --git a/SentryTestUtils/TestDispatchFactory.swift b/SentryTestUtils/TestDispatchFactory.swift index f2eebe049e7..d62b1526ab9 100644 --- a/SentryTestUtils/TestDispatchFactory.swift +++ b/SentryTestUtils/TestDispatchFactory.swift @@ -5,6 +5,7 @@ import Foundation @_spi(Private) public class TestDispatchFactory: SentryDispatchFactory { public var vendedSourceHandler: ((TestDispatchSourceWrapper) -> Void)? public var vendedQueueHandler: ((TestSentryDispatchQueueWrapper) -> Void)? + public var vendedUtilityQueueHandler: ((TestSentryDispatchQueueWrapper) -> Void)? public var createUtilityQueueInvocations = Invocations<(name: String, relativePriority: Int32)>() @@ -18,7 +19,9 @@ import Foundation createUtilityQueueInvocations.record((String(cString: name), relativePriority)) // Due to the absense of `dispatch_queue_attr_make_with_qos_class` in Swift, we do not pass any attributes. // This will not affect the tests as they do not need an actual low priority queue. - return TestSentryDispatchQueueWrapper(name: name, attributes: nil) + let queue = TestSentryDispatchQueueWrapper(name: name, attributes: nil) + vendedUtilityQueueHandler?(queue) + return queue } public override func source(withInterval interval: Int, leeway: Int, queueName: UnsafePointer, attributes: __OS_dispatch_queue_attr, eventHandler: @escaping () -> Void) -> SentryDispatchSourceWrapper { diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m new file mode 100644 index 00000000000..750730d56da --- /dev/null +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -0,0 +1,171 @@ +#import "SentryStdOutLogIntegration.h" +#import "SentryDependencyContainer.h" +#import "SentryLogC.h" +#import "SentryOptions.h" +#import "SentrySwift.h" +#import +#import + +@interface SentryStdOutLogIntegration () + +@property (strong, nonatomic) NSPipe *stdErrPipe; +@property (strong, nonatomic) NSPipe *stdOutPipe; +@property (nonatomic, copy) void (^logHandler)(NSData *, BOOL isStderr); +@property (nonatomic, assign) int originalStdOut; +@property (nonatomic, assign) int originalStdErr; +@property (strong, nonatomic, nullable) SentryLogger *injectedLogger; +@property (strong, nonatomic, nullable) SentryDispatchFactory *injectedDispatchFactory; +@property (strong, nonatomic, nullable) SentryDispatchQueueWrapper *dispatchQueueWrapper; + +@end + +// Global atomic flag for infinite loop protection +static _Atomic bool _isForwardingLogs = false; + +@implementation SentryStdOutLogIntegration + +- (instancetype)init:(SentryDispatchFactory *)dispatchFactory +{ + return [self initWithDispatchFactory:dispatchFactory logger:nil]; +} + +// Only for testing +- (instancetype)initWithDispatchFactory:(SentryDispatchFactory *)dispatchFactory + logger:(nullable SentryLogger *)logger +{ + if (self = [super init]) { + self.injectedLogger = logger; + self.injectedDispatchFactory = dispatchFactory; + } + return self; +} + +- (SentryLogger *)logger +{ + return self.injectedLogger ?: SentrySDK.logger; +} + +- (SentryDispatchFactory *)dispatchFactory +{ + return self.injectedDispatchFactory ?: SentryDependencyContainer.sharedInstance.dispatchFactory; +} + +- (BOOL)installWithOptions:(SentryOptions *)options +{ + if (![super installWithOptions:options]) { + return NO; + } + + // Only install if logs are enabled + if (!options.enableLogs) { + return NO; + } + + self.dispatchQueueWrapper = + [self.dispatchFactory createUtilityQueue:"com.sentry.stdout_log_writing_queue" + relativePriority:-3]; + + __weak typeof(self) weakSelf = self; + self.logHandler = ^(NSData *data, BOOL isStderr) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) + return; + + if (data && data.length > 0) { + NSString *logString = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + if (logString) { + // Check global atomic flag to avoid infinite loops + if (atomic_exchange(&_isForwardingLogs, true)) { + return; // Already forwarding, break the loop. + } + NSDictionary *attributes = + @{ @"sentry.log.source" : isStderr ? @"stderr" : @"stdout" }; + if (isStderr) { + [strongSelf.logger warn:logString attributes:attributes]; + } else { + [strongSelf.logger info:logString attributes:attributes]; + } + + // Clear global atomic flag + atomic_store(&_isForwardingLogs, false); + } + } + }; + + [self start]; + + return YES; +} + +- (void)start +{ + self.originalStdOut = dup(STDOUT_FILENO); + self.originalStdErr = dup(STDERR_FILENO); + + self.stdOutPipe = [self duplicateFileDescriptor:STDOUT_FILENO isStderr:NO]; + self.stdErrPipe = [self duplicateFileDescriptor:STDERR_FILENO isStderr:YES]; +} + +- (void)stop +{ + if (self.stdOutPipe || self.stdErrPipe) { + // Restore original file descriptors + if (self.originalStdOut >= 0) { + dup2(self.originalStdOut, STDOUT_FILENO); + close(self.originalStdOut); + self.originalStdOut = -1; + } + + if (self.originalStdErr >= 0) { + dup2(self.originalStdErr, STDERR_FILENO); + close(self.originalStdErr); + self.originalStdErr = -1; + } + + // Clean up pipes + self.stdOutPipe.fileHandleForReading.readabilityHandler = nil; + self.stdErrPipe.fileHandleForReading.readabilityHandler = nil; + + self.stdOutPipe = nil; + self.stdErrPipe = nil; + self.logHandler = nil; + } +} + +- (void)uninstall +{ + [self stop]; +} + +// Write the input file descriptor to the input file handle, preserving the original output as well. +// This can be used to save stdout/stderr to a file while also keeping it on the console. +- (NSPipe *)duplicateFileDescriptor:(int)fileDescriptor isStderr:(BOOL)isStderr +{ + NSPipe *pipe = [[NSPipe alloc] init]; + int newDescriptor = dup(fileDescriptor); + NSFileHandle *newFileHandle = [[NSFileHandle alloc] initWithFileDescriptor:newDescriptor + closeOnDealloc:YES]; + + if (dup2(pipe.fileHandleForWriting.fileDescriptor, fileDescriptor) < 0) { + SENTRY_LOG_ERROR(@"Unable to duplicate file descriptor %d", fileDescriptor); + close(newDescriptor); + return nil; + } + + __weak typeof(self) weakSelf = self; + __weak NSFileHandle *weakNewFileHandle = newFileHandle; + __weak SentryDispatchQueueWrapper *weakQueue = self.dispatchQueueWrapper; + + pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) { + NSData *data = handle.availableData; + if (weakSelf.logHandler) { + weakSelf.logHandler(data, isStderr); + } + [weakQueue dispatchAsyncWithBlock:^{ [weakNewFileHandle writeData:data]; }]; + }; + + return pipe; +} + +@end diff --git a/Sources/Sentry/SentyOptionsInternal.m b/Sources/Sentry/SentyOptionsInternal.m index 63315f6cfdb..86edde788ab 100644 --- a/Sources/Sentry/SentyOptionsInternal.m +++ b/Sources/Sentry/SentyOptionsInternal.m @@ -12,6 +12,7 @@ #import "SentryOptions.h" #import "SentryOptionsInternal.h" #import "SentrySessionReplayIntegration.h" +#import "SentryStdoutLogIntegration.h" #import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" @@ -54,7 +55,7 @@ @implementation SentryOptionsInternal [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], - [SentrySwiftAsyncIntegration class], nil]; + [SentryStdOutLogIntegration class], [SentrySwiftAsyncIntegration class], nil]; #if TARGET_OS_IOS && SENTRY_HAS_UIKIT [defaultIntegrations addObject:[SentryUserFeedbackIntegration class]]; diff --git a/Sources/Sentry/include/SentryStdOutLogIntegration.h b/Sources/Sentry/include/SentryStdOutLogIntegration.h new file mode 100644 index 00000000000..602dd2ef3db --- /dev/null +++ b/Sources/Sentry/include/SentryStdOutLogIntegration.h @@ -0,0 +1,21 @@ +#import "SentryBaseIntegration.h" + +NS_ASSUME_NONNULL_BEGIN + +@class SentryLogger; +@class SentryDispatchFactory; +@class SentryDispatchQueueWrapper; + +/** + * Integration that captures stdout and stderr output and forwards it to Sentry logs. + * This integration is automatically enabled when enableLogs is set to true. + */ +@interface SentryStdOutLogIntegration : SentryBaseIntegration + +// Only for testing +- (instancetype)initWithDispatchFactory:(SentryDispatchFactory *)dispatchFactory + logger:(nullable SentryLogger *)logger; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift new file mode 100644 index 00000000000..9f73204433f --- /dev/null +++ b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift @@ -0,0 +1,131 @@ +@_spi(Private) @testable import Sentry +@_spi(Private) import SentryTestUtils +import XCTest + +class SentryStdOutLogIntegrationTests: XCTestCase { + + private class Fixture { + let options: Options + let client: TestClient + let hub: SentryHub + let batcher: TestLogBatcher + let logger: SentryLogger + let dispatchFactory: TestDispatchFactory + + var testQueue: TestSentryDispatchQueueWrapper? + + init() { + options = Options() + options.enableLogs = true + + client = TestClient(options: options)! + hub = TestHub(client: client, andScope: Scope()) + batcher = TestLogBatcher(client: client, dispatchQueue: TestSentryDispatchQueueWrapper()) + logger = SentryLogger(hub: hub, dateProvider: TestCurrentDateProvider(), batcher: batcher) + + dispatchFactory = TestDispatchFactory() + dispatchFactory.vendedUtilityQueueHandler = { [weak self] queue in + self?.testQueue = queue + } + } + + func getIntegration() -> SentryStdOutLogIntegration { + return SentryStdOutLogIntegration(dispatchFactory: dispatchFactory, logger: logger) + } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func testInstallWithLogsEnabled() { + let integration = fixture.getIntegration() + let installed = integration.install(with: fixture.options) + + XCTAssertTrue(installed, "Integration should install when logs are enabled") + + // Clean up + integration.uninstall() + } + + func testInstallWithLogsDisabled() { + let options = Options() + options.enableLogs = false + + let integration = fixture.getIntegration() + let result = integration.install(with: options) + + XCTAssertFalse(result, "Integration should not install when logs are disabled") + } + + func testUninstall() { + let integration = fixture.getIntegration() + let installed = integration.install(with: fixture.options) + XCTAssertTrue(installed, "Integration should install first") + + // Uninstall should not crash + integration.uninstall() + + // Test that we can uninstall multiple times without issues + integration.uninstall() + } + + func testStdoutCapture() throws { + let integration = fixture.getIntegration() + _ = integration.install(with: fixture.options) + + print("App stdout message from print") + expect("Wait for stdout capture to trigger async dispatch") + + let log = try XCTUnwrap(fixture.batcher.addInvocations.first) + XCTAssertEqual(log.level, SentryLog.Level.info, "Should use info level for stdout") + XCTAssertTrue(log.body.contains("App stdout message from print"), "Should contain the stdout test message") + XCTAssertEqual(log.attributes["sentry.log.source"]?.value as? String, "stdout", "Should have stdout source attribute") + + // Clean up + integration.uninstall() + } + + func testStderrCapture() throws { + let integration = fixture.getIntegration() + _ = integration.install(with: fixture.options) + + // Use NSLog to write to stderr (this should be captured) + NSLog("App stderr message from NSLog") + expect("Wait for stderr capture to trigger async dispatch") + + let log = try XCTUnwrap(fixture.batcher.addInvocations.first) + XCTAssertEqual(log.level, SentryLog.Level.warn, "Should use warn level for stderr") + XCTAssertTrue(log.body.contains("App stderr message from NSLog"), "Should contain the stderr test message") + XCTAssertEqual(log.attributes["sentry.log.source"]?.value as? String, "stderr", "Should have stderr source attribute") + + // Clean up + integration.uninstall() + } + + // Helper + + private func expect(_ description: String, timeout: TimeInterval = 0.1) { + // Record the initial count of async invocations + let initialAsyncCount = fixture.testQueue?.dispatchAsyncInvocations.count ?? 0 + + // Wait for the capture to trigger an async dispatch + let expectation = XCTestExpectation(description: description) + let timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in + if (self.fixture.testQueue?.dispatchAsyncInvocations.count ?? 0) > initialAsyncCount { + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + timer.invalidate() + } +} diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 4d8d622b8b4..5d2a33898f6 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -168,6 +168,7 @@ #import "SentrySpotlightTransport.h" #import "SentryStacktrace.h" #import "SentryStacktraceBuilder.h" +#import "SentryStdOutLogIntegration.h" #import "SentrySubClassFinder.h" #import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" From e5f42844e73f5bf26c80708c88ae3191eeb37fdd Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 16 Oct 2025 14:54:01 +0200 Subject: [PATCH 2/4] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d23d6826e..8df7375d5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ ### Features - Add SentryDistribution as Swift Package Manager target (#6149) -- Add automatic stdout log integration when `enableLogs` is set to true (#XXXX) - Add option `enablePropagateTraceparent` to support OTel/W3C trace propagation (#6356) +- Structured Logs: Collect `stdout/stderr` per default (#6441) ### Fixes From 3b1fac81258e77ad9ea15f32389b603d39bb1b98 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 16 Oct 2025 15:31:00 +0200 Subject: [PATCH 3/4] fix weak ref handling --- Sources/Sentry/SentryStdoutLogIntegration.m | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index 750730d56da..056bd8d8a73 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -154,15 +154,12 @@ - (NSPipe *)duplicateFileDescriptor:(int)fileDescriptor isStderr:(BOOL)isStderr } __weak typeof(self) weakSelf = self; - __weak NSFileHandle *weakNewFileHandle = newFileHandle; - __weak SentryDispatchQueueWrapper *weakQueue = self.dispatchQueueWrapper; - pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) { NSData *data = handle.availableData; if (weakSelf.logHandler) { weakSelf.logHandler(data, isStderr); } - [weakQueue dispatchAsyncWithBlock:^{ [weakNewFileHandle writeData:data]; }]; + [weakSelf.dispatchQueueWrapper dispatchAsyncWithBlock:^{ [newFileHandle writeData:data]; }]; }; return pipe; From 17e96a7948170d32836c3d96a6e9c09c1ab91855 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 16 Oct 2025 15:34:34 +0200 Subject: [PATCH 4/4] remove SentrySDKLog from logger --- Sources/Swift/Tools/SentryLogger.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/Swift/Tools/SentryLogger.swift b/Sources/Swift/Tools/SentryLogger.swift index dd5bbff5d60..ac7139c398a 100644 --- a/Sources/Swift/Tools/SentryLogger.swift +++ b/Sources/Swift/Tools/SentryLogger.swift @@ -216,10 +216,6 @@ public final class SentryLogger: NSObject { } if let processedLog { - SentrySDKLog.log( - message: "[SentryLogger] \(processedLog.body)", - andLevel: processedLog.level.toSentryLevel() - ) batcher.add(processedLog) } }