obsi 1.0.1 copy "obsi: ^1.0.1" to clipboard
obsi: ^1.0.1 copied to clipboard

Production-focused tracing, logging, metrics, errors, and W3C context propagation for pure Dart.

obsi #

pub package CI License: MIT

obsi is the observability core for Dart applications. It helps explain what an application is doing through four related signals:

  • traces follow an operation and each step it performs;
  • logs record structured events with execution context;
  • metrics measure counts, current state, and distributions over time;
  • errors capture failures with user data, breadcrumbs, and technical context.

The package also provides resources, sampling, W3C propagation, baggage, processors, and exporter contracts. It is pure Dart: there is no Flutter, HTTP client, backend, or OpenTelemetry SDK dependency. Backends and framework instrumentations are added through optional packages.

Installation #

dart pub add obsi
import 'package:obsi/obsi.dart';

The mental model #

An Obsi installation has three layers:

  1. Application code creates telemetry through a Tracer, Logger, Meter, or ErrorManager.
  2. A processor decides when to deliver it. A batch processor, for example, queues spans and exports them outside the application path.
  3. An exporter sends the data to a destination such as the console, OTLP, Sentry, Crashlytics, or your own implementation.

ObsiProvider groups all four signals and Obsi exposes them through one process-wide facade. Global access is optional: providers can also be injected directly into application classes.

application → tracer/logger/meter/errors → processor/reader → exporter

First configuration #

The following example configures all four signals with console exporters. It is a useful starting point before connecting a production backend.

final resource = Resource({
  'service.name': 'checkout-api',
  'service.version': '1.0.0',
  'deployment.environment.name': 'development',
});

final provider = ObsiProvider(
  traces: TracerProvider(
    resource: resource,
    processor: BatchSpanProcessor(const ConsoleSpanExporter()),
  ),
  logs: LoggerProvider(
    resource: resource,
    processor: BatchLogProcessor(const ConsoleLogExporter()),
  ),
  metrics: MeterProvider(
    resource: resource,
    readers: [
      PeriodicMetricReader(
        const ConsoleMetricExporter(),
        interval: const Duration(seconds: 30),
      ),
    ],
  ),
  errors: ErrorManager(
    resource: resource,
    exporter: const ConsoleErrorExporter(),
  ),
);

Obsi.configure(provider);

try {
  await runApplication();
} finally {
  await Obsi.shutdown();
}

Resource describes the process producing telemetry. Reuse the same resource across providers so traces, logs, metrics, and errors can be queried as parts of the same service.

BatchSpanProcessor and BatchLogProcessor are suitable for production: they keep bounded queues and export outside the main execution path. The Simple...Processor variants deliver each item immediately and are useful for tests and small programs.

Human-friendly console output #

The default console exporters continue to emit one JSON object per record. This is useful for CI, log collectors, and other machine consumers. During local development, use the named pretty constructors for readable, colored output:

const pretty = PrettyConsoleOptions(
  colors: true,
  includeResource: false,
  includeTraceContext: true,
  includeStackTrace: true,
);

final provider = ObsiProvider(
  traces: TracerProvider(
    processor: BatchSpanProcessor(
      const ConsoleSpanExporter.pretty(options: pretty),
    ),
  ),
  logs: LoggerProvider(
    processor: BatchLogProcessor(
      const ConsoleLogExporter.pretty(options: pretty),
    ),
  ),
  metrics: MeterProvider(
    readers: [
      PeriodicMetricReader(
        const ConsoleMetricExporter.pretty(options: pretty),
      ),
    ],
  ),
  errors: ErrorManager(
    exporter: const ConsoleErrorExporter.pretty(options: pretty),
  ),
);

Pretty output prioritizes severity or signal type, scope, message, duration, attributes, trace correlation, metric points, span events, error breadcrumbs, and stack traces. Set colors: false for terminals without ANSI support. Other options can hide timestamps, scope, trace context, resources, or stack traces; print attributes on separate lines; and bound displayed value lengths.

