通过OpenTelemetry接入iOS Trace数据

本文介绍通过OpenTelemetry将iOS应用的Trace数据接入到日志服务的操作步骤。

前提条件

已创建Trace实例。更多信息,请参见创建Trace实例

步骤一:集成SDK

Swift项目

  1. 导入opentelemetry-swift。

    1. 在Xcode中,选择File>Add Package

    2. 在添加Package文本框中,输入如下链接。

      https://github.com/open-telemetry/opentelemetry-swift
    3. 选择Dependency RuleExact Version,设置版本号为1.4.0

      更多信息,请参见OpenTelemetry iOS SDK Releaseimage

  2. 选择Swift Products。

    建议选择以下Products。

    • OpenTelemetryApi

    • OpenTelemetryProtocolExporter

    • OpenTelemetrySdk

    • URLSessionInstrumentation

    • StdoutExporter

    • ResourceExtension

    关于Swift Products的更多信息,请参见附录:OpenTelemetry Products说明image

Objc项目

opentelemetry-swift对Objc语言的兼容较差,日志服务基于opentelemetry-swift实现了一个Objc扩展。Objc项目除了需要完成Swift项目的配置外,还需要增加opentelemetry-objc-extension。

  1. 完成Swift项目的配置。

  2. 导入opentelemetry-objc-extension。

    1. 在Xcode中,选择File>Add Package

    2. 在添加Package文本框中,输入如下链接。

      https://github.com/aliyun-sls/opentelemetry-objc-extension
    3. 选择Dependency RuleExact Version,输入版本号为1.0.0

      更多信息,请参见OpenTelemetry iOS Objc Extension SDK Releaseimage

  3. 选择Objc Extension Products。

    建议选择以下Products。

    • OpenTelemetryApiObjc

    • OpenTelemetryProtocolExporterObjc

    • OpenTelemetrySdkObjc

    • ResourceExtensionObjc

    • URLSessionInstrumentationObjc

    • StdoutExporterObjc

关于Objc Extension Products的更多信息,请参见附录:OpenTelemetry Products说明

步骤二:初始化SDK

一般建议在AppDelegate类的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法中进行SDK初始化。

Swift项目

// 导入如下模块。
import GRPC
import NIO
import OpenTelemetryApi
import OpenTelemetrySdk
import OpenTelemetryProtocolExporter
import StdoutExporter
import URLSessionInstrumentation

// 初始化Exporter。Exporter用于导出Trace数据到日志服务Logstore。
let client = ClientConnection.usingPlatformAppropriateTLS(for: MultiThreadedEventLoopGroup(numberOfThreads: 1))
.connect(host: "${endpoint}", port: ${port})	
let otlpTraceExporter = OtlpTraceExporter(
 channel: client,
 config: OtlpConfiguration(
 timeout: OtlpConfiguration.DefaultTimeoutInterval,
 headers:
 [
 ("x-sls-otel-project", "${project}"),
 ("x-sls-otel-instance-id", "${instanceId}"),
 ("x-sls-otel-ak-id", "${access-key-id}"),
 ("x-sls-otel-ak-secret", "${access-key-secret}")
 ]
 )
)

// 初始化tracer provider。tracer provider用于暴露主要的API,对Span进行预处理、自定义Clock,
// 自定义TraceId、SpanId生成规则,自定义采样器等。您可以根据实际的需要进行配置。
let spanExporters = MultiSpanExporter(spanExporters: [StdoutExporter(isDebug: true), otlpTraceExporter])
let spanProcessor = SimpleSpanProcessor(spanExporter: spanExporters)
OpenTelemetry.registerTracerProvider(
 tracerProvider: TracerProviderBuilder()
 .add(spanProcessor: spanProcessor)
 .with(resource: 
 Resource(attributes:
 [
 ResourceAttributes.serviceName.rawValue: AttributeValue.string("${service}"),
 ResourceAttributes.serviceNamespace.rawValue: AttributeValue.string("${service.namespace}"),
 ResourceAttributes.serviceVersion.rawValue: AttributeValue.string("${version}"),
 ResourceAttributes.hostName.rawValue: AttributeValue.string("${host}"),
 ResourceAttributes.deploymentEnvironment.rawValue: AttributeValue.string("${environment}"),
 ]
 )
 )
 .build()
)

// 配置其他的Instrumentation采集器。请根据实际业务需要进行配置。如下配置是开启采集NSUrlSession网络库的请求信息。
URLSessionInstrumentation(configuration: URLSessionInstrumentationConfiguration(
 shouldInstrument: { request in
 return true
 })
)
let tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: "OTel Application", instrumentationVersion: "1.0.0")

Objc 项目

// 导入以下模块
@import OpenTelemetryApiObjc;
@import OpenTelemetrySdkObjc;
@import OpenTelemetryProtocolExporterObjc;
@import URLSessionInstrumentationObjc;
@import StdoutExporterObjc;

// 初始化Exporter。Exporter用于导出Trace数据到日志服务Logstore。
NSDictionary *headers = @{
 @"x-sls-otel-project": @"${project}",
 @"x-sls-otel-instance-id": @"${instanceId}",
 @"x-sls-otel-ak-id": @"${access-key-id}",
 @"x-sls-otel-ak-secret": @"${access-key-secret}"
};
OtlpConfigurationObjc *configuration = [OtlpConfigurationObjc configuration:OtlpConfigurationObjc.DefaultTimeoutInterval headers:headers];
OtlpTraceExporterObjc *exporter = [OtlpTraceExporterObjc exporter:@"https://${endpoint}"
 port:@"${port}"
 configuration:configuration
];

// 初始化tracer provider。tracer provider用于暴露主要的API,对Span进行预处理,自定义Clock。
// 自定义TraceId、SpanId生成规则,自定义采样器等。您可以根据实际的需要进行配置。
NSArray *exporters = @[
 [StdoutExporterObjc stdoutExporter:true], // 开发调试时,建议开启,实现在console中打印出Span的具体的内容。
 exporter
];
SpanProcessorObjc *spanProcessor = [BatchSpanProcessorObjc processor: [MultiSpanExporterObjc multiSpanExporter:exporters]];

TracerProviderBuilderObjc *providerBuilder = [TracerProviderBuilderObjc builder];
NSDictionary *resources = @{
 ResourceAttributesObjc.serviceName: [AttributeValueObjc string:@"${service}"], // 当前要追踪Trace的服务名称,建议取模块的名称,例如首页、会员中心。
 ResourceAttributesObjc.serviceNamespace: [AttributeValueObjc string:@"${service.namespace}"], // 建议固定为iOS/iPadOS/macOS/tvOS/watchOS。
 ResourceAttributesObjc.serviceVersion: [AttributeValueObjc string:@"${version}"], // 建议和App版本号保持一致。
 ResourceAttributesObjc.hostName: [AttributeValueObjc string:@"${host}"], // 建议固定为iOS。
 ResourceAttributesObjc.deploymentEnvironment: [AttributeValueObjc string:@"${environment}"] // 开发环境,一般开发阶段建议设置为dev,线上环境设置为prod。
};
providerBuilder = [providerBuilder withResource: [ResourceObjc resource:resources]];
providerBuilder = [providerBuilder addSpanProcessor: spanProcessor];

// 初始化OpenTelemetrySdk。
[OpenTelemetryObjc registerTracerProvider:[providerBuilder build]];

// 配置其他的Instrumentation采集器。请根据实际业务需要进行配置。如下配置是开启采集NSUrlSession网络库的请求信息。
[URLSessionInstrumentationObjc urlSessionInstrumentation:
 [URLSessionInstrumentationConfigurationObjc urlSessionInstrumentationConfiguration:[TestURLSessionInstrumentation instrumentation]]
];

变量说明如下表所示:

变量

说明

示例

${endpoint}

服务入口是访问一个Project及其内部数据的URL,日志服务提供私网域名和公网域名。更多信息,请参见服务入口

  • Port:网络端口,固定为10010。

cn-hangzhou.log.aliyuncs.com:10010

${project}

日志服务Project名称,更多信息,请参见管理Project

test-project

${instance}

Trace服务实例ID。更多信息,请参见创建Trace实例

test-traces

${access-key-id}

AccessKey ID用于标识用户,更多信息,请参见访问密钥

建议您遵循最小化原则,按需授予RAM用户必要的权限。关于授权的具体操作,请参见创建RAM用户及授权RAM自定义授权示例

${access-key-secret}

AccessKey Secret是用户用于加密签名字符串和日志服务用来验证签名字符串的密钥,必须保密。

${service.namespace}

服务归属的命名空间。

order

${service}

服务名,根据您的实际场景配置。

payment

${version}

服务版本号,建议按照va.b.c格式定义。

v1.0.0

${host}

主机名。

localhost

${environment}

部署环境,例如测试环境、生产环境。

pre

步骤三:使用SDK

创建Tracer

建议根据不同的业务场景创建Tracer。创建Tracer时,需要传入instrumentation scope name,利于按照scope区分不同的Trace数据。

Swift示例

let tracer: Tracer = OpenTelemetry.instance.tracerProvider.get(instrumentationName: "OTel Application", instrumentationVersion: "1.0.0")

ObjC示例

TracerObjc *tracer = [OpenTelemetryObjc.instance.tracerProvider getWithInstrumentationName:@"app" instrumentationVersion:@"1.0"];

创建基本Span

Span代表了事务中的操作,每个Span都封装了操作名称、起止时间戳、属性信息、事件信息和Context信息等。

Swift示例

let spanBuilder = tracer.spanBuilder(spanName: "operation name")
let span = spanBuilder .setSpanKind(spanKind: .client).startSpan()
// do stuff
// ...
span.end()

ObjC示例

SpanBuilderObjc *spanBuilder = [_tracer spanBuilder:@"operation name"];
SpanObjc *span = [[spanBuilder setSpanKind:SpanKindObjc.CLIENT] startSpan];
// do stuff
// ...
[span end];

创建嵌套Span

当您希望为嵌套操作关联Span时,OpenTelemetry支持在进程内和跨远程进程进行跟踪。例如针对methodA调用方法methodB ,您可以通过以下方式创建嵌套Span。

Swift示例

func methodA() {
 let span = tracer.spanBuilder(spanName: "operation methodA").setSpanKind(spanKind: .client).startSpan()
	method(span)
 span.end()
}

func methodB(_ parent: Span) {
 let spanBuilder = tracer.spanBuilder(spanName: "operation methodB")
 spanBuilder.setParent(parent)
 let child = spanBuilder .setSpanKind(spanKind: .client).startSpan()
 // do stuff
 // ...
 child.end()
}

ObjC示例

- (void) methodA {
 SpanObjc *span = [[[_tracer spanBuilder:@"operation methodA"] setSpanKind:SpanKindObjc.CLIENT] startSpan];
 [self methodB: span];
 [span end];
}

- (void) methodB: (SpanObjc *)parent {
 SpanBuilderObjc *spanBuilder = [_tracer spanBuilder:@"operation name"];
 [spanBuilder setParent: parent];
 SpanObjc *span = [[spanBuilder setSpanKind:SpanKindObjc.CLIENT] startSpan];
 // do stuff
 // ...
 [span end];
}

OpenTemetry API还提供了一种自动化的方式来传播parentSpan。

Swift示例

func methodA() {
 let span = tracer.spanBuilder(spanName: "operation methodA").setSpanKind(spanKind: .client).startSpan()
 OpenTelemetry.instance.contextProvider.setActiveSpan(span)
 
 methodB(span)
 
 span.end()
}

func methodB(_ parent: Span) {
 let child = tracer.spanBuilder(spanName: "operation methodB").setSpanKind(spanKind: .client).startSpan()
 // do stuff
 // ...
 child.end()
}

ObjC示例

- (void) methodA {
 SpanObjc *span = [[[_tracer spanBuilder:@"operation methodA"] setSpanKind:SpanKindObjc.CLIENT] startSpan];
 [OpenTelemetryObjc.instance.contextProvider setActiveSpan:span];
 [self methodB];
 // do stuff
 // ...
 [span end];
}

- (void) methodB {
 SpanBuilderObjc *spanBuilder = [_tracer spanBuilder:@"operation name"];
 SpanObjc *span = [[spanBuilder setSpanKind:SpanKindObjc.CLIENT] startSpan];
 // do stuff
 // ...
 [span end];
}

创建带属性的Span

您可以通过属性在Span上提供特定操作的上下文信息。例如执行结果、关联的其他业务信息等,示例如下所示。

Swift示例

let spanBuilder = tracer.spanBuilder(spanName: "GET /resource/catalog")
let span = spanBuilder .setSpanKind(spanKind: .client).startSpan()
span.setAttribute(key: "http.method", value: "GET")
span.setAttribute(key: "http.url", value: "http url")

ObjC示例

SpanBuilderObjc *spanBuilder = [_tracer spanBuilder:@"GET /resource/catalog"];
SpanObjc *span = [[spanBuilder setSpanKind:SpanKindObjc.CLIENT] startSpan];
[span setAttribute:@"http.method" stringValue:@"GET"];
[span setAttribute:@"http.url" stringValue:request.URL.baseURL];

创建带事件的Span

您可以通过携带多个事件的方式对Span进行注释。

Swift示例

span.addEvent(name: "start")
// do stuff
// ...
span.addEvent(name: "start")

// 也可以携带属性。
span.addEvent(name: "event with attributes",
 attributes: [ "key1": AttributeValue.string("value1"),
 "key2": AttributeValue.double(2.2)
 ]
)

ObjC示例

[chilsSpan addEvent:@"start"];
// do stuff
// ...
[chilsSpan addEvent:@"end"];

// 也可以携带属性。
[chilsSpan addEvent:@"event with attributes" attributes:@{
 @"key1": [AttributeValueObjc string:@"value1"],
 @"key2": [AttributeValueObjc double:1.1],
}];

创建带链接的Span

一个 Span可以链接一个或多个因果相关的其他Span。

Swift示例

spanBuilder.addLink(spanContext: parent.context)
let span = spanBuilder .setSpanKind(spanKind: .client).startSpan()

ObjC示例

SpanBuilderObjc *spanBuilder = [_tracer spanBuilder:@"Another"];
[spanBuilder addLink:parent.context];

SpanObjc *span = [[spanBuilder setSpanKind:SpanKindObjc.CLIENT] startSpan];

从远程进程中读取上下文信息的具体操作,请参见Context Propagation

给Span添加状态

Span包含StatusCode.UNSETStatusCode.OKStatusCode.ERROR三个状态,分别表示默认状态、成功状态、操作包含错误。例如参考如下示例,为Span添加操作包含错误的状态。

Swift示例

let span = tracer.spanBuilder(spanName: "operation name").startSpan()
do {
 try expression
 // do stuff
 // ...
} catch {
 span.status = .error(description: "\(error)")
}

ObjC示例

SpanObjc *span = [[_tracer spanBuilder:@"operation name"] startSpan];
@try {
 // do stuff
 // ...
} @catch (NSException *exception) {
 [span setStatus:[StatusObjc error: exception.description]];
} @finally {
 [span end];
}

传播上下文信息

OpenTelemetry提供了一种基于文本的方法,传播上下文信息。以下是使用 NSURLSession发出HTTP请求的示例。

Swift示例

// 在SDK初始化时,需要完成URLSessionInstrumentation的初始化。
// 按需传入对应的参数。
URLSessionInstrumentation(
 configuration: URLSessionInstrumentationConfiguration(
 shouldInstrument: { request in
 // 是否采集该request的数据。
 return true
 }
 )
)

ObjC示例

// 在SDK初始化时,需要完成URLSessionInstrumentationObjc的初始化。
// 初始化URLSessionInstrumentationObjc时,需要传入一个实现了URLSessionInstrumentationConfigurationImpl协议的类(可参见下面的TestURLSessionInstrumentation实现)
[URLSessionInstrumentationObjc urlSessionInstrumentation:
 [URLSessionInstrumentationConfigurationObjc urlSessionInstrumentationConfiguration:[TestURLSessionInstrumentation instrumentation]]
];

// TestURLSessionInstrumentation的实现示例。
#pragma mark - URLSessionInstrumentation
@interface TestURLSessionInstrumentation: NSObject<URLSessionInstrumentationConfigurationImpl>
+ (instancetype) instrumentation;
@end

@implementation TestURLSessionInstrumentation
+ (instancetype) instrumentation {
 return [[TestURLSessionInstrumentation alloc] init];
}
- (void)createdRequest:(NSURLRequest * _Nonnull)request span:(SpanObjc * _Nonnull)span {
 // request被创建时回调。
 [span setAttribute:@"createdRequest" stringValue:@"request created"];
}
- (void)injectCustomHeaders:(NSURLRequest * _Nonnull)request span:(SpanObjc * _Nullable)span {
 // 注入自定义请求头。
 [(NSMutableURLRequest *)request addValue:@"custom header" forHTTPHeaderField:@"injectCustomHeaders"];
}
- (NSString * _Nullable)nameSpan:(NSURLRequest * _Nonnull)request {
 // 重命名Span。
 if ([request.URL.host containsString:@"dns.alidns.com"]) {
 return @"请求解析DNS";
 }
 return nil;
}
- (void)receivedError:(NSError * _Nonnull)error dataOrFile:(NSObject * _Nullable)dataOrFile span:(SpanObjc * _Nonnull)span {
 // 接口失败时回调。
}
- (void)receivedResponse:(NSURLResponse * _Nonnull)response dataOrFile:(NSObject * _Nullable)dataOrFile span:(SpanObjc * _Nonnull)span {
 // 接口成功时回调。
 NSLog(@"receivedResponse: %@", dataOrFile);
}
- (BOOL)shouldInjectTracingHeaders:(NSURLRequest * _Nonnull)request {
 // 注入tracing headers。
 return YES;
}
- (BOOL)shouldInstrument:(NSURLRequest * _Nonnull)request {
 // 是否采集该request的数据。
 return YES;
}
- (BOOL)shouldRecordPayload:(NSURLSession * _Nonnull)session {
 // 是否采集请求体信息。
 return YES;
}
- (void)spanCustomization:(NSURLRequest * _Nonnull)request spanBuilder:(SpanBuilderObjc * _Nonnull)spanBuilder {
 // 自定义Span信息。
 [spanBuilder setAttribute:@"spanCustomization" stringValue:@"customize span"];
}
@end

目前,OpenTelemetry SDK支持按照W3C Trace Context标准传播上下文信息。更多信息,请参见W3CTraceContextPropagator类

关于OpenTelemetry SDK的更多信息,请参见官方文档

附录:OpenTelemetry Products说明

Swift

Objc

用途

OpenTelemetryApi

OpenTelemetryApiObjc

OpenTelemetry API的约定和最小实现。

OpenTelemetrySdk

OpenTelemetrySdkObjc

OpenTelemetry API的最小实现。

OpenTelemetryProtocolExporter

OpenTelemetryProtocolExporterObjc

OpenTelemetry Protocol实现。

StdoutExporter

StdoutExporterObjc

标准输出Exporter,用于将Trace数据打印在console。

ResourceExtension

ResourceExtensionObjc

Resource的扩展采集实现。

URLSessionInstrumentation

URLSessionInstrumentationObjc

URLSession网络库的自动采集实现。

常见问题

  • 问题:使用Swift SDK后,无数据上报到日志服务。

  • 解决方法:当前SDK需要通过OtlpTraceExporter进行数据上报,请确认OtlpTraceExporter的配置是否正确。以下为正确配置的示例。

    重要
    • host中不能添加 http:// https://

    • port必须设置为10010。

    let client = ClientConnection
    .usingPlatformAppropriateTLS(for: MultiThreadedEventLoopGroup(numberOfThreads: 1))
    .connect(host: "cn-beijing.log.aliyuncs.com", port: 10010)
    
    let otlpTraceExporter = OtlpTraceExporter(channel: client, config: OtlpConfiguration(timeout: OtlpConfiguration.DefaultTimeoutInterval, headers: [
     ("x-sls-otel-project", "your trace project"),
     ("x-sls-otel-instance-id", "your trace instance id"),
     ("x-sls-otel-ak-id", "your access key id"),
     ("x-sls-otel-ak-secret", "your access key secret")
    ]))