通过OpenTelemetry接入Flutter/Dart Trace数据

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

前提条件

已创建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,日志服务提供私网域名和公网域名。更多信息,请参见服务入口

cn-hangzhou.log.aliyuncs.com

${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

${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),
                  )),
            )),
      ],
    );
  }
}