otlp_dart
OpenTelemetry Protocol (OTLP) client library for Dart. Export traces, metrics, and logs to OTLP-compatible backends like .NET Aspire Dashboard, Jaeger, Prometheus, and more.
Features
- ✅ OTLP/HTTP support - Send telemetry via HTTP with JSON encoding
- ✅ OTLP/HTTP2 with Protobuf - High-performance binary encoding for Aspire
- ✅ Distributed Tracing - Create spans, nested spans, and distributed traces
- ✅ Structured Logging - Emit structured logs with attributes
- ✅ Comprehensive Metrics - Full SDK with all instrument types
- Counter (monotonically increasing)
- UpDownCounter (can increase or decrease)
- Histogram (value distributions with buckets)
- ObservableGauge (callback-based current values)
- ObservableCounter (callback-based cumulative values)
- ✅ Batch Processing - Efficient batch export with configurable timing
- ✅ Resource Attributes - Identify your service with rich metadata
- ✅ .NET Aspire Dashboard - First-class support for Aspire
- ✅ Automatic Context Propagation - Parent-child span relationships
- ✅ Exception Recording - Automatic exception capture in spans
- ✅ Delta Temporality - Efficient metric reporting with automatic reset
Getting Started
Installation
Add this to your pubspec.yaml:
dependencies:
otlp_dart: ^0.1.0
Then run:
dart pub get
Quick Start with .NET Aspire Dashboard
1. Start the Aspire Dashboard
Using Docker:
docker run --rm -it -p 18888:18888 -p 18889:18889 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
Or if you have .NET Aspire installed:
dotnet run --project YourAspireProject
2. Use the Library
import 'package:otlp_dart/otlp_dart.dart';
import 'package:otlp_dart/src/sdk/trace/tracer_provider_impl.dart';
import 'package:otlp_dart/src/sdk/trace/span_processor.dart';
import 'package:otlp_dart/src/sdk/logs/logger_provider_impl.dart';
import 'package:otlp_dart/src/sdk/logs/log_processor.dart';
void main() async {
// Create a resource identifying your service
final resource = Resource.create(
serviceName: 'my-dart-app',
serviceVersion: '1.0.0',
);
// Setup trace exporter
final traceExporter = OtlpHttpTraceExporter.aspire(
host: 'localhost',
port: 18889,
);
final tracerProvider = TracerProviderImpl(
resource: resource,
processor: BatchSpanProcessor(exporter: traceExporter),
);
// Setup log exporter
final logExporter = OtlpHttpLogExporter.aspire(
host: 'localhost',
port: 18889,
);
final loggerProvider = LoggerProviderImpl(
resource: resource,
processor: BatchLogRecordProcessor(exporter: logExporter),
);
// Get tracer and logger
final tracer = tracerProvider.getTracer('my-app');
final logger = loggerProvider.getLogger('my-app');
// Create traces
await tracer.withSpanAsync('process-request', (span) async {
span.setAttribute('user.id', AttributeValue.string('123'));
logger.info('Processing request');
// Do some work
await Future.delayed(Duration(milliseconds: 100));
span.setStatus(SpanStatus.ok());
});
// Cleanup
await tracerProvider.forceFlush();
await loggerProvider.forceFlush();
await tracerProvider.shutdown();
await loggerProvider.shutdown();
}
3. View Your Telemetry
Open the Aspire Dashboard at http://localhost:18888 and explore:
- Traces tab - View distributed traces and spans
- Structured tab - Browse structured logs
- Metrics tab - See metrics and gauges
Usage Examples
Tracing
Basic Span
final span = tracer.startSpan('my-operation');
span.setAttribute('key', AttributeValue.string('value'));
// Do work...
span.end();
Automatic Span Management
await tracer.withSpanAsync('my-operation', (span) async {
span.setAttribute('key', AttributeValue.string('value'));
// Span automatically ends when function completes
await doWork();
});
Nested Spans
await tracer.withSpanAsync('parent-operation', (parentSpan) async {
parentSpan.setAttribute('type', AttributeValue.string('parent'));
// Create child span
await tracer.withSpanAsync(
'child-operation',
(childSpan) async {
childSpan.setAttribute('type', AttributeValue.string('child'));
await doChildWork();
},
parent: parentSpan, // Link to parent
);
});
Error Handling
try {
await tracer.withSpanAsync('risky-operation', (span) async {
throw Exception('Something went wrong!');
});
} catch (e) {
// Exception is automatically recorded in the span
print('Error: $e');
}
Manual Exception Recording
final span = tracer.startSpan('my-operation');
try {
throw Exception('Error!');
} catch (e, stackTrace) {
span.recordException(e, stackTrace: stackTrace);
span.setStatus(SpanStatus.error('Operation failed'));
} finally {
span.end();
}
Span Events
final span = tracer.startSpan('processing');
span.addEvent('validation-started');
// ... do validation ...
span.addEvent('validation-completed', attributes: {
'items': AttributeValue.int(42),
});
span.end();
Span Links (Distributed Tracing)
// First operation in service A
final span1 = tracer.startSpan('fetch-data');
// ... work ...
span1.end();
// Second operation in service B, linked to first
final span2 = tracer.startSpan(
'process-data',
links: [SpanLink(context: span1.context)],
);
span2.end();
Metrics
Setting up Metrics
// Create resource
final resource = Resource(
attributes: [
Attribute('service.name', AttributeValue.string('my-service')),
Attribute('service.version', AttributeValue.string('1.0.0')),
],
);
// Create exporter
final exporter = OtlpHttpMetricExporter.aspire(
host: 'localhost',
port: 18889,
);
// Create metric reader with periodic export (every 60 seconds)
final reader = PeriodicMetricReader(
exporter: exporter,
resource: resource,
scope: InstrumentationScope(name: 'my-app', version: '1.0.0'),
interval: const Duration(seconds: 60),
);
// Create meter provider
final meterProvider = MeterProviderImpl(
resource: resource,
reader: reader,
);
// Get a meter
final meter = meterProvider.getMeter('my-app');
Counter - Monotonically Increasing Values
final requestCounter = meter.createCounter(
'http.server.requests',
unit: 'requests',
description: 'Total number of HTTP requests',
);
// Increment counter with attributes
requestCounter.add(1, attributes: {
'http.method': AttributeValue.string('GET'),
'http.route': AttributeValue.string('/api/users'),
'http.status_code': AttributeValue.int(200),
});
UpDownCounter - Values That Can Increase or Decrease
final activeConnections = meter.createUpDownCounter(
'http.server.active_connections',
unit: 'connections',
description: 'Number of active HTTP connections',
);
// Connection opened
activeConnections.add(1);
// Connection closed
activeConnections.add(-1);
Histogram - Value Distributions
final requestDuration = meter.createHistogram(
'http.server.duration',
unit: 'ms',
description: 'HTTP request duration',
);
// Record request duration
requestDuration.record(42.5, attributes: {
'http.method': AttributeValue.string('GET'),
'http.route': AttributeValue.string('/api/users'),
});
// Histogram automatically calculates:
// - Count of measurements
// - Sum of all values
// - Min and max values
// - Distribution across bucket boundaries
ObservableGauge - Current Value via Callback
var memoryUsage = 0.0;
final memoryGauge = meter.createObservableGauge(
'process.runtime.dart.memory',
() => memoryUsage,
unit: 'bytes',
description: 'Current memory usage',
);
// Update value as needed
memoryUsage = 1024.0 * 1024.0; // 1 MB
// Value is automatically reported during metric collection
ObservableCounter - Cumulative Value via Callback
var totalBytes = 0;
final bytesCounter = meter.createObservableCounter(
'network.bytes.sent',
() => totalBytes,
unit: 'bytes',
description: 'Total bytes sent',
);
// Update cumulative value
totalBytes += 1024;
// Delta is automatically calculated during collection
Complete Metrics Example
// Create instruments
final requestCounter = meter.createCounter('http.requests');
final requestDuration = meter.createHistogram('http.duration', unit: 'ms');
final activeConnections = meter.createUpDownCounter('http.connections');
// Simulate HTTP request
activeConnections.add(1); // Connection opened
requestCounter.add(1, attributes: {
'method': AttributeValue.string('GET'),
'route': AttributeValue.string('/api/users'),
});
requestDuration.record(156.7, attributes: {
'method': AttributeValue.string('GET'),
'route': AttributeValue.string('/api/users'),
});
activeConnections.add(-1); // Connection closed
// Force export
await meterProvider.forceFlush();
// Cleanup
await meterProvider.shutdown();
Logging
Basic Logging
logger.info('Application started');
logger.debug('Debug information');
logger.warn('Warning message');
logger.error('Error occurred');
Structured Logging with Attributes
logger.info('User logged in', attributes: {
'user.id': AttributeValue.string('12345'),
'user.email': AttributeValue.string('[email protected]'),
'login.method': AttributeValue.string('oauth'),
});
Correlated Logs (with Trace Context)
await tracer.withSpanAsync('operation', (span) async {
logger.log(
Severity.info,
'Operation in progress',
traceId: span.context.traceId,
spanId: span.context.spanId,
);
});
Resource Configuration
Resources identify your service and provide context for all telemetry:
final resource = Resource.create(
serviceName: 'my-service',
serviceVersion: '1.2.3',
serviceInstanceId: 'pod-123',
additionalAttributes: {
'environment': 'production',
'datacenter': 'us-west-2',
'host.name': 'server-01',
'deployment.id': 'v1.2.3-20240101',
},
);
Batch Processing Configuration
Control how often telemetry is exported:
final processor = BatchSpanProcessor(
exporter: traceExporter,
maxQueueSize: 2048, // Max spans to queue
maxExportBatchSize: 512, // Max spans per batch
scheduledDelayMillis: Duration(seconds: 5), // Export interval
);
Custom OTLP Endpoint
Use with any OTLP-compatible backend:
final exporter = OtlpHttpTraceExporter(
endpoint: 'https://otlp.example.com/v1/traces',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'X-Custom-Header': 'value',
},
timeout: Duration(seconds: 30),
);
OTLP Backends Compatibility
This library is compatible with any OTLP-compliant backend:
.NET Aspire Dashboard
- Default ports: 18888 (UI), 18889 (OTLP)
- Use:
OtlpHttpTraceExporter.aspire()
Jaeger
final exporter = OtlpHttpTraceExporter(
endpoint: 'http://localhost:4318/v1/traces',
);
Prometheus + OTLP Receiver
final exporter = OtlpHttpMetricExporter(
endpoint: 'http://localhost:4318/v1/metrics',
);
Grafana Tempo
final exporter = OtlpHttpTraceExporter(
endpoint: 'https://tempo.example.com/v1/traces',
headers: {'Authorization': 'Basic YOUR_TOKEN'},
);
Grafana Loki
final exporter = OtlpHttpLogExporter(
endpoint: 'https://loki.example.com/v1/logs',
headers: {'Authorization': 'Basic YOUR_TOKEN'},
);
OpenTelemetry Collector
// Send to OTEL Collector
final exporter = OtlpHttpTraceExporter(
endpoint: 'http://otel-collector:4318/v1/traces',
);
Architecture
┌─────────────────┐
│ Application │
└────────┬────────┘
│
├─────────► TracerProvider ─► Tracer ─► Span
│ │
├─────────► LoggerProvider ─► Logger ─► LogRecord
│ │
└─────────► MeterProvider ─► Meter ─► Metric
│
▼
SpanProcessor/
LogProcessor
│
▼
Batch Processor
│
▼
OTLP HTTP Exporter
│
▼
Backend (Aspire/Jaeger/etc)
Advanced Topics
Sampling
Control which spans are recorded:
// Future enhancement - sampling will be added
Context Propagation
Automatically maintain parent-child relationships:
// Parent span
await tracer.withSpanAsync('parent', (parent) async {
// Child automatically inherits parent context
await tracer.withSpanAsync(
'child',
(child) async { /* work */ },
parent: parent,
);
});
Span Kinds
Use appropriate span kinds:
// Server receiving a request
tracer.startSpan('handle-request', kind: SpanKind.server);
// Client making a request
tracer.startSpan('http-call', kind: SpanKind.client);
// Internal operation
tracer.startSpan('compute', kind: SpanKind.internal);
// Message producer
tracer.startSpan('publish', kind: SpanKind.producer);
// Message consumer
tracer.startSpan('consume', kind: SpanKind.consumer);
Best Practices
1. Use Resource Attributes
Always identify your service:
final resource = Resource.create(
serviceName: 'your-service-name',
serviceVersion: '1.0.0',
);
2. Use Semantic Attributes
Follow OpenTelemetry semantic conventions:
span.setAttribute('http.method', AttributeValue.string('GET'));
span.setAttribute('http.url', AttributeValue.string('https://api.example.com'));
span.setAttribute('http.status_code', AttributeValue.int(200));
3. Always Flush on Shutdown
await tracerProvider.forceFlush();
await tracerProvider.shutdown();
4. Use Batch Processing
For better performance:
BatchSpanProcessor(exporter: exporter) // ✓ Good
SimpleSpanProcessor(exporter) // ✗ Avoid in production
5. Handle Errors Gracefully
try {
await tracer.withSpanAsync('operation', (span) async {
await riskyOperation();
});
} catch (e) {
// Exception already recorded in span
logger.error('Operation failed: $e');
}
Examples
See the /example directory for complete examples:
aspire_example.dart- Complete .NET Aspire integration with traces and logsmetrics_example.dart- Comprehensive metrics example with all instrument typeshttp_client_example.dart- HTTP client instrumentation examplecomposable_http_client_example.dart- Advanced composable HTTP client example
Run examples:
dart run example/metrics_example.dart
dart run example/aspire_example.dart
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Resources
Support
For issues and questions:
- GitHub Issues: https://github.com/jamiewest/otlp-dart/issues
- OpenTelemetry Community: https://opentelemetry.io/community/
Libraries
- otlp_dart
- OpenTelemetry Protocol (OTLP) client library for Dart.