Every console exporter also accepts a writer callback. For example, Flutter applications can pass debugPrint, while tests can collect output in a list. The callback changes only the destination, not JSON or pretty formatting.

Tracing: follow an operation #

A trace represents a complete operation. Each Span is one step within it. trace() creates a span, makes it current inside the callback, records failures, and ends it when the callback completes.

final tracer = Obsi.tracer;

final order = await tracer.trace(
  'order.create',
  () async {
    final customer = await tracer.trace(
      'customer.load',
      () => repository.loadCustomer(),
    );

    return tracer.trace(
      'order.persist',
      () => repository.createOrder(customer),
    );
  },
  attributes: {
    'order.channel': 'web',
    'cart.items': 3,
  },
);

Nested spans inherit the current span through Dart Zone context, which is preserved across Future and await. Use traceSync() for synchronous work. Use traceStream() when the span must remain open until a stream completes, fails, or is cancelled.

For full control, create a span manually:

final span = Obsi.tracer.startSpan(
  'payment.authorize',
  kind: SpanKind.client,
  attributes: {'payment.provider': 'example'},
);

try {
  await span.run(() => paymentClient.authorize());
  span
    ..addEvent('payment.accepted')
    ..setStatus(SpanStatus.ok);
} catch (error, stackTrace) {
  span
    ..recordException(error, stackTrace: stackTrace)
    ..setStatus(SpanStatus.error);
  rethrow;
} finally {
  span.end();
}

Use SpanKind.client for outbound calls, server for inbound requests, producer and consumer for messaging, and internal for local work. Prefer stable names such as GET /users/:id; unique values such as GET /users/87421 create unnecessary cardinality.

Sampling #

The sampler decides whether a new trace should be recorded and exported. A parent-based sampler preserves an upstream decision and applies a local ratio only to root traces:

final traces = TracerProvider(
  processor: BatchSpanProcessor(spanExporter),
  sampler: ParentBasedSampler(
    root: TraceIdRatioBasedSampler(0.10),
  ),
);

AlwaysOnSampler records everything and AlwaysOffSampler drops everything. Sampling reduces volume, but it does not replace bounded queues or a backend retention policy.

Logging: events with context #

A logger is named after the component producing its records. Its name and version become the InstrumentationScope, allowing modules and libraries in the same service to be distinguished.

final logger = Obsi.logger('checkout.payment', version: '1.0.0');

logger.info(
  'Payment requested',
  attributes: {
    'payment.method': 'card',
    'order.currency': 'EUR',
  },
);

try {
  await charge();
} catch (error, stackTrace) {
  logger.error(
    'Payment failed',
    error: error,
    stackTrace: stackTrace,
    attributes: {'payment.retryable': true},
  );
  rethrow;
}

When a log is emitted inside a span, Obsi automatically attaches its trace and span identifiers. Logs also become error breadcrumbs, so a later error report contains the events that led to the failure.

The API provides trace, debug, info, warn, error, fatal, and the generic log method. LoggerProvider.minimumSeverity drops less important levels before processing.

Metrics: measure aggregate behavior #

A Meter creates instruments. Choose the instrument based on the question you want the metric to answer:

  • Counter: how many times did something happen? It only increases.
  • UpDownCounter: how many items exist now? It can increase or decrease.
  • Histogram: how are durations or sizes distributed?
  • Gauge: what is the latest known value?
  • observable instruments: what is the value when the reader collects it?
final meter = Obsi.meter('checkout.orders', version: '1.0.0')!;

final created = meter.createCounter<int>(
  'orders.created',
  unit: '{order}',
);
final active = meter.createUpDownCounter<int>(
  'orders.active',
  unit: '{order}',
);
final duration = meter.createHistogram<double>(
  'orders.processing.duration',
  unit: 's',
  boundaries: [0.01, 0.05, 0.1, 0.5, 1, 2.5, 5],
);

