全部產品
Search
文件中心

:通過OpenTelemetry接入Flutter/Dart Trace資料

更新時間:Oct 25, 2024

本文介紹通過OpenTelemetry將Flutter、Dart應用的Trace資料接入到Log Service的操作步驟。

前提條件

已建立Trace執行個體。具體操作,請參見建立Trace執行個體

步驟一:SDK整合

  1. 建立Flutter SDK。具體操作,請參見安裝Flutter SDK

  2. 在專案根目錄下執行以下命令,匯入opentelemetry-dart SDK模組。

    flutter pub add opentelemetry_sls

    匯入成功後,專案的pubspec.yaml檔案中將增加以下資訊,並隱式執行flutter pub get命令。

    dependencies:
     opentelemetry_sls: ^0.15.1
  3. 在指定的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"),
        ]));
  }

變數

說明

樣本

${endpoint}

服務入口是訪問一個Project及其內部資料的URL,Log Service提供私網網域名稱和公網網域名稱。更多資訊,請參見服務入口

cn-hangzhou.log.aliyuncs.com

${project}

Log ServiceProject名稱,更多資訊,請參見管理Project

test-project

${instance}

Trace服務執行個體ID。更多資訊,請參見建立Trace執行個體

test-traces

${access-key-id}

AccessKey ID用於標識使用者,更多資訊,請參見存取金鑰

建議您遵循最小化原則,按需授予RAM使用者必要的許可權。關於授權的具體操作,請參見建立RAM使用者及授權RAM自訂授權樣本

${access-key-secret}

AccessKey Secret是使用者用於加密簽名字串和Log Service用來驗證簽名字串的密鑰,必須保密。

${service.namespace}

服務歸屬的命名空間。

order

${service}

服務名,根據您的實際情境配置。

payment

${version}

服務版本號碼,建議按照va.b.c格式定義。

v1.0.0

${environment}

部署環境,例如測試環境、生產環境。

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.unsetStatusCode.okStatusCode.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),
                  )),
            )),
      ],
    );
  }
}