purple_otel_dio 0.1.3
purple_otel_dio: ^0.1.3 copied to clipboard
OpenTelemetry auto-instrumentation for Dio HTTP client — automatic CLIENT spans with W3C trace context.
purple_otel_dio #
Zero-code distributed tracing for every Dio HTTP request.
Add one line to your Dio instance and every outbound request becomes a fully traced
OpenTelemetry span — complete with W3C trace context
propagation, semantic HTTP attributes, error recording, and automatic span lifecycle
management. No manual span creation. No boilerplate. Just plug it in and your HTTP
traces appear in your backend.
Features #
- Automatic
CLIENTspans — every request creates an OTel span namedGET api.example.com/users, tagged withSpanKind.client, via Dio's interceptor chain - W3C
traceparentinjection — trace context propagates automatically in outgoing request headers so downstream services link correctly in distributed traces - HTTP semantic attributes —
http.method,http.url,http.host, andhttp.status_codeare set on every span, conforming to the OpenTelemetry HTTP semantic conventions - Error recording —
DioExceptionfailures capture the exception message, type, stack trace (span.recordException), and mark the span as errored - Status mapping — 4xx and 5xx responses automatically set the span to error status; 2xx responses mark it ok
- Extension method —
.addOtelInterceptor(tracer)on anyDioinstance — no subclassing, no mixins, no DI tricks - Powered by
purple_otel_sdk— leverages the full OpenTelemetry SDK (sampling, batch processing, OTLP export, context propagation) under the hood
Installation #
Add to your pubspec.yaml:
dependencies:
purple_otel_dio: ^0.1.0
purple_otel_sdk: ^0.1.0 # required peer — provides Tracer, Span, exporters
dio: ^5.7.0 # the HTTP client being instrumented
Then run:
dart pub get
Quick Start #
Below is a complete, runnable example that wires up a Dio client with tracing
and exports spans to an OTLP collector (e.g. Jaeger, Grafana Tempo, Azure Monitor).
import 'package:dio/dio.dart';
import 'package:purple_otel_sdk/purple_otel_sdk.dart';
import 'package:purple_otel_dio/purple_otel_dio.dart';
Future<void> main() async {
// 1. Create the OTel tracer provider with an OTLP exporter
final tracerProvider = SDKTracerProvider(
resource: Resource(
attributes: [
Attribute('service.name', AttributeValue.string('my-flutter-app')),
Attribute('service.version', AttributeValue.string('1.0.0')),
],
),
processors: [
BatchSpanProcessor(
OtlpHttpSpanExporter(
endpoint: Uri.parse('http://localhost:4318/v1/traces'),
),
),
],
);
// 2. Get a named tracer
final tracer = tracerProvider.get('http-client');
// 3. Create Dio and add the interceptor — one line
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..addOtelInterceptor(tracer);
// 4. Every request automatically produces a traced span
final users = await dio.get('/users');
final posts = await dio.get('/posts');
final result = await dio.post('/search', data: {'query': 'Flutter'});
print('Status: ${users.statusCode}');
// 5. Shut down to flush pending spans
await tracerProvider.shutdown();
}
What happens at runtime:
dio.get('/users')fires → interceptor creates a span namedGET api.example.com/users- A
traceparentheader is injected into the request (e.g.00-<trace-id>-<span-id>-01) - The response arrives →
http.status_code=200is recorded, span marked OK, span ended - All three spans are exported as a batch to
localhost:4318/v1/traces
Before / After #
Without purple_otel_dio (~25 lines per endpoint) #
Future<Response> getUsers(Dio dio, Tracer tracer) async {
final span = tracer.startSpan('GET api.example.com/users',
kind: SpanKind.client);
span.setAttribute('http.method', AttributeValue.string('GET'));
span.setAttribute('http.url',
AttributeValue.string('https://api.example.com/users'));
final carrier = <String, String>{};
W3CTraceContextPropagator.inject(
Context.root.withValue(spanContextKey, span), carrier);
try {
final response = await dio.get('/users',
options: Options(headers: carrier));
span.setAttribute('http.status_code',
AttributeValue.int(response.statusCode ?? 0));
span.setStatus(response.statusCode == 200
? SpanStatus.ok
: SpanStatus.error('HTTP ${response.statusCode}'));
return response;
} on DioException catch (e) {
span.recordException(e, stackTrace: e.stackTrace);
span.setStatus(SpanStatus.error('${e.type.name}: ${e.message}'));
rethrow;
} finally {
span.end();
}
}
With purple_otel_dio (1 line, once) #
final dio = Dio()..addOtelInterceptor(tracer);
// That's it. All get/post/put/delete requests are traced.
final users = await dio.get('/users');
final posts = await dio.get('/posts');
final result = await dio.post('/search', data: {'query': 'Flutter'});
Savings: ~25 lines of manual instrumentation per endpoint → 1 line of setup total. For an app with 10 API calls, that's ~250 lines eliminated.
Interceptor Lifecycle #
| Dio Hook | OTel Action | Details |
|---|---|---|
onRequest |
Create span | Starts a new CLIENT span named {METHOD} {host}{path}, sets http.method, http.url, http.host attributes |
onRequest |
Inject traceparent | Uses W3CTraceContextPropagator.inject() to add traceparent (and optionally tracestate) to the request headers |
onRequest |
Store span | Saves the span reference in options.extra['otel_span'] for retrieval in onResponse / onError |
onResponse |
Record status | Sets http.status_code attribute from the response |
onResponse |
Map status | statusCode >= 400 → span error; otherwise → span OK |
onResponse |
End span | Calls span.end(), closing the span timing |
onError |
Record exception | Calls span.recordException(err, stackTrace: err.stackTrace) |
onError |
Set error | Marks the span status as error with {type}: {message} |
onError |
End span | Calls span.end() so the error span is exported |
How It Works #
┌─────────────────────────────────┐
│ Your Flutter App │
│ │
│ dio.get('/users') │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ OtelDioInterceptor │ │
│ │ │ │
│ │ onRequest: │ │
│ │ 1. tracer.startSpan() │ │
│ │ 2. set http.* attributes│ │
│ │ 3. inject traceparent │──────── traceparent: 00-abc123-def456-01
│ │ 4. store span in extra │ │
│ │ │ │
│ │ onResponse: │ │
│ │ 5. set http.status_code │ │
│ │ 6. set span status │ │
│ │ 7. span.end() │ │
│ │ │ │
│ │ onError: │ │
│ │ 8. recordException() │ │
│ │ 9. set span status │ │
│ │ 10. span.end() │ │
│ └──────────┬───────────────┘ │
│ │ │
└─────────────┼───────────────────┘
│
┌─────────────▼───────────────────┐
│ SpanProcessor Pipeline │
│ │
│ SimpleSpanProcessor / │
│ BatchSpanProcessor │
│ │ │
│ ▼ │
│ SpanExporter (OTLP / Console) │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ OTLP Collector / Backend │
│ (Jaeger, Grafana Tempo, etc.) │
└─────────────────────────────────┘
- Your code calls
dio.get(...)as usual - Dio invokes
onRequest— the interceptor creates an OTel span, attaches HTTP attributes, and injects thetraceparentheader so the receiving service can continue the trace - Dio sends the HTTP request (now with distributed tracing headers)
- When the response arrives,
onResponserecords the status code, maps the span status, and ends the span - If the request fails,
onErrorrecords the exception with stack trace and ends the span as errored - The finished span flows through the configured
SpanProcessorpipeline and is exported to your observability backend
Real-World Example #
A typical Flutter app with multiple API endpoints — all traced with zero ceremony:
import 'package:dio/dio.dart';
import 'package:purple_otel_sdk/purple_otel_sdk.dart';
import 'package:purple_otel_dio/purple_otel_dio.dart';
class ApiClient {
late final Dio _dio;
ApiClient({required Tracer tracer}) {
_dio = Dio(BaseOptions(baseUrl: 'https://jsonplaceholder.typicode.com'))
..addOtelInterceptor(tracer);
}
Future<Response> getUsers() => _dio.get('/users');
Future<Response> getUser(int id) => _dio.get('/users/$id');
Future<Response> createPost(Map<String, dynamic> data) =>
_dio.post('/posts', data: data);
Future<Response> updatePost(int id, Map<String, dynamic> data) =>
_dio.put('/posts/$id', data: data);
Future<Response> deletePost(int id) => _dio.delete('/posts/$id');
}
Future<void> main() async {
final tracerProvider = SDKTracerProvider(
resource: Resource.empty,
processors: [SimpleSpanProcessor(ConsoleSpanExporter())],
);
final tracer = tracerProvider.get('api-client');
final api = ApiClient(tracer: tracer);
// Each of these creates a distinct span with full HTTP attributes:
//
// GET jsonplaceholder.typicode.com/users (200 OK)
// GET jsonplaceholder.typicode.com/users/1 (200 OK)
// POST jsonplaceholder.typicode.com/posts (201 Created)
// PUT jsonplaceholder.typicode.com/posts/1 (200 OK)
// DELETE jsonplaceholder.typicode.com/posts/1 (200 OK)
await api.getUsers();
await api.getUser(1);
await api.createPost({'title': 'Hello', 'body': 'World', 'userId': 1});
await api.updatePost(1, {'title': 'Updated'});
await api.deletePost(1);
await tracerProvider.shutdown();
}
Every endpoint call above generates a properly-named span with the correct
HTTP method, full URL, status code, and W3C trace context — all without a
single line of manual instrumentation in ApiClient.
Companion Packages #
purple_otel_dio is part of the Purple OTel ecosystem:
| Package | Description |
|---|---|
purple_otel_api |
OpenTelemetry API contracts — Tracer, Span, Context, Attribute, etc. |
purple_otel_sdk |
OpenTelemetry SDK implementation — SDKTracerProvider, exporters, processors, samplers |
purple_otel_dio |
This package — auto-instrumentation for Dio HTTP client |
purple_otel_flutter |
Flutter-specific instrumentation — navigation observers, widget lifecycle spans |
purple_logger |
Structured logging framework |
purple_logger_otel |
Bridge: send purple_logger records as OTel log records |
purple_logger_otel_sdk |
OTel SDK log exporter integration for purple_logger |
Built by PurpleSoft #
PurpleSoft S.r.l. — software house with offices in Monza, Milano, and Lugano (Switzerland). Since 2017.
We build what doesn't exist yet.
The sectors we dominate #
Database & Storage Engines. We design, implement, and ship production-grade databases. Our LSM-tree storage engine — built in Rust for performance, exposed via FFI to Dart/Flutter, compiled to WASM for web — uses WiscKey-style key-value separation with concurrent compaction and crash recovery. It runs on all 6 platforms. This is not a wrapper around SQLite. It is a ground-up embedded NoSQL database written from scratch.
Conversational AI & Voice Assistants. We ship end-to-end AI voice platforms — from the physical device (ESP32 with custom Opus codec firmware) to the cloud backend (.NET 10, ASP.NET Core, Blazor Server) to the mobile companion app (Flutter with BLE). Our multi-agent LLM architecture orchestrates specialized agents (conversation, memory, content enrichment, research) with a multi-layered memory system spanning graph, episodic, and working memory — including adaptive forgetting, poison detection via statistical outlier analysis, and automatic episodic-to-semantic compression. Our content intelligence pipelines ingest, enrich via LLM, embed (1024-dim vectors), and serve via hybrid semantic search with HNSW indexing and training-free chunk pre-filtering — 28,000+ items enriched, 30,000+ embeddings generated.
Fintech & Payments. We build payment orchestration layers that abstract multiple gateways and cryptocurrencies behind a single API. Our engineers have shipped POS terminal firmware, fiscal receipt systems compliant with Italian regulatory standards, and cash register management platforms processing millions of transactions.
Cybersecurity & Identity. We ship post-quantum cryptography implementations using NIST-standardized algorithms on .NET 10 — the cryptography standard that will replace RSA and ECC. Our authentication infrastructure integrates SPID (Italian public digital identity) and OAuth 2.0/OpenID Connect across every major identity provider. We build digital signature platforms with PKCS#11 HSM support handling the full envelope lifecycle.
Artificial Intelligence & On-Device ML. We deploy ONNX models to phones via custom Dart runtime bindings with GPU acceleration. We integrate on-device LLM inference engines. We build neural text-to-speech engines that run across all 6 Flutter platforms and speech recognition systems with Italian dialect support. Our scientific research pipeline uses a multi-scorer verification chain with consensus voting to ensure factual accuracy in LLM outputs.
Enterprise SaaS & Cloud-Native Architecture. We architect and operate platforms at enterprise scale. Our .NET 10 monorepo spans 239 C# projects with consistent CI/CD, 297 test suites, zero build errors. We design multi-engine database abstraction layers (PostgreSQL, MySQL, Microsoft SQL Server) with automated schema-to-code generation. Our notification engine handles 6 channels with DNS-based email validation.
IoT & Embedded Systems. We write ESP32 firmware targeting ESP-IDF v5.4 with 21 FreeRTOS tasks, I²S audio pipelines, Opus codec integration, and a validated WiFi state machine. We build certificate authority infrastructure for device TLS. We write native Flutter plugins for hardware that doesn't have one yet.
Observability & DevOps. Full-stack observability is the foundation of our client solutions. We ship a complete OpenTelemetry SDK for Dart/Flutter (traces, logs, metrics, W3C propagation, OTLP export), enterprise structured logging with file rotation, and auto-instrumentation — all red-team audited with 256 automated tests.
The technologies we master #
Our engineering team works across the full stack — from ESP32 firmware to cloud-native backends to cross-platform mobile apps. We write production code in Rust (database engines, FFI), C# (.NET 10), Dart/Flutter, C (ESP-IDF, audio codecs), TypeScript, Python, and C++. Our frameworks of choice are ASP.NET Core, Blazor Server, Flutter, Angular, and React. We operate Microsoft Azure (Key Vault, Blob Storage, IoT Hub), deploy on NGINX, and manage PostgreSQL + pgvector, MySQL, Microsoft SQL Server, and ESP-IDF at scale. Our database layer uses EF Core with Npgsql, our embedded engine uses Rust + dart:ffi + WASM. Our AI stack spans LLMs, embeddings, vector search, semantic kernel, and multi-agent orchestration. We use FFmpeg for audio, LVGL for embedded displays, and BLE for device provisioning. Our CI/CD runs on Azure Pipelines.
Microsoft Partner since 2018 · SumUp Partner · Dell Partner #
Trusted by #
ABB Intesa Sanpaolo Tenaris Reply Aubay Comune di Milano BCC FIMAP Alten Altran Prometeia illimity Be Shaping the Future DS Group NVALUE Inoptim Docflow P&C
and 40+ other enterprises across banking, manufacturing, energy, and public sector.
Your project can't wait. We've solved these exact problems for companies you know. Let's solve them for you.
🌐 purplesoft.io · 📧 developers@purplesoft.io · 📞 +39 0362 148 3978 · 💼 LinkedIn · 🐙 GitHub
License #
GNU Affero General Public License v3.0 — see LICENSE for the full text.