final stopwatch = Stopwatch()..start();
active.add(1);
try {
  await processOrder();
  created.add(1, attributes: {'order.result': 'accepted'});
} finally {
  active.add(-1);
  duration.record(
    stopwatch.elapsedMicroseconds / Duration.microsecondsPerSecond,
    attributes: {'order.channel': 'web'},
  );
}

Use an observable instrument for a value already maintained by the application:

meter.createObservableGauge<int>(
  'worker.queue.depth',
  () => [Observation(queue.length)],
  unit: '{job}',
);

Metric attributes create distinct time series and must have low cardinality. status=success is appropriate; user.id and order.id are not. The provider bounds cardinality and exposes droppedMeasurements. A MetricView customizes buckets and limits for one instrument:

final metrics = MeterProvider(
  readers: [PeriodicMetricReader(metricExporter)],
  cardinalityLimit: 2000,
  views: const [
    MetricView(
      instrumentName: 'orders.processing.duration',
      histogramBoundaries: [0.01, 0.1, 0.5, 1, 5],
      cardinalityLimit: 100,
    ),
  ],
);

Errors: capture and enrich failures #

ErrorManager builds an ErrorReport, runs its processors, and passes it to an ErrorExporter. Its default pipeline sanitizes common sensitive fields, deduplicates repeated reports, and rate-limits non-fatal errors.

Errors.guard() is useful at an operation boundary. It captures an error and then rethrows it with the original stack trace:

await Errors.guard(
  () async {
    await runCommand();
  },
  fatal: false,
  handled: true,
);

withScope() adds context to one synchronous or asynchronous branch without mutating concurrent requests:

await Errors.withScope(
  () async {
    Errors.addBreadcrumb(ErrorBreadcrumb(
      timestamp: DateTime.now(),
      category: 'checkout',
      message: 'User confirmed the order',
      data: {'cart.items': 3},
    ));

    try {
      await submitOrder();
    } catch (error, stackTrace) {
      final errorId = await Errors.captureException(
        error,
        stackTrace: stackTrace,
        reason: 'Order submission failed',
        tags: {'feature': 'checkout'},
        contexts: {
          'order': {'currency': 'EUR', 'items': 3},
        },
      );
      Obsi.logger('checkout').warn(
        'Error report accepted',
        attributes: {'error.id': '$errorId'},
      );
    }
  },
  user: ErrorUser(id: 'customer-42'),
  tags: {'tenant': 'acme'},
);

An ErrorId means the local pipeline accepted the report; it does not prove that a remote backend received it. Inspect acceptedReports, exportedReports, droppedReports, exportFailures, and processorFailures to monitor delivery.

Use Errors.runGuarded() for otherwise uncaught asynchronous failures. Dart isolates do not share memory: use Errors.listenToIsolateErrors() for a secondary isolate and close the returned ErrorIsolateListener. Flutter global errors are handled by the optional obsi_flutter package.

Attributes, privacy, and conventions #

Attributes support String, bool, int, finite double, and homogeneous lists of those types. Empty keys, null values, non-finite numbers, and unsupported types throw ArgumentError. Failing early prevents ambiguous data from reaching exporters.

Apply the same redactor to providers that receive external data:

final redact = SensitiveAttributeRedactor(
  sensitiveKeys: ['password', 'token', 'authorization', 'email'],
).call;

final traces = TracerProvider(
  processor: BatchSpanProcessor(spanExporter),
  attributeRedactor: redact,
);
final logs = LoggerProvider(
  processor: BatchLogProcessor(logExporter),
  attributeRedactor: redact,
);
final metrics = MeterProvider(
  readers: [PeriodicMetricReader(metricExporter)],
  attributeRedactor: redact,
);

SemanticAttributes, SemanticEvents, SemanticMetrics, and SemanticHttp provide names and helpers aligned with OpenTelemetry conventions. Use them in instrumentations instead of introducing multiple names for the same concept.

Distributed context and baggage #

