123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- //
- // QNUploadInfoReporter.m
- // QiniuSDK
- //
- // Created by WorkSpace_Sun on 2019/6/24.
- // Copyright © 2019 Qiniu. All rights reserved.
- //
- #import "QNUploadInfoReporter.h"
- #import "QNUploadInfoCollector.h"
- #import "QNResponseInfo.h"
- #import "QNFile.h"
- #import "QNUpToken.h"
- #import "QNUserAgent.h"
- #import "QNAsyncRun.h"
- #import "QNSystemTool.h"
- #import "QNVersion.h"
- #import <objc/runtime.h>
- @interface QNReportBaseItem ()
- // 打点类型 request、block、quality
- @property (nonatomic, copy) NSString *log_type;
- // 客户端时间戳
- @property (nonatomic, assign) int64_t up_time;
- @end
- @implementation QNReportBaseItem
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- self.up_time = [[NSDate dateWithTimeIntervalSinceNow:0] timeIntervalSince1970];
- }
- return self;
- }
- - (NSString *)toJson {
- NSMutableDictionary *itemDic = [NSMutableDictionary dictionary];
-
- // self class property
- unsigned int selfPropertyCount = 0;
- objc_property_t *selfProperties = class_copyPropertyList([self class], &selfPropertyCount);
- for (unsigned int i = 0; i < selfPropertyCount; i ++) {
- objc_property_t property = selfProperties[i];
- const char *name = property_getName(property);
- unsigned int attrCount = 0;
- objc_property_attribute_t * attrs = property_copyAttributeList(property, &attrCount);
- for (unsigned int j = 0; j < attrCount; j ++) {
- objc_property_attribute_t attr = attrs[j];
- const char *attrName = attr.name;
- if (0 == strcmp(attrName, "T")) {
- const char *value = attr.value;
- if (0 == strcmp(value, "@\"NSString\"")) {
- NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
- NSString *ivarValue = [self valueForKey:key];
- if (ivarValue) [itemDic setValue:ivarValue forKey:key];
- } else {
- // 默认其他属性的基本类型是int
- NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
- NSNumber *ivarValue = [self valueForKey:key];
- if (ivarValue && ![ivarValue isEqualToNumber:@(QN_IntNotSet)]) [itemDic setValue:ivarValue forKey:key];
- }
- }
- }
- free(attrs);
- }
- free(selfProperties);
-
- // super class property
- unsigned int superPropertyCount = 0;
- objc_property_t *superProperties = class_copyPropertyList([self superclass], &superPropertyCount);
- for (unsigned int i = 0; i < superPropertyCount; i ++) {
- objc_property_t property = superProperties[i];
- const char *name = property_getName(property);
- unsigned int attrCount = 0;
- objc_property_attribute_t * attrs = property_copyAttributeList(property, &attrCount);
- for (unsigned int j = 0; j < attrCount; j ++) {
- objc_property_attribute_t attr = attrs[j];
- const char *attrName = attr.name;
- if (0 == strcmp(attrName, "T")) {
- const char *value = attr.value;
- if (0 == strcmp(value, "@\"NSString\"")) {
- NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
- NSString *ivarValue = [self valueForKey:key];
- if (ivarValue) [itemDic setValue:ivarValue forKey:key];
- } else {
- // 默认其他属性的基本类型是int
- NSString *key = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
- NSNumber *ivarValue = [self valueForKey:key];
- if (ivarValue) [itemDic setValue:ivarValue forKey:key];
- }
- }
- }
- free(attrs);
- }
- free(superProperties);
-
- NSError *error;
- NSData *itemData = [NSJSONSerialization dataWithJSONObject:itemDic options:kNilOptions error:&error];
- if (error) return nil;
- NSString *itemJson = [[NSString alloc] initWithData:itemData encoding:NSUTF8StringEncoding];
- return itemJson;
- }
- @end
- @interface QNReportRequestItem ()
- // API 请求类型,可选值有 "form","mkblk","bput","mkfile","put","init_parts","upload_part","complete_part","uc_query","httpdns_query"
- @property (nonatomic, copy) NSString *up_type;
- // 记录⽬标 Bucket 名称
- @property (nonatomic, copy) NSString *target_bucket;
- // 记录⽬标 Key 名称
- @property (nonatomic, copy) NSString *target_key;
- // 本次分片上传的偏移量,单位为字节
- @property (nonatomic, assign) int64_t file_offset;
- // ⽬标上传的区域 ID,可选值为 "z0","z1","z2","as0","na0" 等
- @property (nonatomic, copy) NSString *target_region_id;
- // 当前上传的区域 ID,可选值为 "z0","z1","z2","as0","na0" 等
- @property (nonatomic, copy) NSString *current_region_id;
- // 该域名通过预取 DNS 得到的 IP 地址数量(目前是0)
- @property (nonatomic, assign) int64_t prefetched_ip_count;
- // 当前进程 ID
- @property (nonatomic, assign) int64_t pid;
- // 当前线程 ID
- @property (nonatomic, assign) int64_t tid;
- // 当前平台的操作系统名称
- @property (nonatomic, copy) NSString *os_name;
- // 当前平台的操作系统版本号
- @property (nonatomic, copy) NSString *os_version;
- // 当前 SDK 名称,默认Object-C
- @property (nonatomic, copy) NSString *sdk_name;
- // 当前 SDK 版本号
- @property (nonatomic, copy) NSString *sdk_version;
- // 记录响应状态码
- @property (nonatomic, assign) int64_t status_code;
- // 记录响应中存储的 ReqId
- @property (nonatomic, copy) NSString *req_id;
- // 记录主机域名(不含解析,不含端⼝)
- @property (nonatomic, copy) NSString *host;
- // 记录成功建⽴连接的服务器 IP 地址
- @property (nonatomic, copy) NSString *remote_ip;
- // 记录主机端口号
- @property (nonatomic, assign) int64_t port;
- // 记录从发送请求到收到响应之间的单调时间差,单位为毫秒
- @property (nonatomic, assign) int64_t total_elapsed_time;
- // 记录⼀次请求中 DNS 查询的耗时,单位为毫秒,如果当前请求不需要进⾏ DNS 查询,则填写 0
- @property (nonatomic, assign) int64_t dns_elapsed_time;
- // 记录一次请求中建立⽹络连接的耗时,单位为毫秒,如果当前请求不不需要进行⽹络连接,则填写 0
- @property (nonatomic, assign) int64_t connect_elapsed_time;
- // 记录一次请求中建立安全⽹络连接的耗时,单位为毫秒(该耗时被 connect_elapsed_time 包含,因此总是⼩小于或等于 connect_elapsed_time,如果当前请求不需要进行安全连接,则填写 0)
- @property (nonatomic, assign) int64_t tls_connect_elapsed_time;
- // 记录⼀次请求中发送请求的耗时,单位为毫秒
- @property (nonatomic, assign) int64_t request_elapsed_time;
- // 记录⼀次请求中从发送请求完毕到收到响应前的耗时,单位为毫秒
- @property (nonatomic, assign) int64_t wait_elapsed_time;
- // 记录⼀次请求中读取响应的耗时,单位为毫秒
- @property (nonatomic, assign) int64_t response_elapsed_time;
- // 本次成功发送请求的请求体大小,单位为字节
- @property (nonatomic, assign) int64_t bytes_sent;
- // 预期发送请求的请求体大小,单位为字节
- @property (nonatomic, assign) int64_t bytes_total;
- // 错误类型
- @property (nonatomic, copy) NSString *error_type;
- // 对于服务器器成功响应,且响应体中包含 error 字段的,则给出 error 字段的内容。否则对于其他错误,则可以⾃自定义错误描述 信息
- @property (nonatomic, copy) NSString *error_description;
- // 请求结束时的⽹网络类型,可选值有 "wifi", "2g", "3g", "4g" 等。如果当前⽹网络不不可⽤用,则给出 "none"
- @property (nonatomic, copy) NSString *network_type;
- // 请求结束时的信号强度
- @property (nonatomic, assign) int64_t signal_strength;
- @end
- @implementation QNReportRequestItem
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- self.log_type = @"request";
- }
- return self;
- }
- + (instancetype)buildWithUpType:(NSString *)up_type
- TargetBucket:(NSString *)target_bucket
- targetKey:(NSString *)target_key
- fileOffset:(int64_t)file_offset
- targetRegionId:(NSString *)target_region_id
- currentRegionId:(NSString *)current_region_id
- prefetchedIpCount:(int64_t)prefetched_ip_count
- pid:(int64_t)pid
- tid:(int64_t)tid
- statusCode:(int64_t)status_code
- reqId:(NSString *)req_id
- host:(NSString *)host
- remoteIp:(NSString *)remote_ip
- port:(int64_t)port
- totalElapsedTime:(int64_t)total_elapsed_time
- dnsElapsedTime:(int64_t)dns_elapsed_time
- connectElapsedTime:(int64_t)connect_elapsed_time
- tlsConnectElapsedTime:(int64_t)tls_connect_elapsed_time
- requestElapsedTime:(int64_t)request_elapsed_time
- waitElapsedTime:(int64_t)wait_elapsed_time
- responseElapsedTime:(int64_t)response_elapsed_time
- bytesSent:(int64_t)bytes_sent
- bytesTotal:(int64_t)bytes_total
- errorType:(NSString *)error_type
- errorDescription:(NSString *)error_description
- networkType:(NSString *)network_type
- signalStrength:(int64_t)signal_strength {
-
- QNReportRequestItem *item = [[QNReportRequestItem alloc] init];
- item.up_type = up_type;
- item.target_bucket = target_bucket;
- item.target_key = target_key;
- item.file_offset = file_offset;
- item.target_region_id = target_region_id;
- item.current_region_id = current_region_id;
- item.prefetched_ip_count = prefetched_ip_count;
- item.pid = pid;
- item.tid = tid;
- item.status_code = status_code;
- item.req_id = req_id;
- item.host = host;
- item.remote_ip = remote_ip;
- item.port = port;
- item.total_elapsed_time = total_elapsed_time;
- item.dns_elapsed_time = dns_elapsed_time;
- item.connect_elapsed_time = connect_elapsed_time;
- item.tls_connect_elapsed_time = tls_connect_elapsed_time;
- item.request_elapsed_time = request_elapsed_time;
- item.wait_elapsed_time = wait_elapsed_time;
- item.response_elapsed_time = response_elapsed_time;
- item.bytes_sent = bytes_sent;
- item.bytes_total = bytes_total;
- item.error_type = error_type;
- item.error_description = error_description;
- item.network_type = network_type;
- item.signal_strength = signal_strength;
-
- #if __IPHONE_OS_VERSION_MIN_REQUIRED
- item.os_name = [[UIDevice currentDevice] model];
- item.os_version = [[UIDevice currentDevice] systemVersion];
- #else
- item.os_name = @"Mac OS X";
- item.os_version = [[NSProcessInfo processInfo] operatingSystemVersionString];
- #endif
-
- item.sdk_name = @"Object-C";
- item.sdk_version = kQiniuVersion;
- return item;
- }
- @end
- // block type item - 用于统计分片上传整体质量信息
- @interface QNReportBlockItem ()
- // ⽬标上传的区域 ID,可选值为 "z0","z1","z2","as0","na0"
- @property (nonatomic, copy) NSString *target_region_id;
- // 当前上传的区域 ID,可选值为 "z0","z1","z2","as0","na0"
- @property (nonatomic, copy) NSString *current_region_id;
- // 记录对于当前上传的区域,从发送第一个请求到收到最后一个响应 之间的单调时间差,单位为毫秒
- @property (nonatomic, assign) int64_t total_elapsed_time;
- // 成功上传⾄服务器的分块尺寸总和,单位为字节
- @property (nonatomic, assign) int64_t bytes_sent;
- // 上次失败时已上传的文件尺⼨(也就是上传恢复点),单位为字节
- @property (nonatomic, assign) int64_t recovered_from;
- // 要上传的文件总尺寸,单位为字节
- @property (nonatomic, assign) int64_t file_size;
- // 当前进程 ID
- @property (nonatomic, assign) int64_t pid;
- // 当前线程 ID
- @property (nonatomic, assign) int64_t tid;
- // 分⽚上传 API 版本,可选值为 1 和 2
- @property (nonatomic, assign) int64_t up_api_version;
- @end
- @implementation QNReportBlockItem
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- self.log_type = @"block";
- }
- return self;
- }
- + (instancetype)buildWithTargetRegionId:(NSString *)target_region_id
- currentRegionId:(NSString *)current_region_id
- totalElapsedTime:(int64_t)total_elapsed_time
- bytesSent:(int64_t)bytes_sent
- recoveredFrom:(int64_t)recovered_from
- fileSize:(int64_t)file_size
- pid:(int64_t)pid
- tid:(int64_t)tid
- upApiVersion:(int64_t)up_api_version {
-
- QNReportBlockItem *item = [[QNReportBlockItem alloc] init];
- item.target_region_id = target_region_id;
- item.current_region_id = current_region_id;
- item.total_elapsed_time = total_elapsed_time;
- item.bytes_sent = bytes_sent;
- item.recovered_from = recovered_from;
- item.file_size = file_size;
- item.pid = pid;
- item.tid = tid;
- item.up_api_version = up_api_version;
-
- return item;
- }
- @end
- // quality type item - 用于统计上传结果
- @interface QNReportQualityItem ()
- // 记录上传结果
- @property (nonatomic, copy) NSString *result;
- // 记录对于当前上传的⽂文件,从发送第⼀个请求到收到最后⼀个响应之间的单调时间差,单位为毫秒
- @property (nonatomic, assign) int64_t total_elapsed_time;
- // 为了完成本次上传所发出的 HTTP 请求总数(含 UC Query 和 HTTPDNS Query)
- @property (nonatomic, assign) int64_t requests_count;
- // 为了完成本次上传所使用的区域数量
- @property (nonatomic, assign) int64_t regions_count;
- // 为了完成本次上传所发出的 HTTP 请求体尺寸总量(含 UC Query 和 HTTPDNS Query)
- @property (nonatomic, assign) int64_t bytes_sent;
- @end
- @implementation QNReportQualityItem
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- self.log_type = @"quality";
- }
- return self;
- }
- + (instancetype)buildWithResult:(NSString *)result
- totalElapsedTime:(int64_t)total_elapsed_time
- requestsCount:(int64_t)requests_count
- regionsCount:(int64_t)regions_count
- bytesSent:(int64_t)bytes_sent {
-
- QNReportQualityItem *item = [[QNReportQualityItem alloc] init];
- item.result = result;
- item.total_elapsed_time = total_elapsed_time;
- item.requests_count = requests_count;
- item.regions_count = regions_count;
- item.bytes_sent = bytes_sent;
-
- return item;
- }
- @end
- @implementation QNReportConfig
- + (instancetype)sharedInstance {
-
- static QNReportConfig *sharedInstance = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- sharedInstance = [[self alloc] init];
- });
- return sharedInstance;
- }
- - (instancetype)init {
- self = [super init];
- if (self) {
- _reportEnable = YES;
- _interval = 10;
- _serverURL = @"https://uplog.qbox.me/log/4";
- _recordDirectory = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"com.qiniu.report"];
- _maxRecordFileSize = 2 * 1024 * 1024;
- _uploadThreshold = 4 * 1024;
- _timeoutInterval = 10;
- }
- return self;
- }
- @end
- static const NSString *recorderFileName = @"recorder";
- static const NSString *reportTypeValueList[] = {@"form", @"mkblk", @"bput", @"mkfile", @"block"};
- @interface QNUploadInfoReporter ()
- @property (nonatomic, strong) QNReportConfig *config;
- @property (nonatomic, assign) NSTimeInterval lastReportTime;
- @property (nonatomic, strong) NSFileManager *fileManager;
- @property (nonatomic, strong) NSString *recorderFilePath;
- @property (nonatomic, strong) dispatch_queue_t recordQueue;
- @property (nonatomic, strong) dispatch_semaphore_t semaphore;
- @property (nonatomic, copy, readwrite) NSString *X_Log_Client_Id;
- @end
- @implementation QNUploadInfoReporter
- + (instancetype)sharedInstance {
-
- static QNUploadInfoReporter *sharedInstance = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- sharedInstance = [[self alloc] init];
- });
- return sharedInstance;
- }
- - (instancetype)init {
-
- self = [super init];
- if (self) {
- _config = [QNReportConfig sharedInstance];
- _lastReportTime = 0;
- _recorderFilePath = [NSString stringWithFormat:@"%@/%@", _config.recordDirectory, recorderFileName];
- _fileManager = [NSFileManager defaultManager];
- _recordQueue = dispatch_queue_create("com.qiniu.reporter", DISPATCH_QUEUE_SERIAL);
- }
- return self;
- }
- - (void)clean {
-
- if ([_fileManager fileExistsAtPath:_recorderFilePath]) {
- NSError *error = nil;
- [_fileManager removeItemAtPath:_recorderFilePath error:&error];
- if (error) {
- NSLog(@"remove recorder file failed: %@", error);
- return;
- }
- }
- }
- - (BOOL)checkReportAvailable {
-
- if (!_config.isReportEnable) return NO;
- if (!(_config.maxRecordFileSize > _config.uploadThreshold)) {
- NSLog(@"maxRecordFileSize must be larger than uploadThreshold");
- return NO;
- }
- return YES;
- }
- - (void)report:(NSString *)jsonString token:(NSString *)token {
-
- if (![self checkReportAvailable] || !jsonString) return;
-
- // 串行队列处理文件读写
- dispatch_async(_recordQueue, ^{
- [self innerReport:jsonString token:token];
- });
- }
- - (void)innerReport:(NSString *)jsonString token:(NSString *)token {
-
- // 检查recorder文件夹是否存在
- NSError *error = nil;
- if (![_fileManager fileExistsAtPath:_config.recordDirectory]) {
- [_fileManager createDirectoryAtPath:_config.recordDirectory withIntermediateDirectories:YES attributes:nil error:&error];
- if (error) {
- NSLog(@"create record directory failed, please check record directory: %@", error.localizedDescription);
- return;
- }
- }
- // 拼接换行符
- NSString *finalRecordInfo = [jsonString stringByAppendingString:@"\n"];
- if (![_fileManager fileExistsAtPath:_recorderFilePath]) {
- // 如果recordFile不存在,创建文件并写入首行,首次不上传
- [finalRecordInfo writeToFile:_recorderFilePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
- } else {
- // recordFile存在,拼接文件内容、上传到服务器
- QNFile *file = [[QNFile alloc] init:_recorderFilePath error:&error];
- if (error) {
- NSLog(@"create QNFile with path failed: %@", error.localizedDescription);
- return;
- }
-
- // 判断recorder文件大小是否超过maxRecordFileSize
- if (file.size < _config.maxRecordFileSize) {
- @try {
- // 上传信息写入recorder文件
- NSFileHandle *fileHandler = [NSFileHandle fileHandleForUpdatingAtPath:_recorderFilePath];
- [fileHandler seekToEndOfFile];
- [fileHandler writeData: [finalRecordInfo dataUsingEncoding:NSUTF8StringEncoding]];
- [fileHandler closeFile];
- } @catch (NSException *exception) {
- NSLog(@"NSFileHandle cannot write data: %@", exception.description);
- }
- }
-
- // 判断是否满足上传条件:文件大于上报临界值 && (首次上传 || 距上次上传时间大于_config.interval)
- NSTimeInterval currentTime = [[NSDate dateWithTimeIntervalSinceNow:0] timeIntervalSince1970];
- if (file.size > _config.uploadThreshold && (_lastReportTime == 0 || currentTime - _lastReportTime > _config.interval * 60)) {
-
- NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:_config.serverURL]];
- [request setValue:[NSString stringWithFormat:@"UpToken %@", token] forHTTPHeaderField:@"Authorization"];
- [request setValue:[[QNUserAgent sharedInstance] getUserAgent:[QNUpToken parse:token].access] forHTTPHeaderField:@"User-Agent"];
- if (self.X_Log_Client_Id) {
- [request setValue:self.X_Log_Client_Id forHTTPHeaderField:@"X-Log-Client-Id"];
- }
- [request setHTTPMethod:@"POST"];
- [request setTimeoutInterval:_config.timeoutInterval];
- __block NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
- NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:_recorderFilePath] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
- NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
- if (httpResponse.statusCode == 200) {
- self.lastReportTime = [[NSDate dateWithTimeIntervalSinceNow:0] timeIntervalSince1970];
- NSDictionary *respHeader = httpResponse.allHeaderFields;
- if (!self.X_Log_Client_Id && [respHeader.allKeys containsObject:@"x-log-client-id"]) {
- self.X_Log_Client_Id = respHeader[@"x-log-client-id"];
- }
- [self clean];
- } else {
- NSLog(@"upload info report failed: %@", error.localizedDescription);
- }
- [session finishTasksAndInvalidate];
- dispatch_semaphore_signal(self.semaphore);
- }];
- [uploadTask resume];
-
- // 控制上传过程中,文件内容不被修改
- _semaphore = dispatch_semaphore_create(0);
- dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
- }
- }
- }
- @end
|