OpenTelemetry SDK for Dart

License OpenTelemetry Specification Coverage

Dartastic is an OpenTelemetry SDK to add standard observability to Dart applications. Dartastic can be used with any OTel backend since it's standards-compliant.

Dartastic supports all Dart and Flutter targets including, mobile, desktop, web and wasm.

The Dartastic OTel SDK is in the process of being Donation to the CNCF to become the official standard for Dart OpenTelemetry.

Flutter developers should use the Flutterific OpenTelemetry SDK which builds on top of Dartastic OTel.

Dartastic and Flutterrific OTel are made with ๐Ÿ’™ by Michael Bushe at Mindful Software

Commercial Support

Dartastic.io provides an OpenTelemetry support, training, consulting and an Observability backend customized for Flutter apps, Dart backends, and any other service or process that produces OpenTelemetry data.

  • Dartastic Cloud - your Observability platform of choice integrated with your Dart and Flutter builds. See source stack traces from production errors in your observability platform for Dart and Flutter without revealing your source code (debug symbols, source maps) to your Observability vendors.
  • Dartastic Pub Dev
    • Use a private pub dev server to share package with your team or partners, managed access with a familiar pub.dev feel.
    • Store debug symbols and source maps with Dartastic Cloud and turn your production errors into source code lines right in your Observability platform. See source code in the UI, send source code lines with tickets or alerts. Squash bugs fast.
  • Pro Dartastic OTel libraries
    • access the Dartastic Pro professionally maintained package that is has features and fixes not available in the open source library.
    • access to packages with advanced features, such PII protection, not available in the open source offering.
    • access instrumented versions of Dart and Flutter libraries, such as dio and go_router.
  • Paid support to deliver with confidence.
  • Training on OTel for Dart, Flutter on GCP and other platforms.
  • Professional consulting in Dart, Flutter, Observability and AI development.

Features

  • ๐Ÿš€ Friendly API: An easy to use, discoverable, immutable, typesafe API that feels familiar to Dart developers.
  • ๐Ÿ“ Standards Compliant: Complies with the OpenTelemetry specification so it's portable and future-proof.
  • ๐ŸŒŽ Ecosystem:
    • Dartastic.io is an OTel backend for Dart with a generous free tier, professional support and enterprise features.
    • Flutterrific OTel adds Dartastic OTel to Flutter apps with ease. Observe app routes, errors, web vitals and more with as few as two lines of code.
  • ๐Ÿ’ช๐Ÿป Powerful:
    • Propagate OpenTelemetry Context across async gaps and Isolates.
    • Pick from a rich set of Samplers including On/Off, probability and rate-limiting.
    • Automatically capture platform resources on initialization.
    • No skimping - If it's optional in the spec, it's included in Dartastic.
    • A pluggable and extensible API and SDK enables implementation freedom.
  • ๐Ÿงท Typesafe Semantics: Ensure you're speaking the right language with a massive set of enums matching the OpenTelemetry Semantics Conventions.
  • ๐Ÿ“Š Excellent Performance:
    • Low overhead
    • Batch processing
    • Performance test suite for proven benchmarks
  • ๐Ÿž Well Tested: Very good test coverage (>90%).
  • ๐Ÿ“ƒ Quality Documentation: If it's not clearly documented, it's a bug. Extensive examples and best practices are provided. See the examples directory.
  • ๐ŸŽฌ Demo The Wonderous OpenTelemetry Demo demonstrates
    gskinner's Wonderous App with instrumentation for OpenTelemetry.
  • โœ… Supported Telemetry Signals and Features:
    • Tracing with span processors and samplers
    • Metrics collection and aggregation
    • Logs with log record processors and exporters
    • Context propagation
    • Baggage management and optional BaggageSpanProcessor to automatically copy baggage entries as span attributes

Dartastic.io offers a Pro OTel libraries, a private pub.dev server for your team and partners integrated with your OTel backend - get source code lines for production errors immediately.

Flutterrific OTel adds Dartastic OTel to Flutter apps with ease.

Dartastic OTel is suitable for Dart backends, CLIs or any Dart application.

Dartastic OTel API is the API for the Dartastic OTel SDK. The dartastic_opentelemetry_api exists as a standalone library to strictly adhere to the OpenTelemetry specification which separates API and the SDK. All OpenTelemetry API classes on in dartastic_opentelemetry_api. Developers should use this SDK.

Getting started

Include this in your pubspec.yaml:

dependencies:
  dartastic_opentelemetry: ^0.9.5

Usage