W3CTraceContextPropagator injects and extracts traceparent and tracestate. W3CBaggagePropagator does the same for baggage. The HTTP, Dio, and Shelf integrations already use them, so most applications only need to install the appropriate instrumentation package.

Baggage carries small values that must follow an operation across layers or services:

await Baggage.empty
    .set('tenant.id', 'acme')
    .set('release.channel', 'stable')
    .run(() async {
      await Obsi.tracer.trace('catalog.refresh', refreshCatalog);
    });

Never store secrets or large objects in baggage because it can travel through network headers. Active context uses Dart Zone; each isolate must configure its own Obsi installation.

Exporters and composition #

Console exporters are intended for development. Production applications typically use OTLP for traces, logs, and metrics, and Sentry or Crashlytics for errors. MultiSpanProcessor, MultiLogProcessor, and MultiErrorExporter deliver one signal to multiple destinations:

final processor = MultiSpanProcessor([
  BatchSpanProcessor(primaryExporter),
  BatchSpanProcessor(auditExporter),
]);

A processor owns its exporter. An ObsiProvider owns its signal providers. Do not share the same owned instance with multiple owners unless that implementation explicitly supports repeated shutdown.

Lifecycle and internal failures #

  • Obsi.configure(provider) installs a provider without closing a previous installation. It is normally called once during startup.
  • await Obsi.replace(next) detaches the previous provider, waits for it to drain and close, and then installs the replacement.
  • Obsi.disable() only detaches global facades; it does not flush or release resources.
  • await Obsi.shutdown() stops accepting telemetry, drains accepted work, and closes every owned resource exactly once.

Shutdown is idempotent and safe under concurrent calls. While replace or shutdown is pending, synchronous configure and disable calls throw StateError. After terminal shutdown, synchronous signal APIs ignore new work; terminal asynchronous APIs may complete with StateError.

Pipelines use limits and timeouts so an unavailable backend cannot consume memory indefinitely. Monitor counters such as droppedSpans, droppedRecords, droppedMeasurements, droppedReports, and exportFailures as part of the observability system itself.

Internal failures are delivered to a TelemetryErrorHandler. The handler must use a fallback channel that does not call Obsi recursively:

void telemetryDiagnostic(Object error, StackTrace stackTrace) {
  stderr.writeln('Obsi failure: $error\n$stackTrace');
}

final processor = BatchSpanProcessor(
  spanExporter,
  maxQueueSize: 2048,
  maxExportBatchSize: 512,
  exportTimeout: const Duration(seconds: 10),
  onExportError: telemetryDiagnostic,
);

API inventory #

