本文介紹通過OpenTelemetry將Flutter、Dart應用的Trace資料接入到Log Service的操作步驟。
前提條件
已建立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,Log Service提供私網網域名稱和公網網域名稱。更多資訊,請參見服務入口。 | cn-hangzhou.log.aliyuncs.com |
| Log ServiceProject名稱,更多資訊,請參見管理Project。 | test-project |
| Trace服務執行個體ID。更多資訊,請參見建立Trace執行個體。 | test-traces |
| AccessKey ID用於標識使用者,更多資訊,請參見存取金鑰。 建議您遵循最小化原則,按需授予RAM使用者必要的許可權。關於授權的具體操作,請參見建立RAM使用者及授權,RAM自訂授權樣本。 | 無 |
| AccessKey Secret是使用者用於加密簽名字串和Log Service用來驗證簽名字串的密鑰,必須保密。 | 無 |
| 服務歸屬的命名空間。 | 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),
)),
)),
],
);
}
}