The entrypoint to the SDK is the OTel class. OTel has static "factory" methods for all OTel API and SDK objects. OTel needs to be initialized first to point to an OpenTelemetry backend. Initialization does a lot of work under the hood including gathering a rich set of standard resources for any OS that Dart runs in. It prepares for the creation of the global default TracerProvider, MeterProvider, and LoggerProvider, with the serviceName and a default Tracer, Meter, and OTelLogger, all created on first use.

All configuration, including Trace, Metric and Log exporter configuration, can be made in code via OTel.initialize(). Codeless configuration can be done with standard OpenTelemetry environmental variables either through POSIX variable or -D or --define for Dart or with --dart-define for Flutter apps. See Running with Environment Variables below

Minimal Code Example

import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';

Future<void> main() async {
  // Initialize - automatically reads environment variables.
  await OTel.initialize();

  // Get the default tracer.
  final tracer = OTel.tracer();

  // Per the OpenTelemetry spec, tracer.startSpan() does NOT activate the
  // span. Use tracer.withSpanAsync to make the span active for the
  // duration of doWork() so that any spans started inside are parented
  // to it via Context.current.
  final span = tracer.startSpan('my-operation');
  try {
    await tracer.withSpanAsync(span, doWork);
  } catch (e, stackTrace) {
    // The span has a status of SpanStatus.Ok on creation, set it to
    // Error when an error occurs in the span.
    span.recordException(e, stackTrace: stackTrace);
    span.setStatus(SpanStatusCode.Error, e.toString());
    rethrow;
  } finally {
    // Always end the span โ€” even on error.
    span.end();
  }

  await OTel.shutdown();
}

Future<void> doWork() async {
  // Your business logic here.
}

Since dartastic_opentelemetry exports all the classes of opentelemetry_api, refer to opentelemetry_api for documentation of API classes.

See the /example folder for more examples.

OpenTelemetry Tracing API

The Tracing API is the primary signal in OpenTelemetry. A trace represents the end-to-end journey of a request through your system. Each trace is composed of spans โ€” individual units of work with a name, timing, attributes, and parent-child relationships.

Concepts

  • TracerProvider: Entry point to the tracing API, responsible for creating Tracers and configuring the tracing pipeline
  • Tracer: Creates Spans for a particular instrumentation scope (library, package, or module)
  • Span: Represents a single operation โ€” tracks its name, start/end time, attributes, events, links, and status
  • SpanProcessor: Handles span lifecycle events (start, end) and manages export
  • SpanExporter: Sends finished spans to a backend (OTLP, console, etc.)
  • Sampler: Decides which spans to record and export
  • Context: Carries the active span and baggage across async boundaries and service boundaries

Basic Tracing

import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';

Future<void> main() async {
  await OTel.initialize(serviceName: 'my-service');

  // Get the default tracer
  final tracer = OTel.tracer();

  // Create a span and make it active for the duration of doWork() via
  // withSpanAsync. Per the OpenTelemetry spec, startSpan does NOT activate
  // the span โ€” child spans started inside the closure are parented to
  // `span` via Context.current.
  // Prefer typed enum keys over raw strings โ€” UserSemantics.userId is
  // the OTel semantic-convention key. For app-specific attributes that
  // don't have a semantic convention, define your own typed enum (see
  // the Span Attributes section below).
  final span = tracer.startSpan(
    'main-operation',
    kind: SpanKind.server,
    attributes: OTel.attributesFromMap({
      UserSemantics.userId.key: 'user-123',
      // app-specific key โ€” would normally come from your own typed enum:
      'request.type': 'example',
    }),
  );

  try {
    await tracer.withSpanAsync(span, doWork);
  } catch (e, stackTrace) {
    // The span has a status of SpanStatus.Ok on creation, set it to
    // Error when an error occurs in the span.
    span.recordException(e, stackTrace: stackTrace);
    span.setStatus(SpanStatusCode.Error, e.toString());
    rethrow;
  } finally {
    span.end();
  }

  await OTel.shutdown();
}

Parent-Child Spans

Spans form a tree by linking child spans to parent spans via context:

final parentSpan = tracer.startSpan('parent-operation');
try {
  // Create a child span linked to the parent. Passing the parent's
  // SpanContext via `context:` parents this span without requiring the
  // parent to be active in Context.current.
  final childSpan = tracer.startSpan(
    'database.query',
    kind: SpanKind.client,
    context: OTel.context(spanContext: parentSpan.spanContext),
    attributes: OTel.attributesFromMap({
      DatabaseResource.dbSystem.key: 'postgresql',
      DatabaseResource.dbOperation.key: 'SELECT',
    }),
  );
  try {
    await queryDatabase();
  } catch (e, stackTrace) {
    // The span has a status of SpanStatus.Ok on creation, set it to
    // Error when an error occurs in the span.
    childSpan.recordException(e, stackTrace: stackTrace);
    childSpan.setStatus(SpanStatusCode.Error, e.toString());
    rethrow;
  } finally {
    childSpan.end();
  }
} catch (e, stackTrace) {
  // The span has a status of SpanStatus.Ok on creation, set it to
  // Error when an error occurs in the span.
  parentSpan.recordException(e, stackTrace: stackTrace);
  parentSpan.setStatus(SpanStatusCode.Error, e.toString());
  rethrow;
} finally {
  parentSpan.end();
}

