APM监控系统-启动监控

前言

iOS 网络库普遍用的开源库是AFNetworking和微信的Mars,大多数都是基于AFNetworkin苹果xrg实现自己的网络库。实现网络监控,挺简单的,但是身处于基础服务组,需要对业务侧无侵入监控,将会遇到一些困难。在此记录APM专项网络指标监控所遇到的问题。

App 网络请苹果x求过程

APM监控系统-网络监控

NSURLSehttp 500ssionTaskDelegate 新代理方法

iOS 10 之后,NSURLSessionTa监控怎么查看回放skDelegate 中增加了一个新的代理方法:

/*
 * Sent when complete statistics information has been collected for the task.
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

不要想着 iOS苹果范冰冰 9 没有,就放弃苹果爸爸给的糖。别在意那么一丢丢用户量。

以下有三种方案,方案三是为实现业务无侵入https和http的区别而实现,若司内没有过多的项目且没有所谓的基础服务库,直接使用方案一和方案二。

方案一:NSURLProtocol 监控 App 网络请求

自定义CustomURLProtocol,实现网络监控。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
}
- (void)startLoading {
}
#pragma NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
    // 实现网络指标上报
}

缺点

需要实现 protocolClasses。 问题在于多个自定义URLProtocol,只响应一个。如果项目业务侧没有其http协议他自定义URhttpwatchLProtocol,可以用此方案实现。如果在基础服务组,而司内的项目又有自定义URLProtocol,会导致业务侧无法实现自定义URLProtocol逻辑代码。

NSURLSessionConfiguration *config=[NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses=@[[CustomURLProtocol class]];

方案二:实现 AFNetworkiAPPng 的 MetricsBlock

- (void)setTaskDidFinishCollectingMetricsBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, NSURLSessionTaskMetrics * _Nullable metrics))block AF_API_AVAILABLE(ios(10), macosx(10.12), watchos(3), tvos(10));

可以在司内apple苹果官网的基础服务网络库实现此block,进行网络指标上报。

缺点

  1. AFNetworking 4.0.0 才有此block,必须升级到此版本;
  2. 个别项目业务侧也实现此block,出现覆盖问题;
  3. 个别项目没有使用司内的网络库,独立封装AFNetworking,实监控系统现网络请求。因此没法监控到;

方案三:hook AFNetw监控apporking 的实现http 302 NSURLSessionTaskDelegate的代理方法

不管AFNetworking是 3.2.1及以下还是4.0.0及以上,不管项目组有没http 302有使用司内基础服务网络库,只有用AFNetworking库发起请求的。都将能实现网络指标监控。

优点

实现不必在基础服务网络库,可以在其他基础服务库实现,如司内的基础服务APM库,实现网络监控,并通过APM上报指标数苹果8plus据。

代码实现

AFHTTPSessionManager+APM.h


#import "AFHTTPSessionManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface AFHTTPSessionManager (APM)
@end
NS_ASSUME_NONNULL_END

AFHTTPS监控眼essionManager+APM.m

#import "AFHTTPSessionManager+APM.h"
#import "APMNetworkMetricsModel.h"
#import <objc/runtime.h>
static inline void swizzling_exchangeMethod(Class clazz ,SEL originalSelector, SEL swizzledSelector){
    Method originalMethod = class_getInstanceMethod(clazz, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(clazz, swizzledSelector);
    BOOL success = class_addMethod(clazz, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(clazz, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@implementation AFHTTPSessionManager (APM)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        swizzling_exchangeMethod(self, @selector(URLSession:task:didFinishCollectingMetrics:), @selector(apm_URLSession:task:didFinishCollectingMetrics:));
    });
}
- (void)apm_URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
    [self apm_URLSession:session task:task didFinishCollectingMetrics:metrics];
    NSLog(@"metrics: %@", metrics);
    if (@available(iOS 10.0, *) &&
        metrics.transactionMetrics.count)
    {
        [metrics.transactionMetrics enumerateObjectsUsingBlock:^(NSURLSessionTaskTransactionMetrics * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if (obj == nil) return;
            APMNetworkMetricsModel *model = [APMNetworkMetricsModel networkMetricsModelWithMetrics:obj];
            // 对model进行处理,通过APM上报网络指标
            // 这里可以通过业务侧动态设置采样率,不必全量上报
            // 通过APM上报后也会执行到这里,需要有判断条件过滤,否则将无限循环
        }];
    }
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // AFNetworking 4.0.0 有实现didFinishCollectingMetrics
    // 但是4.0.0以下没有实现,方法交换后会崩溃,所以在此动态添加方法
    if (sel == @selector(URLSession:task:didFinishCollectingMetrics:)) {
        Method method = class_getInstanceMethod(self, @selector(doMethod));
        IMP imp = method_getImplementation(method);
        const char *typeEncoding = method_getTypeEncoding(method);
        return class_addMethod(self, sel, imp, typeEncoding);
    }
    return [super resolveInstanceMethod:sel];
}
- (void)doMethod
{
    // 孤单的空实现
}
@end

APMNetworkMetricsModel.h

属性定义用下划线是为了方便映射表,忽略。

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface APMNetworkMetricsModel : NSObject
/// 接口
@property (nonatomic, copy) NSString *path;
/// 请求的 URL 地址
@property (nonatomic, copy) NSString *req_url;
/// 请求参数
@property (nonatomic, copy) NSString *req_params;
/// 请求头
@property (nonatomic, strong) NSDictionary *req_headers;
/// 请求头流量
@property (nonatomic, assign) int64_t req_header_byte;
/// 请求体流量
@property (nonatomic, assign) int64_t req_body_byte;
/// 响应头
@property (nonatomic, strong) NSDictionary *res_headers;
/// 响应码
@property (nonatomic, assign) NSInteger status_code;
/// 响应头流量
@property (nonatomic, assign) int64_t res_header_byte;
/// 响应体流量
@property (nonatomic, assign) int64_t res_body_byte;
/// HTTP 方法
@property (nonatomic, copy) NSString *http_method;
/// 协议名
@property (nonatomic, copy) NSString *protocol_name;
/// 是否使用代理
@property (nonatomic, assign) BOOL proxy_connection;
/// 是否蜂窝连接
@property (nonatomic, assign) BOOL cellular;
/// 本地 ip
@property (nonatomic, copy) NSString *local_ip;
/// 本地端口
@property (nonatomic, assign) NSInteger local_port;
/// 远端 ip
@property (nonatomic, copy) NSString *remote_ip;
/// 远端端口
@property (nonatomic, assign) NSInteger remote_port;
#pragma mark - cost time
/// DNS 解析耗时
@property (nonatomic, assign) int64_t dns_time;
/// TCP 连接耗时
@property (nonatomic, assign) int64_t tcp_time;
/// SSL 握手耗时
@property (nonatomic, assign) int64_t ssl_time;
/// Request 请求耗时
@property (nonatomic, assign) int64_t req_time;
/// Response 响应耗时
@property (nonatomic, assign) int64_t res_time;
/// 请求到响应总耗时
@property (nonatomic, assign) int64_t req_total_time;
@end
@class NSURLSessionTaskTransactionMetrics;
@interface APMNetworkMetricsModel (Helper)
+ (instancetype)networkMetricsModelWithMetrics:(NSURLSessionTaskTransactionMetrics *)metrics;
@end
NS_ASSUME_NONNULL_END

APMNetworkMetricsModel.m

#import "APMNetworkMetricsModel.h"
@implementation APMNetworkMetricsModel
@end
@implementation APMNetworkMetricsModel (Helper)
+ (instancetype)networkMetricsModelWithMetrics:(NSURLSessionTaskTransactionMetrics *)metrics
{
    if (metrics.resourceFetchType == NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad)
    {
        APMNetworkMetricsModel *model = [[APMNetworkMetricsModel alloc] init];
        // 需要在基础服务网络库对header设置接口名
        model.path = metrics.request.allHTTPHeaderFields[@"path"];
        model.req_url = [metrics.request.URL absoluteString];
        model.req_params = [metrics.request.URL parameterString];
        model.req_headers = metrics.request.allHTTPHeaderFields;
        if (@available(iOS 13.0, *)) {
            model.req_header_byte = metrics.countOfRequestHeaderBytesSent;
            model.req_body_byte = metrics.countOfRequestBodyBytesSent;
            model.res_header_byte = metrics.countOfResponseHeaderBytesReceived;
            model.res_body_byte = metrics.countOfResponseBodyBytesReceived;
        }
        if (@available(iOS 13.0, *)) {
            model.local_ip = metrics.localAddress;
            model.local_port = metrics.localPort.integerValue;
            model.remote_ip = metrics.remoteAddress;
            model.remote_port = metrics.remotePort.integerValue;
        }
        if (@available(iOS 13.0, *)) {
            model.cellular = metrics.cellular;
        }
        NSHTTPURLResponse *response = (NSHTTPURLResponse *)metrics.response;
        if ([response isKindOfClass:NSHTTPURLResponse.class])
        {
            model.res_headers = response.allHeaderFields;
            model.status_code = response.statusCode;
        }
        model.http_method = metrics.request.HTTPMethod;
        model.protocol_name = metrics.networkProtocolName;
        model.proxy_connection = metrics.proxyConnection;
        if (metrics.domainLookupStartDate &&
            metrics.domainLookupEndDate)
        {
            model.dns_time = ceil([metrics.domainLookupEndDate timeIntervalSinceDate:metrics.domainLookupStartDate] * 1000);
        }
        if (metrics.connectStartDate &&
            metrics.connectEndDate)
        {
            model.tcp_time = ceil([metrics.connectEndDate timeIntervalSinceDate:metrics.connectStartDate] * 1000);
        }
        if (metrics.secureConnectionStartDate &&
            metrics.secureConnectionEndDate)
        {
            model.ssl_time = ceil([metrics.secureConnectionEndDate timeIntervalSinceDate:metrics.secureConnectionStartDate] * 1000);
        }
        if (metrics.requestStartDate &&
            metrics.requestEndDate)
        {
            model.req_time = ceil([metrics.requestEndDate timeIntervalSinceDate:metrics.requestStartDate] * 1000);
        }
        if (metrics.responseStartDate &&
            metrics.responseEndDate)
        {
            model.res_time = ceil([metrics.responseEndDate timeIntervalSinceDate:metrics.responseStartDate] * 1000);
        }
        if (metrics.fetchStartDate &&
            metrics.responseEndDate)
        {
            model.req_total_time = ceil([metrics.responseEndDate timeIntervalSinceDate:metrics.fetchStartDate] * 1000);
        }
        return model;
    }
    return nil;
}
@end