From 833ee243013613d4cf0985c364e3552f7046a72d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 16 Oct 2025 14:26:52 +0200 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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) } } From d591b0ea3204f56c7aba248ac59b263c05375fe4 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 10 Nov 2025 14:24:16 +0100 Subject: [PATCH 05/16] remove dep container import (ported to swift) --- Sources/Sentry/SentryStdoutLogIntegration.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index 056bd8d8a73..2eed81a152e 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -1,5 +1,4 @@ #import "SentryStdOutLogIntegration.h" -#import "SentryDependencyContainer.h" #import "SentryLogC.h" #import "SentryOptions.h" #import "SentrySwift.h" From 6dfb50f3eb1cbd0149a6d23a8d68f462051557c4 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 10 Nov 2025 14:24:40 +0100 Subject: [PATCH 06/16] add SentryStdOutLogIntegration as default --- Sources/Sentry/SentrySDKInternal.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index bff3ca7211d..5e3d11281ab 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -24,6 +24,7 @@ #import "SentryScope.h" #import "SentrySerialization.h" #import "SentrySessionReplayIntegration.h" +#import "SentryStdOutLogIntegration.h" #import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" #import "SentryTransactionContext.h" @@ -565,7 +566,7 @@ + (void)endSession [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], - [SentrySwiftAsyncIntegration class], nil]; + [SentrySwiftAsyncIntegration class], [SentryStdOutLogIntegration class], nil]; #if TARGET_OS_IOS && SENTRY_HAS_UIKIT [defaultIntegrations addObject:[SentryUserFeedbackIntegration class]]; From d75f26c72bbd4f90d52c77074d6aebd0ed785245 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 10 Nov 2025 14:46:05 +0100 Subject: [PATCH 07/16] filter out strings containing [Sentry] --- Sources/Sentry/SentryStdoutLogIntegration.m | 5 +++ .../SentryStdOutLogIntegrationTests.swift | 31 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index 2eed81a152e..5a59fb9c9d5 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -74,6 +74,11 @@ - (BOOL)installWithOptions:(SentryOptions *)options NSString *logString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if (logString) { + // Skip logs from Sentry itself to avoid infinite loops + if ([logString containsString:@"[Sentry]"]) { + return; + } + // Check global atomic flag to avoid infinite loops if (atomic_exchange(&_isForwardingLogs, true)) { return; // Already forwarding, break the loop. diff --git a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift index 9f73204433f..bd315a9ff3f 100644 --- a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift @@ -7,7 +7,7 @@ class SentryStdOutLogIntegrationTests: XCTestCase { private class Fixture { let options: Options let client: TestClient - let hub: SentryHub + let hub: SentryHubInternal let batcher: TestLogBatcher let logger: SentryLogger let dispatchFactory: TestDispatchFactory @@ -111,6 +111,35 @@ class SentryStdOutLogIntegrationTests: XCTestCase { integration.uninstall() } + func testSentryLogsAreIgnored() throws { + let integration = fixture.getIntegration() + _ = integration.install(with: fixture.options) + + print("[Sentry] This is a Sentry internal print log message") + expect("Wait first print non-capture") + + // OSLOG-E0E93946-72CD-47A5-A9E7-13AD8B177E35 7 80 L 0 {t:1762782027.629955,tz:-60,tzDST:0,tid:0x326ba22,type:"Default",subsystem:null,category:null,offset:0x70a5d8,imgUUID:"249188A3-8F44-3D76-ACB0-0345A43EB0A3",imgPath:"/Library/Developer/CoreSimulator/Volumes/iOS_23B80/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 26.1.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation",procName:"xctest",pid:37095,uid:501} [Sentry] This is a Sentry internal NSLog log message + NSLog("[Sentry] This is a Sentry internal NSLog log message") + expect("Wait first NSLog non-capture") + + SentrySDKLog.error("This is a Sentry internal error message") + expect("Wait first SentrySDKLog.error non-capture") + + // Print another normal log to verify the integration is still working + print("A normal log") + expect("Wait for second normal log capture") + + // Verify only 2 logs were captured (the [Sentry] log was skipped) + XCTAssertEqual(fixture.batcher.addInvocations.count, 1, "Only non-Sentry logs should be captured") + + let log = try XCTUnwrap(fixture.batcher.addInvocations.first) + XCTAssertTrue(log.body.contains("A normal log"), "Only the normal log should be captured") + XCTAssertFalse(log.body.contains("[Sentry]"), "Sentry internal log should not be captured") + + // Clean up + integration.uninstall() + } + // Helper private func expect(_ description: String, timeout: TimeInterval = 0.1) { From 1b8ada0e5dc718256ef448486686e3f4fa819ff6 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 10 Nov 2025 15:18:30 +0100 Subject: [PATCH 08/16] update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8da7e8f83..f3091389693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +## Features + +- Structured Logs: Collect `stdout/stderr` per default (#6441) + ## 9.0.0-alpha.0 ### Breaking Changes @@ -33,7 +39,6 @@ - Add SentryDistribution as Swift Package Manager target (#6149) - Add option `enablePropagateTraceparent` to support OTel/W3C trace propagation (#6356) -- Structured Logs: Collect `stdout/stderr` per default (#6441) - Remove unused `SentryFrame.instruction` property (#6504) - Remove `uuid` and `name` of `SentryDebugMeta` (#6512) Use `debugID` instead of `uuid` and `codeFile` instead of `name`. - Enable enablePreWarmedAppStartTracing by default (#6508). With this option enabled, the SDK collects [prewarmed app starts](https://docs.sentry.io/platforms/apple/tracing/instrumentation/automatic-instrumentation/#prewarmed-app-start-tracing). From 0b164a92c7d96aefd06a8268ccc4a736add284fb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 16:12:40 +0100 Subject: [PATCH 09/16] =?UTF-8?q?don=E2=80=99t=20install=20directly.=20pre?= =?UTF-8?q?pare=20to=20move=20part=20of=20code=20outside=20(not=20user-cre?= =?UTF-8?q?atable=20right=20now)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/Sentry/SentrySDKInternal.m | 3 +- Sources/Sentry/SentryStdoutLogIntegration.m | 55 ++++------------ .../include/SentryStdOutLogIntegration.h | 5 +- .../SentryStdOutLogIntegrationTests.swift | 63 ++++++++++--------- 4 files changed, 49 insertions(+), 77 deletions(-) diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index e89590ae51b..5707f7d0738 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -22,7 +22,6 @@ #import "SentryScope.h" #import "SentrySerialization.h" #import "SentrySessionReplayIntegration.h" -#import "SentryStdOutLogIntegration.h" #import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" #import "SentryTransactionContext.h" @@ -540,7 +539,7 @@ + (void)endSession [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], - [SentrySwiftAsyncIntegration class], [SentryStdOutLogIntegration class], nil]; + [SentrySwiftAsyncIntegration class], nil]; #if TARGET_OS_IOS && SENTRY_HAS_UIKIT [defaultIntegrations addObject:[SentryUserFeedbackIntegration class]]; diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index 5a59fb9c9d5..b5d5181f354 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -1,9 +1,7 @@ #import "SentryStdOutLogIntegration.h" #import "SentryLogC.h" -#import "SentryOptions.h" #import "SentrySwift.h" #import -#import @interface SentryStdOutLogIntegration () @@ -12,43 +10,25 @@ @interface SentryStdOutLogIntegration () @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 +@property (strong, nonatomic) SentryLogger *logger; +@property (strong, nonatomic) SentryDispatchQueueWrapper *dispatchQueue; -// Global atomic flag for infinite loop protection -static _Atomic bool _isForwardingLogs = false; +@end @implementation SentryStdOutLogIntegration -- (instancetype)init:(SentryDispatchFactory *)dispatchFactory -{ - return [self initWithDispatchFactory:dispatchFactory logger:nil]; -} - // Only for testing -- (instancetype)initWithDispatchFactory:(SentryDispatchFactory *)dispatchFactory - logger:(nullable SentryLogger *)logger +- (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue + logger:(SentryLogger *)logger { if (self = [super init]) { - self.injectedLogger = logger; - self.injectedDispatchFactory = dispatchFactory; + self.logger = logger; + self.dispatchQueue = dispatchQueue; } 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]) { @@ -60,10 +40,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options 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; @@ -79,10 +55,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options return; } - // 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) { @@ -90,9 +62,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options } else { [strongSelf.logger info:logString attributes:attributes]; } - - // Clear global atomic flag - atomic_store(&_isForwardingLogs, false); } } }; @@ -129,10 +98,11 @@ - (void)stop // Clean up pipes self.stdOutPipe.fileHandleForReading.readabilityHandler = nil; - self.stdErrPipe.fileHandleForReading.readabilityHandler = nil; - self.stdOutPipe = nil; + + self.stdErrPipe.fileHandleForReading.readabilityHandler = nil; self.stdErrPipe = nil; + self.logHandler = nil; } } @@ -161,9 +131,10 @@ - (NSPipe *)duplicateFileDescriptor:(int)fileDescriptor isStderr:(BOOL)isStderr pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) { NSData *data = handle.availableData; if (weakSelf.logHandler) { - weakSelf.logHandler(data, isStderr); + [weakSelf.dispatchQueue + dispatchAsyncWithBlock:^{ weakSelf.logHandler(data, isStderr); }]; } - [weakSelf.dispatchQueueWrapper dispatchAsyncWithBlock:^{ [newFileHandle writeData:data]; }]; + [newFileHandle writeData:data]; }; return pipe; diff --git a/Sources/Sentry/include/SentryStdOutLogIntegration.h b/Sources/Sentry/include/SentryStdOutLogIntegration.h index 602dd2ef3db..48b1b6700cb 100644 --- a/Sources/Sentry/include/SentryStdOutLogIntegration.h +++ b/Sources/Sentry/include/SentryStdOutLogIntegration.h @@ -3,7 +3,6 @@ NS_ASSUME_NONNULL_BEGIN @class SentryLogger; -@class SentryDispatchFactory; @class SentryDispatchQueueWrapper; /** @@ -13,8 +12,8 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryStdOutLogIntegration : SentryBaseIntegration // Only for testing -- (instancetype)initWithDispatchFactory:(SentryDispatchFactory *)dispatchFactory - logger:(nullable SentryLogger *)logger; +- (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue + logger:(SentryLogger *)logger; @end diff --git a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift index bd315a9ff3f..1df91db5b89 100644 --- a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift @@ -4,33 +4,35 @@ import XCTest class SentryStdOutLogIntegrationTests: XCTestCase { + private class TestLoggerDelegate: NSObject, SentryLoggerDelegate { + let capturedLogs = Invocations() + + func capture(log: SentryLog) { + capturedLogs.record(log) + } + } + private class Fixture { let options: Options let client: TestClient - let hub: SentryHubInternal - let batcher: TestLogBatcher + let delegate: TestLoggerDelegate let logger: SentryLogger - let dispatchFactory: TestDispatchFactory - - var testQueue: TestSentryDispatchQueueWrapper? + let 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) + delegate = TestLoggerDelegate() + logger = SentryLogger(delegate: delegate, dateProvider: TestCurrentDateProvider()) - dispatchFactory = TestDispatchFactory() - dispatchFactory.vendedUtilityQueueHandler = { [weak self] queue in - self?.testQueue = queue - } + testQueue = TestSentryDispatchQueueWrapper() + testQueue.dispatchAsyncExecutesBlock = true } func getIntegration() -> SentryStdOutLogIntegration { - return SentryStdOutLogIntegration(dispatchFactory: dispatchFactory, logger: logger) + return SentryStdOutLogIntegration(dispatchQueue: testQueue, logger: logger) } } @@ -85,7 +87,7 @@ class SentryStdOutLogIntegrationTests: XCTestCase { print("App stdout message from print") expect("Wait for stdout capture to trigger async dispatch") - let log = try XCTUnwrap(fixture.batcher.addInvocations.first) + let log = try XCTUnwrap(fixture.delegate.capturedLogs.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") @@ -102,7 +104,7 @@ class SentryStdOutLogIntegrationTests: XCTestCase { NSLog("App stderr message from NSLog") expect("Wait for stderr capture to trigger async dispatch") - let log = try XCTUnwrap(fixture.batcher.addInvocations.first) + let log = try XCTUnwrap(fixture.delegate.capturedLogs.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") @@ -116,23 +118,22 @@ class SentryStdOutLogIntegrationTests: XCTestCase { _ = integration.install(with: fixture.options) print("[Sentry] This is a Sentry internal print log message") - expect("Wait first print non-capture") - - // OSLOG-E0E93946-72CD-47A5-A9E7-13AD8B177E35 7 80 L 0 {t:1762782027.629955,tz:-60,tzDST:0,tid:0x326ba22,type:"Default",subsystem:null,category:null,offset:0x70a5d8,imgUUID:"249188A3-8F44-3D76-ACB0-0345A43EB0A3",imgPath:"/Library/Developer/CoreSimulator/Volumes/iOS_23B80/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 26.1.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation",procName:"xctest",pid:37095,uid:501} [Sentry] This is a Sentry internal NSLog log message + expect("Wait") + NSLog("[Sentry] This is a Sentry internal NSLog log message") - expect("Wait first NSLog non-capture") + expect("Wait") SentrySDKLog.error("This is a Sentry internal error message") - expect("Wait first SentrySDKLog.error non-capture") + expect("Wait") // Print another normal log to verify the integration is still working print("A normal log") expect("Wait for second normal log capture") - // Verify only 2 logs were captured (the [Sentry] log was skipped) - XCTAssertEqual(fixture.batcher.addInvocations.count, 1, "Only non-Sentry logs should be captured") + // Verify only 1 log was captured (the [Sentry] logs were skipped) + XCTAssertEqual(fixture.delegate.capturedLogs.count, 1, "Only non-Sentry logs should be captured") - let log = try XCTUnwrap(fixture.batcher.addInvocations.first) + let log = try XCTUnwrap(fixture.delegate.capturedLogs.first) XCTAssertTrue(log.body.contains("A normal log"), "Only the normal log should be captured") XCTAssertFalse(log.body.contains("[Sentry]"), "Sentry internal log should not be captured") @@ -142,19 +143,21 @@ class SentryStdOutLogIntegrationTests: XCTestCase { // Helper - private func expect(_ description: String, timeout: TimeInterval = 0.1) { - // Record the initial count of async invocations - let initialAsyncCount = fixture.testQueue?.dispatchAsyncInvocations.count ?? 0 + private func expect(_ description: String) { + // Record the initial count of async dispatch invocations + let initialAsyncCount = fixture.testQueue.dispatchAsyncInvocations.count - // Wait for the capture to trigger an async dispatch + // Wait for the log handler to be dispatched to its queue let expectation = XCTestExpectation(description: description) - let timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { _ in - if (self.fixture.testQueue?.dispatchAsyncInvocations.count ?? 0) > initialAsyncCount { + let timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in + let currentAsyncCount = self.fixture.testQueue.dispatchAsyncInvocations.count + if currentAsyncCount > initialAsyncCount { expectation.fulfill() + timer.invalidate() } } - wait(for: [expectation], timeout: timeout) + wait(for: [expectation], timeout: 1) timer.invalidate() } } From 5ea5d934854d6c59bbeec2f55a0b0d24e51fe961 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 16:37:29 +0100 Subject: [PATCH 10/16] move main implementation to swift (driver pattern) --- Sentry.xcodeproj/project.pbxproj | 14 ++- Sources/Sentry/SentryStdoutLogIntegration.m | 117 +++--------------- .../SentryStdOutLogIntegrationDriver.swift | 107 ++++++++++++++++ .../SentryStdOutLogIntegrationTests.swift | 3 - 4 files changed, 134 insertions(+), 107 deletions(-) create mode 100644 Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index e7f863511c9..b7bf0ff5d07 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -711,13 +711,14 @@ 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 */; }; + 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, ); }; }; + 92793D542ECB7509007EA926 /* SentryStdOutLogIntegrationDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92793D522ECB7509007EA926 /* SentryStdOutLogIntegrationDriver.swift */; }; 927A5CC42DD7626B00B82404 /* SentryEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */; }; 928207C42E251B8F009285A4 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */; }; 9286059529A5096600F96038 /* SentryGeo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9286059429A5096600F96038 /* SentryGeo.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -2083,6 +2084,7 @@ 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 = ""; }; + 92793D522ECB7509007EA926 /* SentryStdOutLogIntegrationDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStdOutLogIntegrationDriver.swift; sourceTree = ""; }; 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemHeaderTests.swift; sourceTree = ""; }; 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = ""; }; 9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = ""; }; @@ -4213,6 +4215,14 @@ name = StdOutLog; sourceTree = ""; }; + 92793D532ECB7509007EA926 /* StdOutLog */ = { + isa = PBXGroup; + children = ( + 92793D522ECB7509007EA926 /* SentryStdOutLogIntegrationDriver.swift */, + ); + path = StdOutLog; + sourceTree = ""; + }; 9292AA712EA1110E005DF5E2 /* StdOutLog */ = { isa = PBXGroup; children = ( @@ -4864,6 +4874,7 @@ D8CAC02D2BA0663E00E38F34 /* Integrations */ = { isa = PBXGroup; children = ( + 92793D532ECB7509007EA926 /* StdOutLog */, FAB0073C2E9F47DE001C806A /* Session */, FAE579B42E7DBE9400B710F9 /* SentryGlobalEventProcessor.swift */, D49064862DFAE1B700555785 /* Screenshot */, @@ -6185,6 +6196,7 @@ 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, 6292585F2DAFA8290049388F /* SentryCrashMach-O.c in Sources */, 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */, + 92793D542ECB7509007EA926 /* SentryStdOutLogIntegrationDriver.swift in Sources */, 63FE712520DA4C1000CDBAE8 /* SentryCrashSignalInfo.c in Sources */, 63FE70F320DA4C1000CDBAE8 /* SentryCrashMonitor_Signal.c in Sources */, D859696F27BECDA20036A46E /* SentryCoreDataTracker.m in Sources */, diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index b5d5181f354..50f9fbb41d2 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -1,18 +1,10 @@ #import "SentryStdOutLogIntegration.h" -#import "SentryLogC.h" +#import "SentrySDK+Private.h" #import "SentrySwift.h" -#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) SentryLogger *logger; -@property (strong, nonatomic) SentryDispatchQueueWrapper *dispatchQueue; +@property (strong, nonatomic) SentryStdOutLogIntegrationDriver *driver; @end @@ -23,8 +15,8 @@ - (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueu logger:(SentryLogger *)logger { if (self = [super init]) { - self.logger = logger; - self.dispatchQueue = dispatchQueue; + _driver = [[SentryStdOutLogIntegrationDriver alloc] initWithDispatchQueue:dispatchQueue + logger:logger]; } return self; } @@ -40,104 +32,23 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } - __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) { - // Skip logs from Sentry itself to avoid infinite loops - if ([logString containsString:@"[Sentry]"]) { - return; - } - - NSDictionary *attributes = - @{ @"sentry.log.source" : isStderr ? @"stderr" : @"stdout" }; - if (isStderr) { - [strongSelf.logger warn:logString attributes:attributes]; - } else { - [strongSelf.logger info:logString attributes:attributes]; - } - } - } - }; + // Use default instances if driver wasn't initialized (for production use) + if (!_driver) { + SentryLogger *logger = SentrySDK.logger; + SentryDispatchQueueWrapper *dispatchQueue + = SentryDependencyContainer.sharedInstance.dispatchQueueWrapper; + _driver = [[SentryStdOutLogIntegrationDriver alloc] initWithDispatchQueue:dispatchQueue + logger:logger]; + } - [self start]; + [_driver 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.stdOutPipe = nil; - - self.stdErrPipe.fileHandleForReading.readabilityHandler = 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; - pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) { - NSData *data = handle.availableData; - if (weakSelf.logHandler) { - [weakSelf.dispatchQueue - dispatchAsyncWithBlock:^{ weakSelf.logHandler(data, isStderr); }]; - } - [newFileHandle writeData:data]; - }; - - return pipe; + [_driver stop]; } @end diff --git a/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift b/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift new file mode 100644 index 00000000000..a980c537fc5 --- /dev/null +++ b/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift @@ -0,0 +1,107 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +/** + * Driver class for capturing stdout and stderr output and forwarding it to Sentry logs. + * This is used by SentryStdOutLogIntegration. + */ +@objc @_spi(Private) public class SentryStdOutLogIntegrationDriver: NSObject { + private var stdErrPipe: Pipe? + private var stdOutPipe: Pipe? + private var originalStdOut: Int32 = -1 + private var originalStdErr: Int32 = -1 + + private let logger: SentryLogger + private let dispatchQueue: SentryDispatchQueueWrapper + + @objc(initWithDispatchQueue:logger:) + @_spi(Private) public init(dispatchQueue: SentryDispatchQueueWrapper, logger: SentryLogger) { + self.dispatchQueue = dispatchQueue + self.logger = logger + super.init() + } + + @objc @_spi(Private) public func start() { + originalStdOut = dup(fileno(stdout)) + originalStdErr = dup(fileno(stderr)) + + stdOutPipe = duplicateFileDescriptor(fileno(stdout), isStderr: false) + stdErrPipe = duplicateFileDescriptor(fileno(stderr), isStderr: true) + } + + @objc @_spi(Private) public func stop() { + guard stdOutPipe != nil || stdErrPipe != nil else { + return + } + + // Restore original file descriptors + if originalStdOut >= 0 { + fflush(stdout) + dup2(originalStdOut, fileno(stdout)) + close(originalStdOut) + originalStdOut = -1 + } + + if originalStdErr >= 0 { + fflush(stderr) + dup2(originalStdErr, fileno(stderr)) + close(originalStdErr) + originalStdErr = -1 + } + + // Clean up pipes + stdOutPipe?.fileHandleForReading.readabilityHandler = nil + stdOutPipe = nil + + stdErrPipe?.fileHandleForReading.readabilityHandler = nil + stdErrPipe = nil + } + + /// 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. + private func duplicateFileDescriptor(_ fileDescriptor: Int32, isStderr: Bool) -> Pipe? { + let pipe = Pipe() + let newDescriptor = dup(fileDescriptor) + let newFileHandle = FileHandle(fileDescriptor: newDescriptor, closeOnDealloc: true) + + if dup2(pipe.fileHandleForWriting.fileDescriptor, fileDescriptor) < 0 { + SentrySDKLog.error("Unable to duplicate file descriptor \(fileDescriptor)") + close(newDescriptor) + return nil + } + + pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in + guard let self = self else { return } + + let data = handle.availableData + self.dispatchQueue.dispatchAsync { + self.handleLogData(data, isStderr: isStderr) + } + newFileHandle.write(data) + } + + return pipe + } + + private func handleLogData(_ data: Data, isStderr: Bool) { + guard data.count > 0, + let logString = String(data: data, encoding: .utf8) else { + return + } + + // Skip logs from Sentry itself to avoid infinite loops + if logString.contains("[Sentry]") { + return + } + + let attributes: [String: Any] = [ + "sentry.log.source": isStderr ? "stderr" : "stdout" + ] + + if isStderr { + logger.warn(logString, attributes: attributes) + } else { + logger.info(logString, attributes: attributes) + } + } +} diff --git a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift index 1df91db5b89..80eabf2a97f 100644 --- a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift @@ -123,9 +123,6 @@ class SentryStdOutLogIntegrationTests: XCTestCase { NSLog("[Sentry] This is a Sentry internal NSLog log message") expect("Wait") - SentrySDKLog.error("This is a Sentry internal error message") - expect("Wait") - // Print another normal log to verify the integration is still working print("A normal log") expect("Wait for second normal log capture") From 441a879592aac9fe73792b87d36fa85f99e3f92d Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 17:49:37 +0100 Subject: [PATCH 11/16] re-direct SentrySDKLog to default stdout --- Sources/Swift/Core/Tools/SentrySDKLog.swift | 9 ++--- .../SentryStdOutLogIntegrationDriver.swift | 35 ++++++++++++++++--- .../SentryStdOutLogIntegrationTests.swift | 7 +--- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Sources/Swift/Core/Tools/SentrySDKLog.swift b/Sources/Swift/Core/Tools/SentrySDKLog.swift index 7c47b160662..173dcbf62a5 100644 --- a/Sources/Swift/Core/Tools/SentrySDKLog.swift +++ b/Sources/Swift/Core/Tools/SentrySDKLog.swift @@ -1,6 +1,6 @@ import Foundation -typealias SentryLogOutput = ((String) -> Void) +@_spi(Private) public typealias SentryLogOutput = ((String) -> Void) /// A note on the thread safety: /// The methods configure and log don't use synchronization mechanisms, meaning they aren't strictly speaking thread-safe. @@ -57,12 +57,13 @@ typealias SentryLogOutput = ((String) -> Void) return isDebug && level.rawValue >= diagnosticLevel.rawValue } - #if SENTRY_TEST || SENTRY_TEST_CI - - static func setOutput(_ output: @escaping SentryLogOutput) { + /// Set a custom output function for logs. Used by integrations to redirect output. + @objc @_spi(Private) public static func setOutput(_ output: @escaping SentryLogOutput) { logOutput = output } + #if SENTRY_TEST || SENTRY_TEST_CI + static func getOutput() -> SentryLogOutput { return logOutput } diff --git a/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift b/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift index a980c537fc5..7a63de3cd96 100644 --- a/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift +++ b/Sources/Swift/Integrations/StdOutLog/SentryStdOutLogIntegrationDriver.swift @@ -25,16 +25,25 @@ import Foundation originalStdOut = dup(fileno(stdout)) originalStdErr = dup(fileno(stderr)) + configureSentrySDKLogToBypassPipe() + stdOutPipe = duplicateFileDescriptor(fileno(stdout), isStderr: false) stdErrPipe = duplicateFileDescriptor(fileno(stderr), isStderr: true) } @objc @_spi(Private) public func stop() { + // Restore SDK log print output + + SentrySDKLog.setOutput { + print($0) + } + guard stdOutPipe != nil || stdErrPipe != nil else { return } // Restore original file descriptors + if originalStdOut >= 0 { fflush(stdout) dup2(originalStdOut, fileno(stdout)) @@ -50,6 +59,7 @@ import Foundation } // Clean up pipes + stdOutPipe?.fileHandleForReading.readabilityHandler = nil stdOutPipe = nil @@ -83,17 +93,32 @@ import Foundation return pipe } + // This way we do not produce loops by using SentrySDKLog during stdout log capture. + private func configureSentrySDKLogToBypassPipe() { + let fd = originalStdOut + + SentrySDKLog.setOutput { message in + guard fd >= 0 else { return } + + // Append newline to match print() behavior + let messageWithNewline = message + "\n" + guard let data = messageWithNewline.data(using: .utf8) else { + return + } + data.withUnsafeBytes { bytes in + if let baseAddress = bytes.baseAddress { + write(fd, baseAddress, data.count) + } + } + } + } + private func handleLogData(_ data: Data, isStderr: Bool) { guard data.count > 0, let logString = String(data: data, encoding: .utf8) else { return } - // Skip logs from Sentry itself to avoid infinite loops - if logString.contains("[Sentry]") { - return - } - let attributes: [String: Any] = [ "sentry.log.source": isStderr ? "stderr" : "stdout" ] diff --git a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift index 80eabf2a97f..c09e4c9d3f9 100644 --- a/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/StdOutLog/SentryStdOutLogIntegrationTests.swift @@ -117,13 +117,8 @@ class SentryStdOutLogIntegrationTests: XCTestCase { let integration = fixture.getIntegration() _ = integration.install(with: fixture.options) - print("[Sentry] This is a Sentry internal print log message") - expect("Wait") + SentrySDKLog.error("This is a internal SentrySDK NSLog log message") - NSLog("[Sentry] This is a Sentry internal NSLog log message") - expect("Wait") - - // Print another normal log to verify the integration is still working print("A normal log") expect("Wait for second normal log capture") From 02ffb6c893642b17cb82fddb607f4b9f3174b31a Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 17:51:32 +0100 Subject: [PATCH 12/16] revert vendedUtilityQueueHandler --- SentryTestUtils/Sources/TestDispatchFactory.swift | 5 +---- Sources/Sentry/include/SentryStdOutLogIntegration.h | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/SentryTestUtils/Sources/TestDispatchFactory.swift b/SentryTestUtils/Sources/TestDispatchFactory.swift index d62b1526ab9..f2eebe049e7 100644 --- a/SentryTestUtils/Sources/TestDispatchFactory.swift +++ b/SentryTestUtils/Sources/TestDispatchFactory.swift @@ -5,7 +5,6 @@ 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)>() @@ -19,9 +18,7 @@ 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. - let queue = TestSentryDispatchQueueWrapper(name: name, attributes: nil) - vendedUtilityQueueHandler?(queue) - return queue + return TestSentryDispatchQueueWrapper(name: name, attributes: nil) } 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/include/SentryStdOutLogIntegration.h b/Sources/Sentry/include/SentryStdOutLogIntegration.h index 48b1b6700cb..7b64f57cdfd 100644 --- a/Sources/Sentry/include/SentryStdOutLogIntegration.h +++ b/Sources/Sentry/include/SentryStdOutLogIntegration.h @@ -7,7 +7,6 @@ NS_ASSUME_NONNULL_BEGIN /** * 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 From 87239f4fe99c82c2b35dca39b7ea3d51bc0daed1 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 17:56:16 +0100 Subject: [PATCH 13/16] update changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e200b1802..a75d19ee392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Structured Logs: Collect `stdout/stderr` (#6441) + ## 9.0.0-rc.0 ### Breaking Changes @@ -76,10 +80,6 @@ ## 9.0.0-alpha.1 -## Features - -- Structured Logs: Collect `stdout/stderr` per default (#6441) - ### Breaking Changes - Bumped minimum OS versions to iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, and watchOS 8.0 From 78af4be2c50d02bcc26d4de8a8bd36b0ca1eb004 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 17:59:52 +0100 Subject: [PATCH 14/16] fix cl merge --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a75d19ee392..af682ea2d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -175,11 +175,6 @@ - Removes `enablePerformanceV2` option and makes this the default. The app start duration will now finish when the first frame is drawn instead of when the OS posts the UIWindowDidBecomeVisibleNotification. (#6008) - Removes enableTracing property from SentryOptions (#5694) - Structured Logs: Move options out of experimental (#6359) - -### Features - -- Add SentryDistribution as Swift Package Manager target (#6149) -- Add option `enablePropagateTraceparent` to support OTel/W3C trace propagation (#6356) - Remove unused `SentryFrame.instruction` property (#6504) - Remove `uuid` and `name` of `SentryDebugMeta` (#6512) Use `debugID` instead of `uuid` and `codeFile` instead of `name`. - Enable enablePreWarmedAppStartTracing by default (#6508). With this option enabled, the SDK collects [prewarmed app starts](https://docs.sentry.io/platforms/apple/tracing/instrumentation/automatic-instrumentation/#prewarmed-app-start-tracing). From ff3a6ca7c93c3e2ba2823611ff90627c11c8dd3e Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 17 Nov 2025 18:02:30 +0100 Subject: [PATCH 15/16] make driver nullable --- Sources/Sentry/SentryStdoutLogIntegration.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index 50f9fbb41d2..6efcb435a41 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -4,7 +4,7 @@ @interface SentryStdOutLogIntegration () -@property (strong, nonatomic) SentryStdOutLogIntegrationDriver *driver; +@property (strong, nonatomic, nullable) SentryStdOutLogIntegrationDriver *driver; @end From 0169ba4d14114581ffdfbb998039a8fef13bc47c Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Nov 2025 11:55:55 +0100 Subject: [PATCH 16/16] Add experimental log capture --- .../SentrySampleShared/SentrySDKWrapper.swift | 5 +++++ Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 1 + Sources/Sentry/SentrySDKInternal.m | 3 ++- Sources/Sentry/SentryStdoutLogIntegration.m | 5 +++++ Sources/Swift/SentryExperimentalOptions.swift | 11 +++++++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift index 04199a023c5..24dae66cfb7 100644 --- a/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift +++ b/Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift @@ -157,6 +157,11 @@ public struct SentrySDKWrapper { #endif // !os(macOS) && !os(tvOS) && !os(watchOS) && !os(visionOS) options.enableLogs = true + options.experimental.enableStdOutCapture = true + options.beforeSendLog = { log in + print("foo") + return log + } // Experimental features options.enableFileManagerSwizzling = !SentrySDKOverrides.Other.disableFileManagerSwizzling.boolValue diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index b685e071d6a..c3a11007935 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -23,6 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } SentrySDKWrapper.shared.startSentry() + SampleAppDebugMenu.shared.display() metricKit.receiveReports() diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index 5707f7d0738..e89590ae51b 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -22,6 +22,7 @@ #import "SentryScope.h" #import "SentrySerialization.h" #import "SentrySessionReplayIntegration.h" +#import "SentryStdOutLogIntegration.h" #import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" #import "SentryTransactionContext.h" @@ -539,7 +540,7 @@ + (void)endSession [SentryANRTrackingIntegration class], [SentryAutoBreadcrumbTrackingIntegration class], [SentryAutoSessionTrackingIntegration class], [SentryCoreDataTrackingIntegration class], [SentryFileIOTrackingIntegration class], [SentryNetworkTrackingIntegration class], - [SentrySwiftAsyncIntegration class], nil]; + [SentrySwiftAsyncIntegration class], [SentryStdOutLogIntegration class], nil]; #if TARGET_OS_IOS && SENTRY_HAS_UIKIT [defaultIntegrations addObject:[SentryUserFeedbackIntegration class]]; diff --git a/Sources/Sentry/SentryStdoutLogIntegration.m b/Sources/Sentry/SentryStdoutLogIntegration.m index 6efcb435a41..0273e64efec 100644 --- a/Sources/Sentry/SentryStdoutLogIntegration.m +++ b/Sources/Sentry/SentryStdoutLogIntegration.m @@ -32,6 +32,11 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } + // Only install if experimental flag is enabled + if (!options.experimental.enableStdOutCapture) { + return NO; + } + // Use default instances if driver wasn't initialized (for production use) if (!_driver) { SentryLogger *logger = SentrySDK.logger; diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index 63aca7cbf61..85de085e5ea 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -29,6 +29,17 @@ public final class SentryExperimentalOptions: NSObject { */ public var enableSessionReplayInUnreliableEnvironment = false + /** + * Enables capturing stdout and stderr output and forwarding it to Sentry logs. + * + * When enabled, the SDK will capture all output written to stdout and stderr (including print statements, + * NSLog calls, etc.) and forward them as structured logs to Sentry. + * + * - Note: This requires `SentryOptions.enableLogs` to be set to `true`. + * - Experiment: This is an experimental feature and is therefore disabled by default. + */ + public var enableStdOutCapture = false + @_spi(Private) public func validateOptions(_ options: [String: Any]?) { } }