Activating a span โ€” OTel.withSpan / OTel.withSpanAsync

OTel.withSpan and OTel.withSpanAsync activate a span for the duration of fn (so Context.current.span returns it inside fn) and record any thrown exception with SpanStatusCode.Error before rethrowing. The caller still owns span.end() โ€” the canonical OTel lifecycle is try / catch / finally:

final span = OTel.tracer().startSpan('compute-result');
try {
  final result = OTel.withSpan(span, () => computeExpensiveValue());
} catch (e, stackTrace) {
  // The span has a status of SpanStatus.Ok on creation, set it to
  // Error when an error occurs in the span.
  span.recordException(e, stackTrace: stackTrace);
  span.setStatus(SpanStatusCode.Error, e.toString());
  rethrow;
} finally {
  span.end();
}

// Async version
final fetchSpan = OTel.tracer().startSpan('fetch-data', kind: SpanKind.client);
try {
  final data = await OTel.withSpanAsync(
    fetchSpan,
    () => httpClient.get('/api/data'),
  );
} catch (e, stackTrace) {
  fetchSpan.recordException(e, stackTrace: stackTrace);
  fetchSpan.setStatus(SpanStatusCode.Error, e.toString());
  rethrow;
} finally {
  fetchSpan.end();
}

If you also want the span as a callback argument and want the span ended for you, use tracer.startActiveSpan / startActiveSpanAsync:

// Active span โ€” span is in Context.current AND passed to fn,
// and ended automatically when fn returns.
OTel.tracer().startActiveSpan(
  name: 'process-request',
  fn: (span) {
    span.setStringAttribute(ExampleAttribute.requestId.key, 'abc-123');
    return processRequest();
  },
);

Span Attributes

Attributes are typed key-value pairs on spans. OTel restricts values to String, bool, int, double, and Lists of those types.

Prefer typed enum keys over raw strings. The API ships enums for every namespace in the OTel semantic conventions โ€” HttpResource, UrlResource, ServerResource, ClientResource, DatabaseResource, UserSemantics, SessionViewSemantics, etc. Using them prevents typos, gives you autocomplete, and tracks the spec as it evolves. For app-specific attributes that aren't in a convention, define your own enum implementing OTelSemantic:

// In your own app, name this for your domain (e.g. `CheckoutAttribute`).
enum ExampleAttribute implements OTelSemantic {
  requestType('request.type'),
  processingStage('processing.stage'),
  durationMs('duration_ms'),
  tags('tags'),
  cacheKey('cache.key'),
  cacheRegion('cache.region'),
  linkType('link.type'),
  authMethod('auth.method'),
  orderId('order.id'),
  requestId('request.id');

  @override
  final String key;
  @override
  String toString() => key;
  const ExampleAttribute(this.key);
}
// Type-safe individual attributes โ€” mix API convention enums with your
// own ExampleAttribute for non-convention keys.
final span = tracer.startSpan('operation', attributes: OTel.attributes([
  OTel.attributeString(HttpResource.requestMethod.key, 'GET'),
  OTel.attributeInt(HttpResource.responseStatusCode.key, 200),
  OTel.attributeDouble(ExampleAttribute.durationMs.key, 123.45),
  OTel.attributeStringList(ExampleAttribute.tags.key, ['payment', 'critical']),
]));

// Or from a map (types are inferred automatically).
final span = tracer.startSpan('operation',
  attributes: OTel.attributesFromMap({
    HttpResource.requestMethod.key: 'GET',
    HttpResource.responseStatusCode.key: 200,
  }),
);

// Add attributes after creation.
span.setStringAttribute(UrlResource.urlFull.key, 'https://api.example.com/data');
span.setIntAttribute(HttpResource.responseBodySize.key, 1024);
span.addAttributes(OTel.attributesFromMap({
  ExampleAttribute.processingStage.key: 'complete',
}));

Span Events

Events are time-stamped annotations on a span. Event names themselves are user-defined, but event attributes still benefit from typed enum keys:

span.addEvent(OTel.spanEventNow(
  'cache.hit',
  OTel.attributesFromMap({ExampleAttribute.cacheKey.key: 'user:123'}),
));