Concept Main public API When to use it
Complete installation Obsi, ObsiProvider Configure and close traces, logs, metrics, and errors as one unit.
Per-signal facades Trace, Logs, Metrics, Errors Configure or access one signal without a complete ObsiProvider.
Service identity Resource Attach service name, version, environment, host, and common metadata to every signal.
Component identity InstrumentationScope Identify the module or library creating telemetry; normally created by getTracer, getLogger, or getMeter.
Attributes and privacy Attributes, AttributeRedactor, SensitiveAttributeRedactor Validate metadata and remove credentials or personal data before export.
Internal diagnostics TelemetryErrorHandler Observe timeouts and exporter failures without breaking business operations.
Semantic conventions SemanticAttributes, SemanticEvents, SemanticMetrics, SemanticHttp Produce consistent names and attributes, especially in instrumentations.
Trace creation TracerProvider, Tracer, Span, SpanData Measure operations, dependencies, and internal steps. SpanData is the immutable exporter snapshot.
Span model SpanContext, SpanKind, SpanStatus, SpanEvent, SpanLink, SpanLimits Represent distributed context, operation type, outcome, events, cross-trace relationships, and data limits.
Sampling Sampler, AlwaysOnSampler, AlwaysOffSampler, ParentBasedSampler, TraceIdRatioBasedSampler, SamplingParameters, SamplingResult, SamplingDecision Control which traces are recorded and exported.
ID generation IdGenerator, RandomIdGenerator Generate trace, span, and error IDs. Implement a custom generator only when a different valid policy is required.
Span processing SpanProcessor, SimpleSpanProcessor, BatchSpanProcessor, MultiSpanProcessor Use simple for immediate delivery, batch for production, and multi for multiple destinations.
Span export SpanExporter, ConsoleSpanExporter Implement a custom backend or inspect spans during development.
Logging LoggerProvider, Logger, LogRecord, LogSeverity Emit structured events correlated with the current span.
Log processing LogProcessor, SimpleLogProcessor, BatchLogProcessor, MultiLogProcessor, LogExporter, ConsoleLogExporter Control how logs are batched and where they are sent.
Synchronous metrics MeterProvider, Meter, Counter, UpDownCounter, Gauge, Histogram Record measurements directly from application code.
Observable metrics ObservableCounter, ObservableUpDownCounter, ObservableGauge, Observation Read current values during collection, such as queue depth or resource use.
Metric collection MetricReader, ManualMetricReader, PeriodicMetricReader, MetricExporter, ConsoleMetricExporter Use manual collection in tests and periodic collection in long-running services.
Metric model InstrumentKind, MetricPoint, MetricData, MetricView, AggregationTemporality, MetricProducer Implement exporters/readers and configure buckets, cardinality, and temporality.
Error capture ErrorManager, Errors, ErrorReport, ErrorId, ErrorSeverity, ErrorMechanism Turn exceptions into enriched, correlated reports.
Error context ErrorScope, ErrorUser, ErrorBreadcrumb, ErrorAttachment Add user, recent activity, tags, contexts, fingerprints, and attachments without leaking across requests.
Error policy ErrorProcessor, ErrorSanitizingProcessor, ErrorDeduplicationProcessor, ErrorSamplingProcessor, ErrorRateLimitProcessor, ErrorBeforeSendProcessor Filter, transform, deduplicate, sample, and rate-limit reports before export.
Error export ErrorExporter, MultiErrorExporter, ConsoleErrorExporter Connect a backend, fan out to destinations, or inspect reports locally.
Isolate errors ErrorIsolateListener Listen for uncaught errors from a secondary isolate; the caller must close it.
Trace propagation TracePropagator, W3CTraceContextPropagator, CompositeTracePropagator Continue traces through headers or combine propagation formats.
Baggage Baggage, BaggageEntry, BaggagePropagator, W3CBaggagePropagator Carry small business-context values across operations and services.

Only declarations exported by package:obsi/obsi.dart belong to the stable API. Paths under package:obsi/src/... are internal.

Official plugins #

Package What it adds When to use it
obsi_exporter_otlp OTLP HTTP/Protobuf and HTTP/JSON exporters OpenTelemetry Collector and compatible observability backends.
obsi_error_sentry Sentry error exporter Dart or Flutter applications using Sentry.
obsi_error_crashlytics Firebase Crashlytics exporter Flutter application error delivery.
obsi_flutter Flutter error capture and NavigatorObserver Global Flutter failures, navigation traces, and screen-duration metrics.
obsi_instrumentation_http Instrumented package:http client HTTP traces, metrics, and propagation.
obsi_instrumentation_dio Dio interceptor Instrument existing Dio clients without wrapping every request.
obsi_instrumentation_shelf Shelf middleware Continue traces and measure inbound Shelf requests.

Complete examples are available for a CLI, Shelf server, and Flutter application.

Obsi 1.x follows semantic versioning. Compatible additions may be released in a minor version; incompatible changes are reserved for a major version. The project is distributed under the MIT License.

1
likes
160
points
62
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Production-focused tracing, logging, metrics, errors, and W3C context propagation for pure Dart.

Repository (GitHub)
View/report issues
Contributing

Topics

#observability #tracing #logging #metrics #errors

License

MIT (license)

More

Packages that depend on obsi