Sources/CrashReporter/SLSCrashReporterFeature.m (385 lines of code) (raw):
//
// SLSCrashReporterFeature.m
// AliyunLogProducer
//
// Created by gordon on 2022/7/20.
//
#import "SLSSystemCapabilities.h"
#import "SLSCrashReporterFeature.h"
#import "WPKMobi/WPKSetup.h"
#import "WPKMobi/WPKThreadBlockChecker.h"
#import "SLSUtdid.h"
#import "NSDateFormatter+SLS.h"
#import "SLSCrashReporter.h"
#import "SLSProducer.h"
typedef void(^directory_changed_block)(NSString *);
@interface SLSLastCachedSpan : SLSSpan
+ (SLSLastCachedSpan *) cachedSpan: (SLSSpan *) span;
@end
@implementation SLSLastCachedSpan
+ (SLSLastCachedSpan *) cachedSpan: (SLSSpan *) span {
SLSLastCachedSpan *cachedSpan = [[SLSLastCachedSpan alloc] init];
[cachedSpan setTraceID:span.traceID];
if (span.parentSpanID.length > 0) {
[cachedSpan setSpanID:span.parentSpanID];
}
return cachedSpan;
}
@end
@interface SLSCrashReporterFeature ()<WPKThreadBlockCheckerDelegate>
@property(nonatomic, strong) NSString *wpkStatLogPath;
@property(nonatomic, strong) NSString *wpkCrashLogPath;
@property(nonatomic, strong) dispatch_source_t crashLogSource;
@property(nonatomic, strong) dispatch_source_t crashStatLogSource;
@property(nonatomic, copy) NSString *project;
//@property(nonatomic, strong) SLSConfiguration *configuration;
- (void) observeDirectoryChanged;
- (void) initWPKMobi: (SLSCredentials *) credentials configuration: (SLSConfiguration *) configuration;
- (NSString *) getAppIdByInstanceId: (NSString *) instanceId;
- (void) reportState;
- (void) reportCrash;
- (void) reportState: (NSString *) file;
- (void) reportCrash: (NSString *) file;
@end
@implementation SLSCrashReporterFeature
#pragma mark - init
- (NSString *)name {
return @"crash_reporter";
}
- (void)setCredentials:(SLSCredentials *)credentials {
if (nil == credentials) {
return;
}
if ([credentials.project length] > 0) {
_project = [credentials.project copy];
}
}
- (void) onInitializeSender: (SLSCredentials *) credentials configuration: (SLSConfiguration *) configuration {
[super onInitializeSender:credentials configuration:configuration];
}
- (void)onPreInit:(SLSCredentials *)credentials configuration:(SLSConfiguration *)configuration {
[super onPreInit:credentials configuration:configuration];
_project = credentials.project;
[self observeDirectoryChanged];
[self initWPKMobi: credentials configuration:configuration];
[[SLSCrashReporter sharedInstance] setCrashReporterFeature:self];
}
- (void) onInitialize: (SLSCredentials *) credentials configuration: (SLSConfiguration *) configuration {
[super onInitialize:credentials configuration:configuration];
}
- (void) onPostInitialize {
[super onPostInitialize];
}
- (void) onStop {
[super onStop];
[self stopLogDirectoryMonitor];
}
- (void) onPostStop {
[super onPostStop];
}
- (void) initWPKMobi: (SLSCredentials *) credentials configuration: (SLSConfiguration *) configuration {
if (configuration.enableCrashReporter) {
[WPKSetup setIsEncryptLog:NO];
[WPKSetup enableDebugLog:NO];
[WPKSetup startWithAppName:[self getAppIdByInstanceId:credentials.instanceId]];
}
if (configuration.enableBlockDetection) {
WPKThreadBlockChecker *blockChecker = [WPKSetup threadBlockCheckerWithDelegate:self];
WPKThreadBlockCheckerConfig *blockConfig = [[WPKThreadBlockCheckerConfig alloc] init];
blockConfig.sendBeatInterval = 3;
blockConfig.checkBeatInterval = 3;
blockConfig.toleranceBeatMissingCount = 2;
[blockChecker startWithConfig:blockConfig];
}
[WPKSetup sendAllReports];
}
- (NSString *) getAppIdByInstanceId: (NSString *) instanceId {
return [NSString stringWithFormat:@"sls-%@", instanceId];
}
- (void) observeDirectoryChanged {
#if SLS_HOST_TV
NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
#else
NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;
#endif
SLSLogV(@"start observe directory changed start. library path: %@", libraryPath);
NSString *wpkLogpath = [libraryPath stringByAppendingPathComponent:@".WPKLog"];
if (![self checkAndCreateDirectory:wpkLogpath]) {
SLSLog(@"create wpklog directory fail.");
return;
}
_wpkCrashLogPath = [wpkLogpath stringByAppendingPathComponent:@"CrashLog"];
if (![self checkAndCreateDirectory:_wpkCrashLogPath]) {
SLSLog(@"create CrashLog directory fail.");
return;
}
_wpkStatLogPath = [wpkLogpath stringByAppendingPathComponent:@"CrashStatLog"];
if (![self checkAndCreateDirectory:_wpkStatLogPath]) {
SLSLog(@"create CrashStatLog directory fail.");
return;
}
// report old state & crash file first
[self reportState];
[self reportCrash];
observeDirectory(self.crashLogSource, self.wpkCrashLogPath, ^(NSString *path) {
[self reportCrash];
});
observeDirectory(self.crashStatLogSource, self.wpkStatLogPath, ^(NSString *path) {
[self reportState];
});
SLSLogV(@"observe directory changed end. ");
}
- (BOOL) checkAndCreateDirectory: (NSString*) dir {
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:dir]) {
SLSLogV(@"%@ path not exists.", dir);
BOOL res = [fileManager createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:nil];
if (!res) {
SLSLog(@"create directory %@ error.", dir);
}
return res;
}
return YES;
}
- (void) stopLogDirectoryMonitor {
dispatch_cancel(self.crashLogSource);
dispatch_cancel(self.crashStatLogSource);
}
static void observeDirectory(dispatch_source_t _source, NSString *path, directory_changed_block hander) {
NSURL *dirURL = [NSURL URLWithString:path];
int const fd = open([[dirURL path]fileSystemRepresentation], O_EVTONLY);
if (fd < 0) {
SLSLog(@"SLSCrashReporterFeature, unable to open the path: %@", [dirURL path]);
return;
}
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fd, DISPATCH_VNODE_WRITE, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_event_handler(source, ^() {
unsigned long const type = dispatch_source_get_data(source);
switch (type) {
case DISPATCH_VNODE_WRITE: {
SLSLogV(@"SLSCrashReporterFeature, directory changed. %@", path);
hander(path);
break;
}
default:
break;
}
});
dispatch_source_set_cancel_handler(source, ^{
close(fd);
});
_source = source;
dispatch_resume(_source);
}
- (void) reportState {
if (!_wpkStatLogPath || ![[NSFileManager defaultManager] fileExistsAtPath:_wpkStatLogPath isDirectory:nil]) {
return;
}
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:_wpkStatLogPath error:nil];
if (contents && contents.count > 0) {
SLSLogV(@"report existing state file. count: %lu", (unsigned long)contents.count);
for (NSString *content in contents) {
[self reportState:[_wpkStatLogPath stringByAppendingPathComponent:content]];
}
}
}
- (void) reportCrash {
if (!_wpkCrashLogPath || ![[NSFileManager defaultManager] fileExistsAtPath:_wpkCrashLogPath isDirectory:nil]) {
return;
}
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:_wpkCrashLogPath error:nil];
if (contents && contents.count > 0) {
SLSLogV(@"report existing crash file. count: %lu", (unsigned long)contents.count);
for (NSString *content in contents) {
[self reportCrash:[_wpkCrashLogPath stringByAppendingPathComponent:content]];
}
}
}
- (void) reportState: (NSString *) file {
SLSLogV(@"start report state file. file: %@", file);
if (!file || file.length <= 0 || ![[NSFileManager defaultManager] fileExistsAtPath:file isDirectory:nil]) {
return;
}
NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
if (!content || content.length <= 0) {
return;
}
NSArray *lines = [content componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
if (!lines) {
return;
}
NSString *utdid = [SLSUtdid getUtdid];
// lines 可能存在多行的情况
for (NSString *line in lines) {
if ([line containsString:@"dn"]) {
NSArray *chunks = [line componentsSeparatedByString:@"`"];
if (!chunks) {
return;
}
content = [NSMutableString string];
for (NSString *chunk in chunks) {
if ([chunk containsString:@"dn="]) {
[((NSMutableString *) content) appendFormat: @"dn=%@`", utdid];
} else if (chunk.length > 0){
[((NSMutableString *) content) appendFormat: @"%@`", chunk];
}
}
// 每一行单独上报
SLSSpanBuilder *builder = [self newSpanBuilder:@"state"];
[builder addAttribute:
[SLSAttribute of:@"t" value:@"error"],
[SLSAttribute of:@"ex.type" value:@"state"],
[SLSAttribute of:@"ex.origin" value:content],
[SLSAttribute of:@"ex.uuid" value:utdid],
nil
];
[builder setGlobal:NO];
BOOL ret = [[builder build] end];
if (ret) {
[[NSFileManager defaultManager] removeItemAtPath:file error:nil];
SLSLogV(@"report state file success.");
} else {
SLSLogV(@"report state file fail.");
}
}
}
}
- (void) reportCrash: (NSString *) file {
SLSLogV(@"start report crash file. file: %@", file);
if (!file || file.length <= 0 || ![[NSFileManager defaultManager] fileExistsAtPath:file isDirectory:nil]) {
return;
}
NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
if (!content || content.length <= 0) {
return;
}
NSArray *lines = [content componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
if (!lines) {
return;
}
NSString *time = @"";
NSString *subType = @"crash";
content = [NSMutableString string];
for (NSString *line in lines) {
if ([line containsString:@"Date/Time:"]) {
NSArray *chunks = [line componentsSeparatedByString:@"Time:"];
if (chunks && [chunks count] == 2) {
time = [chunks objectAtIndex:1];
}
}
if ([line containsString:@"UDID:"]) {
[((NSMutableString *) content) appendFormat:@"UDID: %@\n", [SLSUtdid getUtdid]];
} else {
if ([line containsString:@"k_ac:"]) {
NSArray *chunks = [line componentsSeparatedByString:@"k_ac:"];
if (nil != chunks && [chunks count] == 2) {
subType = [chunks[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
}
[((NSMutableString *) content) appendFormat:@"%@\n", line];
}
}
NSString *type = @"crash";
if ([[file pathExtension] isEqualToString:@"block"]) {
type = @"block";
}
SLSLastCachedSpan *lastCachedSpan = nil;
if (self.configuration.enableTrace) {
SLSSpan *span = [SLSContextManager getLastGlobalActiveSpan];
if (nil != span) {
lastCachedSpan = [SLSLastCachedSpan cachedSpan:span];
}
}
SLSSpanBuilder *buidler = [self newSpanBuilder:type];
[buidler addAttribute:
[SLSAttribute of:@"t" value:@"error"],
[SLSAttribute of:@"ex.type" value:type],
[SLSAttribute of:@"ex.sub_type" value:subType],
[SLSAttribute of:@"ex.origin" value:content],
[SLSAttribute of:@"ex.file" value: [file lastPathComponent]],
nil
];
if (nil != lastCachedSpan) {
[buidler setParent:lastCachedSpan];
}
if (time && time.length >0) {
time = [time stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// time format: 2021-06-09 19:32:17.341 +0800
NSDateFormatter *dateFormatter = [NSDateFormatter sharedInstance];
NSDate *date = [dateFormatter fromStringZ:time];
// tcdata.local_timestamp = [NSString stringWithFormat:@"%0.f", [date timeIntervalSince1970] * 1000];
// tcdata.local_time = [dateFormatter fromDate:date];
[buidler setStart:[date timeIntervalSince1970] * 1000000000];
} else {
time = [[NSDateFormatter sharedInstance] fromDate:[NSDate date] formatter:@"YYYY-MM-dd HH:mm:ss.SSS Z"];
}
SLSSpan *crashedSpan = [buidler build];
BOOL ret = [crashedSpan end];
if (ret) {
[[NSFileManager defaultManager] removeItemAtPath:file error:nil];
SLSLogV(@"report crash file success.");
} else {
SLSLogV(@"report crash file fail.");
}
if (self.configuration.enableTrace && [@"crash" isEqualToString:type]) {
Class clazz = NSClassFromString(@"SLSTracer");
if (!clazz) {
return;
}
NSDate *d = [[NSDateFormatter sharedInstance] fromStringZ:time];
NSString *t = [[NSDateFormatter sharedInstance] fromDate:d formatter:@"yyyyMMddHHmmss"];
// SLSSpan *span = [SLSTracer startSpan:@"Application Crashed"];
SLSSpan *span = [clazz performSelector:@selector(startSpan:) withObject:@"Application Crashed"];
[span setSpanID:crashedSpan.spanID];
[span addAttribute:
[SLSAttribute of:@"ex.file" value:[file lastPathComponent]],
[SLSAttribute of:@"ex.uuid" value:[[SLSUtdid getUtdid] copy]],
[SLSAttribute of:@"ex.project" value:_project],
[SLSAttribute of:@"ex.time" value:t],
[SLSAttribute of:@"ex.filter_time" value:[t substringToIndex:10]],
[SLSAttribute of:@"ex.filter_classify" value:@"crash"],
[SLSAttribute of:@"ex.filter_type" value:@""],
nil
];
[span setStatusCode:ERROR];
if (nil != lastCachedSpan) {
[span setParent:lastCachedSpan];
}
[span end];
}
}
#pragma mark - block
/* @brief 检测到一次卡顿
* @param blockTime 卡顿的时长
*/
- (void)onMainThreadBlockedWithBlockInterval:(NSTimeInterval)blockInterval {
SLSLogV(@"onMainThreadBlockedWithBlockInterval, block interval: %f", blockInterval);
}
/* @biref 检测持续发生卡顿(第一次卡顿后,下个检测心跳又一次触发卡顿)。可以在这里做些统计等。
*/
- (void)onMainThreadKeepOnBlocking {
SLSLogV(@"onMainThreadKeepOnBlocking");
[WPKSetup sendAllReports];
}
/* @brief 心跳正常。两种情况表示正常:1、心跳正常(主线程正常); 2、APP被置入后台。
*/
- (void)onMainThreadStayHealthy:(BOOL)mainThreadRespond {
// SLSLogV(@"onMainThreadStayHealthy");
}
/* @brief 重新启动一轮心跳检测(卡顿计数重置)。
*/
- (void)onMainThreadCheckingReset {
// SLSLogV(@"onMainThreadCheckingReset");
}
#pragma mark - setter
- (void) setFeatureEnabled: (BOOL) enable {
if (enable) {
if ([WPKSetup isWPKReporterActive]) {
return;
}
[WPKSetup activeWPKReporter];
SLSLog(@"CrashReporterFeature enabled.");
return;
} else {
if ([WPKSetup isWPKReporterActive]) {
[WPKSetup disableWPKReporter];
}
SLSLog(@"CrashReporterFeature disabled.");
}
}
#pragma mark - getter
- (BOOL) isFeatureEnabled {
return [WPKSetup isWPKReporterActive];
}
#pragma mark - report custom log
- (void) reportCustomLog: (nonnull NSString *)log type: (nonnull NSString *)type {
SLSSpanBuilder *buidler = [self newSpanBuilder:@"custom log"];
[buidler addAttribute:
[SLSAttribute of:@"t" value:@"custom"],
[SLSAttribute of:@"ex.type" value:([type length] > 0 ? type: @"log")],
[SLSAttribute of:@"ex.origin" value: ([log length] > 0 ? log : @"")],
nil
];
[[buidler build] end];
}
#pragma mark - report error
- (void) reportError: (NSString *) type level: (SLSLogLevel) level message: (NSString *) message stacktraces: (NSArray<NSString *> *) stacktraces {
[WPKSetup reportScriptException:[type copy] reason:[message copy] stackTrace:[stacktraces copy] terminateProgram:NO];
[WPKSetup sendAllReports: NO];
}
@end