Skip to content

Commit 3d1ca9c

Browse files
committed
Changed the log collector to use a pipe and added a unit test
1 parent 3865382 commit 3d1ca9c

File tree

3 files changed

+145
-80
lines changed

3 files changed

+145
-80
lines changed

Agent/Utilities/NRAutoLogCollector.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@
1515

1616
+ (void) redirectStandardOutputAndError;
1717
+ (void) restoreStandardOutputAndError;
18-
+ (void) readAndParseLogFile;
1918

2019
@end

Agent/Utilities/NRAutoLogCollector.m

Lines changed: 58 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -11,81 +11,84 @@
1111

1212
int saved_stdout;
1313
int saved_stderr;
14-
FILE* fileDescriptor;
14+
int stdoutPipe[2];
15+
int stderrPipe[2];
1516

1617
@interface NRAutoLogCollector()
1718

1819
@end
1920

2021
@implementation NRAutoLogCollector
2122

22-
+ (NSURL *) logFileURL {
23-
NSFileManager *fileManager = [NSFileManager defaultManager];
24-
NSArray<NSURL *> *urls = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
25-
NSURL *logsDirectory = [urls firstObject];
26-
return [logsDirectory URLByAppendingPathComponent:@"agent.log"];
27-
}
28-
29-
+ (void) clearLogFile {
30-
[[NSFileManager defaultManager] removeItemAtURL:[NRAutoLogCollector logFileURL] error:nil];
31-
32-
[NRAutoLogCollector redirectStandardOutputAndError];
33-
}
34-
3523
+ (void) redirectStandardOutputAndError {
36-
// Save the original stdout file descriptor
24+
// Create pipes for stdout and stderr
25+
if (pipe(stdoutPipe) == -1 || pipe(stderrPipe) == -1) {
26+
return;
27+
}
28+
29+
// Save the original stdout and stderr file descriptors
3730
saved_stdout = dup(fileno(stdout));
3831
saved_stderr = dup(fileno(stderr));
32+
if (saved_stdout == -1 || saved_stderr == -1) {
33+
close(stdoutPipe[0]);
34+
close(stdoutPipe[1]);
35+
close(stderrPipe[0]);
36+
close(stderrPipe[1]);
37+
return;
38+
}
3939

40-
// Redirect stdout to the file
41-
freopen([[NRAutoLogCollector logFileURL].path cStringUsingEncoding:NSUTF8StringEncoding], "a+", stdout);
42-
fileDescriptor = freopen([[NRAutoLogCollector logFileURL].path cStringUsingEncoding:NSUTF8StringEncoding], "a+", stderr);
43-
44-
[NRAutoLogCollector monitorFile:[NRAutoLogCollector logFileURL].path];
40+
// Redirect stdout and stderr to the write ends of the pipes
41+
if (dup2(stdoutPipe[1], fileno(stdout)) == -1 || dup2(stderrPipe[1], fileno(stderr)) == -1) {
42+
close(stdoutPipe[0]);
43+
close(stdoutPipe[1]);
44+
close(stderrPipe[0]);
45+
close(stderrPipe[1]);
46+
close(saved_stdout);
47+
close(saved_stderr);
48+
return;
49+
}
50+
close(stdoutPipe[1]); // Close the original write end of the stdout pipe
51+
close(stderrPipe[1]); // Close the original write end of the stderr pipe
52+
53+
// Read from the pipes in background threads
54+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
55+
[NRAutoLogCollector readAndLog:stdoutPipe[0]];
56+
});
57+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
58+
[NRAutoLogCollector readAndLog:stderrPipe[0]];
59+
});
60+
61+
// Restore the original stdout and stderr when done
62+
atexit_b(^{
63+
[NRAutoLogCollector restoreStandardOutputAndError];
64+
});
65+
}
66+
67+
+ (void) readAndLog:(int) fd {
68+
char buffer[2048];
69+
ssize_t count;
70+
while ((count = read(fd, buffer, sizeof(buffer) - 1)) > 0) {
71+
buffer[count] = '\0'; // Null-terminate the string
72+
NSString *output = [NSString stringWithUTF8String:buffer];
73+
NSArray<NSString *> *newLogEntries = [output componentsSeparatedByString:@"\n\n"];
74+
75+
// Process each log entry
76+
for (NSString *logEntry in newLogEntries) {
77+
if ([logEntry length] > 0) {
78+
[NRLogger log:[NRAutoLogCollector extractType:logEntry] withMessage:logEntry withTimestamp:[NRAutoLogCollector extractTimestamp:logEntry]];
79+
}
80+
}
81+
}
82+
close(fd);
4583
}
4684

4785
+ (void) restoreStandardOutputAndError {
48-
[NRAutoLogCollector readAndParseLogFile];
49-
50-
// Restore the original stdout
5186
dup2(saved_stdout, fileno(stdout));
5287
dup2(saved_stderr, fileno(stderr));
5388
close(saved_stdout);
5489
close(saved_stderr);
5590
}
5691

57-
+ (void) readAndParseLogFile {
58-
fflush(stdout);
59-
fflush(stderr);
60-
// Check if the file exists
61-
if (![[NSFileManager defaultManager] fileExistsAtPath:[NRAutoLogCollector logFileURL].path]) {
62-
return;
63-
}
64-
65-
// Read the file content into an NSString
66-
NSError *error = nil;
67-
NSString *fileContents = [NSString stringWithContentsOfFile:[NRAutoLogCollector logFileURL].path
68-
encoding:NSUTF8StringEncoding
69-
error:&error];
70-
[NRAutoLogCollector clearLogFile];
71-
72-
if (error) {
73-
return;
74-
} else if (fileContents.length == 0){
75-
return;
76-
}
77-
78-
// Split the file contents into individual log entries
79-
NSArray<NSString *> *newLogEntries = [fileContents componentsSeparatedByString:@"\n\n"];
80-
81-
// Process each log entry
82-
for (NSString *logEntry in newLogEntries) {
83-
if ([logEntry length] > 0) {
84-
[NRLogger log:[NRAutoLogCollector extractType:logEntry] withMessage:logEntry withTimestamp:[NRAutoLogCollector extractTimestamp:logEntry]];
85-
}
86-
}
87-
}
88-
8992
+ (BOOL) isValidTimestamp:(NSString *) timestampString {
9093
// Check if the timestamp string can be converted to a double
9194
double timestamp = [timestampString doubleValue];
@@ -148,29 +151,5 @@ + (unsigned int) extractType:(NSString *) inputString {
148151

149152
return NRLogLevelNone;
150153
}
151-
152-
+ (void) monitorFile:(NSString *) filePath {
153-
// Create a dispatch queue for handling log file events
154-
dispatch_queue_t queue = dispatch_queue_create("newrelic.log.monitor.queue", NULL);
155-
156-
// Create a dispatch source to monitor the file descriptor for writes
157-
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileno(fileDescriptor), DISPATCH_VNODE_WRITE, queue);
158-
159-
// Set the event handler block
160-
dispatch_source_set_event_handler(source, ^{
161-
unsigned long flags = dispatch_source_get_data(source);
162-
if (flags & DISPATCH_VNODE_WRITE) {
163-
[NRAutoLogCollector readAndParseLogFile];
164-
}
165-
});
166-
167-
// Set the cancel handler block
168-
dispatch_source_set_cancel_handler(source, ^{
169-
close(fileno(fileDescriptor));
170-
});
171-
172-
// Start monitoring
173-
dispatch_resume(source);
174-
}
175154

176155
@end

Tests/Unit-Tests/NewRelicAgentTests/Uncategorized/NRLoggerTests.m

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#import "NRMAAppToken.h"
2323
#import "NRMAHarvestController.h"
2424
#import "NRTestConstants.h"
25+
#import "NRAutoLogCollector.h"
26+
#import <os/log.h>
2527

2628
@implementation NRLoggerTests
2729
- (void) setUp
@@ -253,4 +255,89 @@ - (void) testRemoteLogLevels {
253255
XCTAssertEqual(foundCount, 3, @"Three remote messages should be found.");
254256
}
255257

