Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

- 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)

### Fixes

Expand Down
28 changes: 28 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -2077,6 +2080,9 @@
92235CAB2E15369900865983 /* SentryLogBatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogBatcher.swift; sourceTree = "<group>"; };
92235CAD2E15549C00865983 /* SentryLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogger.swift; sourceTree = "<group>"; };
92235CAF2E155B2600865983 /* SentryLoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLoggerTests.swift; sourceTree = "<group>"; };
9229D1452E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStdOutLogIntegrationTests.swift; sourceTree = "<group>"; };
925B67CB2EA11970005B2D3B /* SentryStdOutLogIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStdOutLogIntegration.h; path = include/SentryStdOutLogIntegration.h; sourceTree = "<group>"; };
925B67CF2EA11B0E005B2D3B /* SentryStdoutLogIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryStdoutLogIntegration.m; sourceTree = "<group>"; };
9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = "<group>"; };
9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = "<group>"; };
92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2918,6 +2924,7 @@
D85596EF280580BE0041FF8B /* Screenshot */,
0A9BF4E028A114690068D266 /* ViewHierarchy */,
D80CD8D52B752FD9002F710B /* SessionReplay */,
925B67892EA118EA005B2D3B /* StdOutLog */,
FA034AC72DD3DB4900FE3107 /* SentryIntegrationProtocol.h */,
7BA235622600B61200E12865 /* SentryInternalNotificationNames.h */,
0A2D8D5C289815EB008720F6 /* SentryBaseIntegration.h */,
Expand Down Expand Up @@ -3515,6 +3522,7 @@
7BE0DC3F272AE9F0004FA8B7 /* Session */,
7BE0DC3E272AE9DC004FA8B7 /* SentryCrash */,
D80694C12B7CC85800B820E6 /* SessionReplay */,
9292AA712EA1110E005DF5E2 /* StdOutLog */,
7B59398324AB481B0003AAD2 /* NotificationCenterTestCase.swift */,
0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */,
);
Expand Down Expand Up @@ -4222,6 +4230,23 @@
name = Transaction;
sourceTree = "<group>";
};
925B67892EA118EA005B2D3B /* StdOutLog */ = {
isa = PBXGroup;
children = (
925B67CB2EA11970005B2D3B /* SentryStdOutLogIntegration.h */,
925B67CF2EA11B0E005B2D3B /* SentryStdoutLogIntegration.m */,
);
name = StdOutLog;
sourceTree = "<group>";
};
9292AA712EA1110E005DF5E2 /* StdOutLog */ = {
isa = PBXGroup;
children = (
9229D1452E9FF09E00FD09ED /* SentryStdOutLogIntegrationTests.swift */,
);
path = StdOutLog;
sourceTree = "<group>";
};
D4009EA02D77196F0007AF30 /* ViewCapture */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5205,6 +5230,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 */,
Expand Down Expand Up @@ -5676,6 +5702,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 */,
Expand Down Expand Up @@ -6122,6 +6149,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 */,
Expand Down
5 changes: 4 additions & 1 deletion SentryTestUtils/TestDispatchFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)>()

Expand All @@ -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<CChar>, attributes: __OS_dispatch_queue_attr, eventHandler: @escaping () -> Void) -> SentryDispatchSourceWrapper {
Expand Down
171 changes: 171 additions & 0 deletions Sources/Sentry/SentryStdoutLogIntegration.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#import "SentryStdOutLogIntegration.h"
#import "SentryDependencyContainer.h"
#import "SentryLogC.h"
#import "SentryOptions.h"
#import "SentrySwift.h"
#import <Foundation/Foundation.h>
#import <stdatomic.h>

@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
3 changes: 2 additions & 1 deletion Sources/Sentry/SentyOptionsInternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import "SentryOptions.h"
#import "SentryOptionsInternal.h"
#import "SentrySessionReplayIntegration.h"
#import "SentryStdoutLogIntegration.h"

Check warning on line 15 in Sources/Sentry/SentyOptionsInternal.m

View workflow job for this annotation

GitHub Actions / Build with SPM

non-portable path to file '"SentryStdOutLogIntegration.h"'; specified path differs in case from file name on disk [-Wnonportable-include-path]

Check warning on line 15 in Sources/Sentry/SentyOptionsInternal.m

View workflow job for this annotation

GitHub Actions / Build with SPM

non-portable path to file '"SentryStdOutLogIntegration.h"'; specified path differs in case from file name on disk [-Wnonportable-include-path]

Check warning on line 15 in Sources/Sentry/SentyOptionsInternal.m

View workflow job for this annotation

GitHub Actions / Build with SPM

non-portable path to file '"SentryStdOutLogIntegration.h"'; specified path differs in case from file name on disk [-Wnonportable-include-path]

Check warning on line 15 in Sources/Sentry/SentyOptionsInternal.m

View workflow job for this annotation

GitHub Actions / Build with SPM

non-portable path to file '"SentryStdOutLogIntegration.h"'; specified path differs in case from file name on disk [-Wnonportable-include-path]
#import "SentrySwift.h"
#import "SentrySwiftAsyncIntegration.h"

Expand Down Expand Up @@ -54,7 +55,7 @@
[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]];
Expand Down
21 changes: 21 additions & 0 deletions Sources/Sentry/include/SentryStdOutLogIntegration.h
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading