本文介绍通过OpenTelemetry将Flutter、Dart应用的Trace数据接入到日志服务的操作步骤。
前提条件
已创建Trace实例。具体操作,请参见创建Trace实例。
步骤一:SDK集成
创建Flutter SDK。具体操作,请参见安装Flutter SDK。
在项目根目录下执行以下命令,导入opentelemetry-dart SDK模块。
flutter pub add opentelemetry_sls
导入成功后,项目的
pubspec.yaml
文件中将增加以下信息,并隐式执行flutter pub get
命令。dependencies: opentelemetry_sls: ^0.15.1
在指定的Dart文件中导入opentelemetry-dart模块。
import 'package:opentelemetry_sls/api.dart'; import 'package:opentelemetry_sls/sdk.dart' as otel_sdk;
步骤二:初始化SDK
在使用SDK前,需要先完成初始化。
TracerProvider? _provider;
Tracer? _tracer;
void _initOTelSDK() {
const project = "qs-demos";
const endpoint = "cn-beijing.log.aliyuncs.com";
const instanceId = "sls-mall";
const accessKeyId = "your access key id";
const accessKeySecret = "your access key secret";
final exporter =
otel_sdk.CollectorExporter(Uri.parse("https://${project}.${endpoint}/opentelemetry/v1/traces"), headers: {
"x-sls-otel-project": "${project}",
"x-sls-otel-instance-id": "${instanceId}",
"x-sls-otel-ak-id": "${accessKeyId}",
"x-sls-otel-ak-secret": "${accessKeySecret}"
});
final processor = otel_sdk.BatchSpanProcessor(exporter);
final simpleProcessor = otel_sdk.SimpleSpanProcessor(otel_sdk.ConsoleExporter());
_provider = otel_sdk.TracerProviderBase(
processors: [processor, simpleProcessor],
resource: otel_sdk.Resource([
Attribute.fromString("service.name", "main"),
Attribute.fromString("service.namespace", "flutter"),
Attribute.fromString("service.version", "1.0.0"),
Attribute.fromString("deployment.environment", "dev"),
]));
}
变量 | 说明 | 示例 |
| 服务入口是访问一个Project及其内部数据的URL,日志服务提供私网域名和公网域名。更多信息,请参见服务入口。 | cn-hangzhou.log.aliyuncs.com |
| 日志服务Project名称,更多信息,请参见管理Project。 | test-project |
| Trace服务实例ID。更多信息,请参见创建Trace实例。 | test-traces |
| AccessKey ID用于标识用户,更多信息,请参见访问密钥。 建议您遵循最小化原则,按需授予RAM用户必要的权限。关于授权的具体操作,请参见创建RAM用户及授权,RAM自定义授权示例。 | 无 |
| AccessKey Secret是用户用于加密签名字符串和日志服务用来验证签名字符串的密钥,必须保密。 | 无 |
| 服务归属的命名空间。 | order |
| 服务名,根据您的实际场景配置。 | payment |
| 服务版本号,建议按照 | v1.0.0 |
| 部署环境,例如测试环境、生产环境。 | pre |
步骤三:使用SDK
创建Tracer
建议根据不同的业务场景来创建Tracer。创建Tracer时需要传入instrumentation scope name,利于按照scope区分不同的Trace数据。
Tracer? _tracer = _provider!.getTracer('hello-otel-dart');
创建基本Span
Span代表事务中的操作,每个Span都封装了操作名称、起止时间戳、属性信息、事件信息和Context信息等。
final span = _tracer!.startSpan("operation");
// do stuff
// ...
span.end();
创建嵌套Span
当您希望为嵌套操作关联Span时,可通过以下方式进行关联。
final parent = _tracer!.startSpan("parent operation");
Context.current.withSpan(parent).execute(() {
final child = _tracer!.startSpan("child operation");
// do stuff
// ...
child.end();
});
parent.end();
创建带属性的Span
您可以通过属性在Span上提供特定操作的上下文信息。例如执行结果、关联的其他业务信息等。
final span = _tracer!.startSpan("GET /resource/catalog", kind: SpanKind.client);
span.setAttribute(Attribute.fromString("http.method", "GET"));
span.setAttribute(Attribute.fromString("http.url", "your http url"));
// do stuff
// ...
span.end();
给Span添加状态
Span包含StatusCode.unset
、StatusCode.ok
、StatusCode.errir
三个状态,分别表示默认状态、成功状态、操作包含错误。
final span = _tracer!.startSpan("operation");
span.setStatus(StatusCode.error, description: "something error");
// 也可以捕获异常信息
try {
_throwException();
} on Exception catch (e) {
span.recordException(e);
}
span.end();
传播上下文信息
OpenTelemetry提供了一种基于文本的方法,传播上下文信息。此处为使用Dart http库发出HTTP GET请求的示例。
import 'package:http/http.dart' as http;
final traceContextPropagator = otel_sdk.W3CTraceContextPropagator();
final textMapSetter = HttpClientTextMapSetter();
final headers = <String, String>{};
final httpSpan = _tracer!.startSpan("start http request");
Context.current.withSpan(httpSpan).execute(() {
traceContextPropagator.inject(Context.current, headers, textMapSetter);
final client = http.Client();
client.get(Uri.parse("http://sls-mall.caa227ac081f24f1a8556f33d69b96c99.cn-beijing.alicontainer.com/catalogue"),
headers: headers);
});
httpSpan.end();
目前,OpenTelemetry SDK支持按照 W3C Trace Context标准传播上下文信息。关于信息,请参见w3c_trace_context_propagator类。
更多 OpenTelemetry SDK使用信息,请参考官方文档。
完整示例
下述示例表示使用OpenTelemetry SDK采集Flutter应用程序的Trace数据。
// ignore: depend_on_referenced_packages
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:opentelemetry_sls/api.dart';
import 'package:opentelemetry_sls/sdk.dart' as otel_sdk;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'OTel Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class HttpClientTextMapSetter implements TextMapSetter<Map> {
@override
void set(Map carrier, String key, String value) {
carrier[key] = value;
}
}
class _MyHomePageState extends State<MyHomePage> {
TracerProvider? _provider;
Tracer? _tracer;
final traceContextPropagator = otel_sdk.W3CTraceContextPropagator();
void _initOTelSDK() {
const project = "qs-demos";
const endpoint = "cn-beijing.log.aliyuncs.com";
final exporter =
otel_sdk.CollectorExporter(Uri.parse("https://$project.$endpoint/opentelemetry/v1/traces"), headers: {
"x-sls-otel-project": "$project",
"x-sls-otel-instance-id": "sls-mall",
"x-sls-otel-ak-id": "",
"x-sls-otel-ak-secret": ""
});
final processor = otel_sdk.BatchSpanProcessor(exporter);
final simpleProcessor = otel_sdk.SimpleSpanProcessor(otel_sdk.ConsoleExporter());
_provider = otel_sdk.TracerProviderBase(
processors: [processor, simpleProcessor],
resource: otel_sdk.Resource([
Attribute.fromString("service.name", "main"),
Attribute.fromString("service.namespace", "flutter"),
Attribute.fromString("service.version", "1.0.0"),
Attribute.fromString("deployment.environment", "dev"),
]));
_tracer = _provider!.getTracer('hello-otel-dart');
}
void _simpleSpan() {
final span = _tracer!.startSpan("operation");
// do stuff
// ...
span.end();
}
void _nestedSpan() {
final parent = _tracer!.startSpan("parent operation");
Context.current.withSpan(parent).execute(() {
final child = _tracer!.startSpan("child operation");
// do stuff
// ...
child.end();
});
parent.end();
}
void _spanWithAttribute() {
final span = _tracer!.startSpan("GET /resource/catalog", kind: SpanKind.client);
span.setAttribute(Attribute.fromString("http.method", "GET"));
span.setAttribute(Attribute.fromString("http.url", "your http url"));
// do stuff
// ...
span.end();
}
void _spanWithStatus() {
final span = _tracer!.startSpan("operation");
span.setStatus(StatusCode.error, description: "something error");
// 也可以捕获异常信息
try {
_throwException();
} on Exception catch (e) {
span.recordException(e);
}
span.end();
}
void _throwException() {
throw Exception("Something bad happened!");
}
void _propagateContext() {
final textMapSetter = HttpClientTextMapSetter();
final headers = <String, String>{};
final httpSpan = _tracer!.startSpan("start http request");
Context.current.withSpan(httpSpan).execute(() {
traceContextPropagator.inject(Context.current, headers, textMapSetter);
final client = http.Client();
client.get(Uri.parse("http://sls-mall.caa227ac081f24f1a8556f33d69b96c99.cn-beijing.alicontainer.com/catalogue"),
headers: headers);
});
httpSpan.end();
}
@override
Widget build(BuildContext context) {
Color color = Theme.of(context).primaryColor;
_initOTelSDK();
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
_buildButton(color, 'init', _initOTelSDK),
_buildButton(color, 'simple span', _simpleSpan),
_buildButton(color, 'nested span', _nestedSpan),
_buildButton(color, 'span with attribute', _spanWithAttribute),
_buildButton(color, 'span with status', _spanWithStatus),
_buildButton(color, 'propagate context', _propagateContext),
],
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
Widget _buildButton(Color color, String label, VoidCallback? onPressed) {
return Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
flex: 1,
child: Container(
margin: const EdgeInsets.only(left: 16, top: 8, right: 16),
child: TextButton(
onPressed: onPressed,
style: ButtonStyle(
shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
side: MaterialStateProperty.all(BorderSide(color: color, width: 0.67)),
backgroundColor: MaterialStateProperty.all(Colors.transparent),
padding:
MaterialStateProperty.all(const EdgeInsets.only(left: 12, top: 6, right: 12, bottom: 6))),
child: Text(
label,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w400, color: color),
)),
)),
],
);
}
}