258+
- (void) testAutoCollectedLogs {
259+
[NRMAFlags enableFeatures: NRFeatureFlag_RedirectStdOutStdErr];
260+
// Set the remote log level to Info.
261+
[NRLogger setRemoteLogLevel:NRLogLevelDebug];
262+
[NRAutoLogCollector redirectStandardOutputAndError];
263+
264+
XCTestExpectation *delayExpectation1 = [self expectationWithDescription:@"Waiting for Log Queue"];
265+
266+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
267+
[delayExpectation1 fulfill];
268+
});
269+
270+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
271+
if (error) {
272+
XCTFail(@"Timeout error");
273+
}
274+
}];
275+
276+
// Three messages should reach the remote log file for upload.
277+
NSLog(@"NSLog Test");
278+
os_log_t customLog = os_log_create("com.agent.tests", "logTest");
279+
// Log messages at different levels
280+
os_log(customLog, "This is a default os_log message.");
281+
os_log_info(customLog, "This is an info os_log message.");
282+
os_log_debug(customLog, "This is a debug os_log message.");
283+
os_log_error(customLog, "This is an error os_log message.");
284+
os_log_fault(customLog, "This is a fault os_log message.");
285+
286+
XCTestExpectation *delayExpectation2 = [self expectationWithDescription:@"Waiting for Log Queue"];
287+
288+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
289+
[delayExpectation2 fulfill];
290+
});
291+
292+
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
293+
if (error) {
294+
XCTFail(@"Timeout error");
295+
}
296+
}];
297+
298+
NSError* error;
299+
NSData* logData = [NRLogger logFileData:&error];
300+
if(error){
301+
NSLog(@"%@", error.localizedDescription);
302+
}
303+
NSString* logMessagesJson = [NSString stringWithFormat:@"[ %@ ]", [[NSString alloc] initWithData:logData encoding:NSUTF8StringEncoding]];
304+
NSData* formattedData = [logMessagesJson dataUsingEncoding:NSUTF8StringEncoding];
305+
NSArray* decode = [NSJSONSerialization JSONObjectWithData:formattedData
306+
options:0
307+
error:nil];
308+
NSLog(@"decode=%@", decode);
309+
310+
NSArray * expectedValues = @[
311+
@{@"message": @"NSLog Test"},
312+
@{@"message": @"This is a default os_log message."},
313+
@{@"message": @"This is an info os_log message."},
314+
@{@"message": @"This is a debug os_log message."},
315+
@{@"message": @"This is an error os_log message."},
316+
@{@"message": @"This is a fault os_log message."},
317+
];
318+
// check for existence of 6 logs.
319+
int foundCount = 0;
320+
// For each expected message.
321+
for (NSDictionary *dict in expectedValues) {
322+
// Iterate through the collected message logs.
323+
for (NSDictionary *dict2 in decode) {
324+
//
325+
NSString* currentMessage = [dict objectForKey:@"message"];
326+
if ([[dict2 objectForKey:@"message"] containsString: currentMessage]) {
327+
foundCount += 1;
328+
XCTAssertTrue([[dict2 objectForKey:@"entity.guid"] isEqualToString:@"Entity-Guid-XXXX"],@"entity.guid set incorrectly");
329+
}
330+
// Verify added attributes with logAttributes.
331+
if ([[dict2 objectForKey:@"message"] isEqualToString:@"This is a test message for the New Relic logging system."]) {
332+
XCTAssertTrue([[dict2 objectForKey:@"additionalAttribute1"] isEqualToString:@"attribute1"],@"additionalAttribute1 set incorrectly");
333+
XCTAssertTrue([[dict2 objectForKey:@"additionalAttribute2"] isEqualToString:@"attribute2"],@"additionalAttribute2 set incorrectly");
334+
}
335+
}
336+
}
337+
338+
XCTAssertEqual(foundCount, 6, @"Three remote messages should be found.");
339+
[NRAutoLogCollector restoreStandardOutputAndError];
340+
[NRMAFlags disableFeatures: NRFeatureFlag_RedirectStdOutStdErr];
341+
}
342+
256343
@end

0 commit comments

Comments
 (0)