span.addEventNow('validation.passed');

Links connect spans across traces โ€” useful for batch processing or fan-out patterns:

final link = OTel.spanLink(
  otherSpan.spanContext,
  attributes: OTel.attributesFromMap({ExampleAttribute.linkType.key: 'triggers'}),
);

final span = tracer.startSpan('batch-process', links: [link]);

SpanKind

Classifies the relationship between a span and its remote counterpart:

SpanKind Description Example
internal Default; internal operation with no remote side Business logic, local computation
server Server handling an incoming request HTTP server endpoint
client Client making an outgoing request HTTP client call, DB query
producer Producer enqueuing a message Kafka producer, queue publisher
consumer Consumer processing a message Kafka consumer, queue subscriber

Samplers

Samplers control which spans are recorded and exported. Configure via OTel.initialize() or per-Tracer.

Sampler Description Use Case
AlwaysOnSampler Samples every span (default) Development, debugging
AlwaysOffSampler Never samples Disable tracing without code changes
TraceIdRatioSampler Samples by trace ID ratio (consistent per trace) Production with consistent sampling
ProbabilitySampler Samples by random probability Testing, non-critical sampling
ParentBasedSampler Respects parent span's sampling decision Distributed tracing across services
RateLimitingSampler Limits sampled traces per second (token bucket) Controlling overhead
CountingSampler Samples every Nth request Periodic sampling
CompositeSampler Combines samplers with AND/OR logic Complex sampling policies
// Sample 10% of traces consistently
await OTel.initialize(
  serviceName: 'my-service',
  sampler: TraceIdRatioSampler(0.1),
);

// Respect parent decisions, sample 50% of new root traces
await OTel.initialize(
  serviceName: 'my-service',
  sampler: ParentBasedSampler(TraceIdRatioSampler(0.5)),
);

// Rate-limit to 100 traces/second
await OTel.initialize(
  serviceName: 'my-service',
  sampler: RateLimitingSampler(100),
);

Span Processors

Processors handle span lifecycle and export:

// SimpleSpanProcessor โ€” exports each span immediately (good for debugging)
await OTel.initialize(
  spanProcessor: SimpleSpanProcessor(ConsoleExporter()),
);

// BatchSpanProcessor โ€” batches spans for efficient production export
await OTel.initialize(
  spanProcessor: BatchSpanProcessor(
    OtlpGrpcSpanExporter(OtlpGrpcExporterConfig(endpoint: 'localhost:4317')),
    BatchSpanProcessorConfig(
      maxQueueSize: 2048,
      scheduleDelay: Duration(milliseconds: 5000),
      maxExportBatchSize: 512,
    ),
  ),
);

Span Exporters

Exporter Protocol Description
ConsoleExporter stdout Prints spans to console for debugging
OtlpGrpcSpanExporter gRPC Exports via OTLP/gRPC (production)
OtlpHttpSpanExporter HTTP/protobuf Exports via OTLP/HTTP (web-compatible)
// Console (development)
final exporter = ConsoleExporter();

// OTLP gRPC (production)
final exporter = OtlpGrpcSpanExporter(OtlpGrpcExporterConfig(
  endpoint: 'otel-collector:4317',
  headers: {'api-key': 'your-key'},
  compression: true,
));

// OTLP HTTP (web-compatible)
final exporter = OtlpHttpSpanExporter(OtlpHttpExporterConfig(
  endpoint: 'https://otel-collector:4318',
  headers: {'api-key': 'your-key'},
));

Context Propagation

Propagate trace context across service boundaries using W3C Trace Context and Baggage:

final propagator = CompositePropagator<Map<String, String>, String>([
  W3CTraceContextPropagator(),
  W3CBaggagePropagator(),
]);

// Inject into outgoing HTTP headers
final headers = <String, String>{};
propagator.inject(Context.current, headers, MapTextMapSetter(headers));
// Send headers with your HTTP request...

// Extract from incoming HTTP headers
final extractedContext = propagator.extract(
  OTel.context(),
  incomingHeaders,
  MapTextMapGetter(incomingHeaders),
);

// Create a child span in the extracted context
await extractedContext.run(() async {
  final span = tracer.startSpan('handle-request');
  // This span is part of the same distributed trace
  span.end();
});

Context also propagates across Dart async gaps and Isolates:

// Across Isolates
final result = await Context.current.runIsolate(() async {
  // Context is automatically restored in the new Isolate
  final span = tracer.startSpan('isolate-work');
  try {
    return await computeInIsolate();
  } finally {
    span.end();
  }
});

OpenTelemetry Metrics API

The Metrics API in OpenTelemetry provides a way to record measurements about your application. These measurements can be exported later as metrics, allowing you to monitor and analyze the performance and behavior of your application.

