Flutter
Instrument your Flutter application with OpenTelemetry to send distributed traces to Last9 using the dio HTTP client
Use OpenTelemetry to instrument your Flutter application and send distributed traces to Last9. This guide covers two approaches: header-only trace propagation using dio, and full SDK instrumentation with the opentelemetry Dart package.
Prerequisites
Before setting up Flutter instrumentation, ensure you have:
- Flutter: 3.x with Dart 3.x
- dio: ^5.0.0 (HTTP client)
- Last9 Account: With OpenTelemetry integration credentials (only for Approach B)
Approach A: Traceparent Header Injection (Recommended)
No SDK dependency needed. A single dio interceptor generates a W3C traceparent header on every outbound request. Downstream services pick it up and continue the same trace.
Flutter app Backend Service A Backend Service B │ │ │ │──traceparent: 00-{same-trace-id}──────────────▶│ │ │ │ [single trace visible in Last9 across all services]-
Create the interceptor
Create
lib/telemetry/traceparent_interceptor.dart:import 'dart:math';import 'package:dio/dio.dart';/// Injects a W3C traceparent header into every outbound HTTP request./// Downstream services will continue the same trace automatically.class TraceparentInterceptor extends Interceptor {final Random _rng = Random.secure();String _randomHex(int bytes) => List.generate(bytes,(_) => _rng.nextInt(256).toRadixString(16).padLeft(2, '0'),).join();@overridevoid onRequest(RequestOptions options, RequestInterceptorHandler handler) {// Format: 00-{16-byte trace-id}-{8-byte span-id}-01// 01 = sampled flag (tell downstream to record this trace)final traceId = _randomHex(16); // 32 hex charsfinal spanId = _randomHex(8); // 16 hex charsoptions.headers['traceparent'] = '00-$traceId-$spanId-01';handler.next(options);}} -
Register the interceptor
Add it to your Dio instance. If your app uses a single shared
Dioinstance (common with providers or service classes), add the interceptor there:import 'package:dio/dio.dart';import 'telemetry/traceparent_interceptor.dart';final dio = Dio();dio.interceptors.add(TraceparentInterceptor()); -
Verify
After making a request, check Last9 Traces. Look for the trace ID in downstream service spans — the backend span should show
traceparentin its incoming HTTP attributes, and all services will share the same trace ID.
Approach B: Full OTel SDK
Use this when you need visibility inside the Flutter app itself: HTTP call durations, error details, retry counts, or user-journey traces.
-
Install dependencies
# pubspec.yamldependencies:opentelemetry: ^0.18.10dio: ^5.0.0 -
Initialize OpenTelemetry
Create
lib/telemetry/otel_setup.dart:import 'package:opentelemetry/api.dart';import 'package:opentelemetry/sdk.dart';void initTelemetry({required Uri otlpEndpoint,required String authHeader,String serviceName = 'your-flutter-app',String environment = 'production',}) {final exporter = CollectorExporter(otlpEndpoint,headers: {'authorization': authHeader},);final provider = TracerProviderBase(processors: [BatchSpanProcessor(exporter)],resource: Resource([Attribute.fromString('service.name', serviceName),Attribute.fromString('deployment.environment', environment),]),);registerGlobalTracerProvider(provider);}Call it from
main.dartbeforerunApp:void main() {WidgetsFlutterBinding.ensureInitialized();initTelemetry(otlpEndpoint: Uri.parse('https://otlp.last9.io'),authHeader: 'Basic YOUR_BASE64_CREDENTIALS',);runApp(const MyApp());} -
Create the instrumented dio interceptor
Create
lib/telemetry/otel_dio_interceptor.dart:import 'package:dio/dio.dart';import 'package:opentelemetry/api.dart';/// TextMapSetter implementation for dio headers.class _DioHeaderSetter implements TextMapSetter<Map<String, dynamic>> {@overridevoid set(Map<String, dynamic> carrier, String key, String value) {carrier[key] = value;}}class OtelDioInterceptor extends Interceptor {final Tracer _tracer = globalTracerProvider.getTracer('flutter-http');final Map<RequestOptions, Span> _spans = {};final _setter = _DioHeaderSetter();@overridevoid onRequest(RequestOptions options, RequestInterceptorHandler handler) {final span = _tracer.startSpan('${options.method} ${options.path}',kind: SpanKind.client,attributes: [Attribute.fromString('http.method', options.method),Attribute.fromString('http.url', options.uri.toString()),Attribute.fromString('http.host', options.uri.host),],);// Inject W3C traceparent for downstream propagationfinal ctx = contextWithSpan(Context.current, span);W3CTraceContextPropagator().inject(ctx, options.headers, _setter);_spans[options] = span;handler.next(options);}@overridevoid onResponse(Response response, ResponseInterceptorHandler handler) {final span = _spans.remove(response.requestOptions);if (span != null) {span.setAttribute(Attribute.fromInt('http.status_code', response.statusCode ?? 0));span.end();}handler.next(response);}@overridevoid onError(DioException err, ErrorInterceptorHandler handler) {final span = _spans.remove(err.requestOptions);if (span != null) {span.setAttribute(Attribute.fromString('error.message', err.message ?? 'unknown'));if (err.response?.statusCode != null) {span.setAttribute(Attribute.fromInt('http.status_code', err.response!.statusCode!));}span.setStatus(StatusCode.error, err.message ?? '');span.end();}handler.next(err);}} -
Register the interceptor
dio.interceptors.add(OtelDioInterceptor()); -
Verify
In Last9 Traces, filter by
service.name = your-flutter-app. You should see spans for each HTTP call with method, URL, status code, and duration. Downstream services will share the same trace ID.
Choosing Between the Two Approaches
Use Approach A (traceparent injection) when you need distributed traces across Flutter and backend services without adding new dependencies.
Use Approach B (full OTel SDK) when you need spans from inside the Flutter app — HTTP call durations, error details, retry visibility, or user-journey tracing.
Troubleshooting
Traces Not Connecting Across Services
traceparent header not reaching backend:
If your backend is behind a reverse proxy or load balancer, ensure it forwards custom HTTP headers. Some proxies strip unknown headers by default — check your proxy’s header passthrough configuration.
Traces appear disconnected:
Ensure the traceparent header format is correct: 00-{32 hex}-{16 hex}-01. The version must be 00, and the flags byte 01 means “sampled”. If flags is 00, downstream services may not record the trace.
Full SDK Issues (Approach B)
Export fails on mobile:
The CollectorExporter uses HTTP/protobuf which works on most mobile networks. If you see connection timeouts, check that your app has internet permissions and the endpoint is reachable from the device network.
High memory usage from span tracking:
The _spans map in OtelDioInterceptor grows if responses are never received (e.g., cancelled requests). Add cleanup logic for stale spans or use dio’s cancel token to clean up properly.
Production Considerations
Crash Recording
Wrap your app entry point to capture unhandled exceptions as spans:
void main() { runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); await initTelemetry(); // Approach B only runApp(const MyApp()); }, (error, stack) { // Record crash as a span (Approach B) final tracer = globalTracerProvider.getTracer('flutter-app'); final span = tracer.startSpan('app.crash'); span.setStatus(StatusCode.error, error.toString()); span.setAttribute(Attribute.fromString('error.stack', stack.toString())); span.end(); });}Flush on Background
Ensure telemetry is flushed before the app goes to the background. Undelivered spans are lost if the OS kills the app:
class MyApp extends StatefulWidget { @override State<MyApp> createState() => _MyAppState();}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); }
@override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { // Force flush pending spans before backgrounding (globalTracerProvider as TracerProviderBase).forceFlush(); } }
@override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); }}Need Help?
If you encounter any issues or have questions:
- Join our Discord community for real-time support
- Contact our support team at support@last9.io