Concepts

  • MeterProvider: Entry point to the metrics API, responsible for creating Meters
  • Meter: Used to create instruments for recording measurements
  • Instrument: Used to record measurements
    • Synchronous instruments: record measurements at the moment of calling their APIs
    • Asynchronous instruments: collect measurements on demand via callbacks

Instrument Types

  • Counter: Synchronous, monotonic increasing counter (can only go up)
  • UpDownCounter: Synchronous, non-monotonic counter (can go up or down)
  • Histogram: Synchronous, aggregable measurements with statistical distributions
  • Gauge: Synchronous, non-additive value that represents current state
  • ObservableCounter: Asynchronous version of Counter
  • ObservableUpDownCounter: Asynchronous version of UpDownCounter
  • ObservableGauge: Asynchronous version of Gauge

Usage Pattern

Similar to the Tracing API, the metrics API follows a multi-layered factory pattern:

  1. API Layer: Defines interfaces and provides no-op implementations
  2. SDK Layer: Provides concrete implementations
  3. Flutter Layer: Adds UI-specific functionality

The API follows the pattern of using factory methods for creation rather than constructors:

// Get a meter from the meter provider
final meter = OTel.meterProvider().getMeter('component_name');

// Create a counter instrument
final counter = meter.createCounter('my_counter');

// Record measurements
counter.add(1, {'attribute_key': 'attribute_value'});

For asynchronous instruments:

// Create an observable counter
final observableCounter = meter.createObservableCounter(
  'my_observable_counter',
  () => [Measurement(10, {'attribute_key': 'attribute_value'})],
);

Understanding Metric Types and When to Use Them

Instrument Type Use Case Example
Counter Count things that only increase Request count, completed tasks
UpDownCounter Count things that can increase or decrease Active requests, queue size
Histogram Measure distributions Request durations, payload sizes
Gauge Record current value CPU usage, memory usage
ObservableCounter Count things that only increase, collected on demand Total CPU time
ObservableUpDownCounter Count things that can increase or decrease, collected on demand Memory usage
ObservableGauge Record current value, collected on demand Current temperature

OpenTelemetry Logs API

The Logs API provides structured logging that integrates with traces and metrics. Unlike traditional logging frameworks, OpenTelemetry logs are first-class telemetry signals that carry context, severity, attributes, and can be correlated with the span that was active when the log was emitted.

Concepts

  • LoggerProvider: Entry point to the logs API, responsible for creating Loggers
  • OTelLogger: Used to emit log records
  • LogRecord: Represents a single log event with body, severity, attributes, timestamps, and trace context
  • LogRecordProcessor: Processes log records before export
  • LogRecordExporter: Exports log records to backends

Quick Start

import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';

void main() async {
  // Initialize with logs enabled (default)
  await OTel.initialize(
    serviceName: 'my-service',
    enableLogs: true,  // Default is true
  );

  // Get a logger
  final logger = OTel.logger('my-component');

  // Emit log records
  logger.emit(
    body: 'Application started',
    severityNumber: Severity.INFO,
  );

  // Log with attributes โ€” prefer typed enum keys.
  logger.emit(
    body: 'User logged in',
    severityNumber: Severity.INFO,
    attributes: OTel.attributesFromMap({
      UserSemantics.userId.key: 'user123',
      UserSemantics.userRole.key: 'admin',
    }),
  );

  // Log an error with exception.
  try {
    throw Exception('Something went wrong');
  } catch (e, stackTrace) {
    logger.emit(
      body: 'Operation failed: $e',
      severityNumber: Severity.ERROR,
      attributes: OTel.attributesFromMap({
        ExceptionResource.exceptionType.key: e.runtimeType.toString(),
        ExceptionResource.exceptionStacktrace.key: stackTrace.toString(),
      }),
    );
  }
}

Intercepting print() Calls

Dartastic OpenTelemetry can automatically capture print() calls and convert them to OpenTelemetry logs:

await OTel.initialize(
  serviceName: 'my-service',
  logPrint: true,  // Enable print interception
  logPrintLoggerName: 'dart.print',  // Optional custom logger name
);

// Use runWithPrintInterception to capture prints
OTel.runWithPrintInterception(() {
  print('This will be captured as an OTel log');
  print('So will this');
});

// For async code
await OTel.runWithPrintInterceptionAsync(() async {
  print('Async print captured');
  await someAsyncOperation();
});

Log Severity Levels

Severity Use Case
Severity.TRACE / Severity.TRACE2-4 Fine-grained debugging
Severity.DEBUG / Severity.DEBUG2-4 Debug information
Severity.INFO / Severity.INFO2-4 General information
Severity.WARN / Severity.WARN2-4 Warning conditions
Severity.ERROR / Severity.ERROR2-4 Error conditions
Severity.FATAL / Severity.FATAL2-4 Critical failures

Basic Logging

// Get a logger from the default provider
final logger = OTel.loggerProvider().getLogger('my-service');

// Emit a simple log. Prefer typed enum keys (UserSemantics, ExampleAttribute,
// HttpResource, etc.) over raw strings.
logger.emit(
  severityNumber: Severity.INFO,
  body: 'User successfully logged in.',
  attributes: OTel.attributesFromMap({
    UserSemantics.userId.key: 'user-123',
    ExampleAttribute.authMethod.key: 'oauth',
  }),
);

// Warning log.
logger.emit(
  severityNumber: Severity.WARN,
  body: 'Cache miss for requested key.',
  attributes: OTel.attributesFromMap({
    ExampleAttribute.cacheKey.key: 'profile_42',
    ExampleAttribute.cacheRegion.key: 'us-east-1',
  }),
);

// Error log.
logger.emit(
  severityNumber: Severity.ERROR,
  body: 'Failed to connect to database.',
  attributes: OTel.attributesFromMap({
    DatabaseResource.dbSystem.key: 'postgresql',
    ErrorSemantics.errorType.key: 'ConnectionTimeout',
  }),
);

Log-to-Trace Correlation

Logs can be linked to the active span through Context, enabling powerful correlation in your backend:

final span = tracer.startSpan('process-order');
try {
  logger.emit(
    severityNumber: Severity.INFO,
    body: 'Processing order.',
    context: Context.current, // Links this log to the active span
    attributes: OTel.attributesFromMap({ExampleAttribute.orderId.key: 'order-789'}),
  );
  await processOrder();
} finally {
  span.end();
}

Custom Log Exporters

// Use a custom exporter
final customExporter = OtlpHttpLogRecordExporter(
  OtlpHttpLogRecordExporterConfig(
    endpoint: 'https://my-collector:4318',
    headers: {'Authorization': 'Bearer token'},
  ),
);

await OTel.initialize(
  serviceName: 'my-service',
  logRecordExporter: customExporter,
);

Console Logging (Development)

// Use console exporter for development
await OTel.initialize(
  serviceName: 'my-service',
  logRecordProcessor: SimpleLogRecordProcessor(ConsoleLogRecordExporter()),
);

Configuration via Environment Variables

Logs can be configured via environment variables:

# Set logs exporter (otlp, console, or none)
export OTEL_LOGS_EXPORTER=otlp

# Set logs-specific endpoint
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4318

# Configure batch processor
export OTEL_BLRP_SCHEDULE_DELAY=5000
export OTEL_BLRP_MAX_QUEUE_SIZE=4096

# Set log record limits
export OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT=128

Severity Levels

OpenTelemetry defines a fine-grained 24-level severity scale, grouped into standard levels:

Level Severities Use Case
TRACE TRACE, TRACE2-4 Finest-grained debugging information
DEBUG DEBUG, DEBUG2-4 Debugging information
INFO INFO, INFO2-4 Normal operational messages
WARN WARN, WARN2-4 Warning conditions
ERROR ERROR, ERROR2-4 Error conditions
FATAL FATAL, FATAL2-4 System is unusable

Severity levels support comparison operators for filtering:

if (severity >= Severity.WARN) {
  // Handle warning or above
}

Flexible Log Bodies

The body parameter accepts diverse types โ€” not just strings:

// String body
logger.emit(body: 'Simple message.');

// Structured body (Map)
logger.emit(body: {'event': 'batch_complete', 'items': 42});

// List body
logger.emit(body: [
  {'job': 'resize_images', 'status': 'ok'},
  {'job': 'generate_thumbnails', 'status': 'failed'},
]);

Named Events

Use eventName to categorize logs as discrete events:

logger.emit(
  eventName: 'user_signup',
  severityNumber: Severity.INFO,
  body: 'New user registered.',
  attributes: OTel.attributesFromMap({
    'user.email_domain': 'example.com',
    'signup.source': 'organic',
  }),
);

Running with Environment Variables

Dartastic OpenTelemetry supports for all standard OpenTelemetry environment variables as defined in the OpenTelemetry Specification.

Environment variables provide a convenient way to configure OpenTelemetry without hardcoding values. All environment variable names are available as strongly-typed constants in the SDK for compile-time safety and IDE autocomplete. See lib/src/environment/env_constants.dart for a complete list.

How It Works

Dart environment variables can be set in two ways:

  1. System Environment Variables (Non-web only): Traditional POSIX environment variables

    export OTEL_SERVICE_NAME=my-service
    dart run
    
  2. Compile-time Constants (All platforms including Flutter web): Passed during compilation/execution

    For Dart commands (dart run, dart compile, dart test):

    # Using --define (or -D shorthand)
    dart run --define=OTEL_SERVICE_NAME=my-service
    dart compile exe -D=OTEL_SERVICE_NAME=my-service -o myapp
    dart test -DOTEL_SERVICE_NAME=my-service
    

    For Flutter commands:

    # Flutter uses --dart-define (note the different flag name)
    flutter run --dart-define=OTEL_SERVICE_NAME=my-service
    flutter build apk --dart-define=OTEL_SERVICE_NAME=my-service
    

Priority: Compile-time constants (--define or --dart-define) take precedence over system environment variables. Explicit parameters to OTel.initialize() override both. Thus, POSIX env vars cannot override --dart-defines and neither POSIX env vars nor --dart-defines can override code. This is sensible and reduces security vectors.

Web Support: Flutter web and Dart web only support compile-time constants (--define or --dart-define), as browser environments don't have access to system environment variables.

Using Environment Variable Constants

All OpenTelemetry environment variable names are available as typed constants:

import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';

void main() {
  // Reference constants instead of strings
  final serviceName = EnvironmentService.instance.getValue(otelServiceName);
  final endpoint = EnvironmentService.instance.getValue(otelExporterOtlpEndpoint);
  
  print('Service: $serviceName');
  print('Endpoint: $endpoint');
}

Constants are defined for all 74 OpenTelemetry environment variables. See lib/src/environment/env_constants.dart for the complete list.

Supported Environmental Variables

Service Configuration

Constant Environment Variable Description Example
otelServiceName OTEL_SERVICE_NAME Sets the service name my-dart-app
otelResourceAttributes OTEL_RESOURCE_ATTRIBUTES Additional resource attributes environment=prod,region=us-west
otelLogLevel OTEL_LOG_LEVEL SDK internal log level INFO, DEBUG, WARN, ERROR

OTLP Exporter Configuration

Constant Environment Variable Description Default Example
otelExporterOtlpEndpoint OTEL_EXPORTER_OTLP_ENDPOINT OTLP endpoint URL http://localhost:4318 https://otel-collector:4317
otelExporterOtlpProtocol OTEL_EXPORTER_OTLP_PROTOCOL Transport protocol http/protobuf grpc, http/protobuf, http/json
otelExporterOtlpHeaders OTEL_EXPORTER_OTLP_HEADERS Headers (key=value,...) None api-key=secret,tenant=acme
otelExporterOtlpTimeout OTEL_EXPORTER_OTLP_TIMEOUT Timeout in milliseconds 10000 5000
otelExporterOtlpCompression OTEL_EXPORTER_OTLP_COMPRESSION Compression algorithm None gzip

Signal-Specific Configuration

Traces
Constant Environment Variable Description
otelTracesExporter OTEL_TRACES_EXPORTER Trace exporter type
otelExporterOtlpTracesEndpoint OTEL_EXPORTER_OTLP_TRACES_ENDPOINT Traces-specific endpoint
otelExporterOtlpTracesProtocol OTEL_EXPORTER_OTLP_TRACES_PROTOCOL Traces-specific protocol
otelExporterOtlpTracesHeaders OTEL_EXPORTER_OTLP_TRACES_HEADERS Traces-specific headers
Metrics
Constant Environment Variable Description
otelMetricsExporter OTEL_METRICS_EXPORTER Metrics exporter type
otelExporterOtlpMetricsEndpoint OTEL_EXPORTER_OTLP_METRICS_ENDPOINT Metrics-specific endpoint
otelExporterOtlpMetricsProtocol OTEL_EXPORTER_OTLP_METRICS_PROTOCOL Metrics-specific protocol
otelExporterOtlpMetricsHeaders OTEL_EXPORTER_OTLP_METRICS_HEADERS Metrics-specific headers
Logs
Constant Environment Variable Description
otelLogsExporter OTEL_LOGS_EXPORTER Logs exporter type (otlp, console, none)
otelExporterOtlpLogsEndpoint OTEL_EXPORTER_OTLP_LOGS_ENDPOINT Logs-specific endpoint
otelExporterOtlpLogsProtocol OTEL_EXPORTER_OTLP_LOGS_PROTOCOL Logs-specific protocol
otelExporterOtlpLogsHeaders OTEL_EXPORTER_OTLP_LOGS_HEADERS Logs-specific headers
Batch LogRecord Processor (BLRP)
Constant Environment Variable Default Description
otelBlrpScheduleDelay OTEL_BLRP_SCHEDULE_DELAY 1000 Delay between exports (milliseconds)
otelBlrpExportTimeout OTEL_BLRP_EXPORT_TIMEOUT 30000 Export timeout (milliseconds)
otelBlrpMaxQueueSize OTEL_BLRP_MAX_QUEUE_SIZE 2048 Maximum queue size
otelBlrpMaxExportBatchSize OTEL_BLRP_MAX_EXPORT_BATCH_SIZE 512 Maximum batch size per export
LogRecord Limits
Constant Environment Variable Default Description
otelLogrecordAttributeValueLengthLimit OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT No limit Maximum length of attribute values
otelLogrecordAttributeCountLimit OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT 128 Maximum number of attributes

For the complete list of all supported environment variables with full documentation, see lib/src/environment/env_constants.dart.

Environment Usage Examples

Dart Application with Environment Variables

Note the ',' in OTEL_RESOURCE_ATTRIBUTES for POSIX env vars but a ';' for --dart-define. This is due to a Dart quirk.

# Set environment variables
export OTEL_SERVICE_NAME=my-backend-service
export OTEL_RESOURCE_ATTRIBUTES="service.version=1.2.3,deployment.environment=prod"
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_HEADERS=api-key=your-key
export OTEL_LOG_LEVEL=DEBUG

# Run your application
dart run bin/my_app.dart

Flutter Application with --dart-define

flutter run \
  --dart-define=OTEL_SERVICE_NAME=my-flutter-app \
  --dart-define=OTEL_RESOURCE_ATTRIBUTES="service.version=1.2.3;deployment.environment=prod"
  --dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector:4317 \
  --dart-define=OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
  --dart-define=OTEL_EXPORTER_OTLP_HEADERS=api-key=your-key

Flutter Web (requires --dart-define)

# Web MUST use --dart-define (environment variables don't work in browsers)
flutter run -d chrome \
  --dart-define=OTEL_SERVICE_NAME=my-web-app \
  --dart-define=OTEL_EXPORTER_OTLP_ENDPOINT=https://collector:4318

Combining Both (--dart-define wins)

# Environment variable
export OTEL_SERVICE_NAME=from-environment

# --dart-define takes precedence
dart run --dart-define=OTEL_SERVICE_NAME=from-dart-define

# Result: Uses "from-dart-define"

In Code

import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';

void main() async {
  // OTel.initialize() automatically reads environment variables
  // when parameters are not explicitly provided
  await OTel.initialize();
  
  // Environment variables are read automatically:
  // - OTEL_SERVICE_NAME
  // - OTEL_EXPORTER_OTLP_ENDPOINT
  // - OTEL_EXPORTER_OTLP_PROTOCOL
  // - And 90+ others...
  
  // Explicit parameters override environment variables
  await OTel.initialize(
    serviceName: 'explicit-service',  // Overrides OTEL_SERVICE_NAME
    endpoint: 'https://override:4318', // Overrides OTEL_EXPORTER_OTLP_ENDPOINT
  );
  
  // You can also read environment variables directly
  final endpoint = EnvironmentService.instance.getValue(otelExporterOtlpEndpoint);
  print('Using endpoint: $endpoint');
}

Testing with Environment Variables

Integration tests can use real environment variables:

# Run tests with environment variables
OTEL_SERVICE_NAME=test-service dart test

# Run tests with --dart-define
dart test --dart-define=OTEL_SERVICE_NAME=test-service

# Run the provided integration test script
./tool/test_env_vars.sh

The SDK includes an integration test suite (test/integration/environment_variables_test.dart) and a test script (tool/test_env_vars.sh) that demonstrates proper environment variable usage.

Integration with Dartastic/Flutterrific

All three signal APIs (Traces, Metrics, Logs) follow the same multi-layered factory pattern:

  1. API Layer: Defines interfaces and provides no-op implementations
  2. SDK Layer: Provides concrete implementations with export and processing
  3. Flutter Layer: Adds UI-specific functionality (route observation, app lifecycle, etc.)

The creation of objects is managed through factory methods on OTel, ensuring a clear separation between API and SDK. Each signal can be used in no-op mode when the SDK is not initialized.

Roadmap

  • Support for Zipkin, Jaeger exporters

CNCF Contribution and Alignment

This project aims to align with Cloud Native Computing Foundation (CNCF) best practices:

  • Interoperability - Works with the broader OpenTelemetry ecosystem
  • Specification compliance - Strictly follows the OpenTelemetry specification
  • Vendor neutrality - Provides a vendor-neutral implementation

License

Apache 2.0 - See the LICENSE file for details.

AI Usage

Practically all code in Dartastic was generated via Claude. EVERY character is reviewed by a human.

Additional information