Skip to content

cyclone-pk/steno

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

steno

Structured-first, async-safe, pluggable logging for Dart and Flutter.

License: MIT Dart Flutter

steno is a logger built around an immutable LogEvent rather than a formatted string. Every log carries typed fields, an error, a stack trace, inherited scope and span context, and trace identifiers — and it all flows through a clean filter → enrich → redact → sample → transform → dispatch pipeline before reaching one or more sinks.

This README is a complete tour of the public API. Every feature has working code and the actual output you should see when you run it. A single runnable file with all the snippets lives at packages/steno/example/comprehensive_example.dart.


This is a monorepo

This repository contains two packages, managed with melos:

Package Path Purpose
steno packages/steno Pure-Dart core. Runs on VM, web, and Flutter. Event model, logger API, pipeline, formatters, sinks.
steno_flutter packages/steno_flutter Flutter integration: runStenoApp, error handlers, DeveloperLogSink, device/app enricher, route observer.

A complete demo Flutter app using both packages lives at packages/steno_flutter/example.

Workspace setup

# One-time
dart pub global activate melos
git clone https://github.com/cyclone-pk/steno.git
cd steno
dart pub get          # bootstraps melos itself
melos bootstrap       # links the local packages

# Common tasks
melos run analyze       # dart analyze in every Dart package
melos run analyze:flutter
melos run test          # dart test in every Dart package
melos run test:flutter
melos run format        # dart format the workspace
melos run publish:dry   # publish dry-run on every package

Table of contents

  1. Why steno
  2. Install
  3. The five concepts
  4. Bootstrap and profiles
  5. Loggers and hierarchy
  6. Levels, isEnabled, and lazy fields
  7. Structured fields, nesting, tags
  8. Errors and stack traces
  9. Async-safe scopes
  10. Spans
  11. LoggerConfig and overrides
  12. Filters
  13. Enrichers
  14. Redactors
  15. Samplers
  16. Transformers
  17. Formatters
  18. Sinks (console, memory, multi, batching)
  19. File and rolling-file sinks
  20. Diagnostics: LoggerHealth
  21. Custom runtime / no global facade
  22. Trace context and source location
  23. LogFieldValue helpers
  24. Testing helpers
  25. Direct context API
  26. Flush and shutdown
  27. Pipeline order
  28. Public API reference

1. Why steno

Capability What you get
Structured first Every log is a LogEvent with typed fields, not a formatted string.
Async-safe scopes LogScope.run injects fields that survive await, Future, Stream, and Timer callbacks.
Real spans runSpan measures duration, propagates trace IDs, auto-emits a finish event.
Pipeline filter → enrich → redact → sample → transform → dispatch, each stage pluggable.
Multiple sinks Pretty console + JSON file + remote service all from the same event.
Redaction built in Mask / partial / hash / remove sensitive keys at any depth or by exact dotted path.
Sampling, batching, backpressure Burst sampler, batching sink, configurable drop policies.
Crash-safe A throwing sink can never take down the app — every failure is contained and counted in LoggerHealth.
Pure-Dart core Runs on the VM, web, and Flutter. dart:io sinks are in a separate library.
Flutter integration Available as the optional steno_flutter package.

2. Install

dependencies:
  steno: ^0.1.0

For Flutter integration (error handlers, route observer, dart:developer sink, device enricher) also add:

dependencies:
  steno_flutter: ^0.1.0

To use file sinks (FileSink, RollingFileSink) you import a separate library entry-point:

import 'package:steno/steno.dart';
import 'package:steno/io.dart';   // FileSink, RollingFileSink
import 'package:steno/testing.dart'; // FakeClock, MemoryBufferSink

3. The five concepts

Concept What it is
AppLogger What your code calls (log.info, log.error, …).
LogEvent The single immutable object every log becomes.
LogScope Async-safe inherited fields/tags via LogScope.run.
LogSpan Timed operation; emits a finish event with duration.
LogSink Where events end up (console, file, remote, memory, …).

Everything else (filters, enrichers, redactors, samplers, transformers, formatters) supports those five.

        ┌────────────┐
        │ AppLogger  │  log.info / log.error / ...
        └─────┬──────┘
              │ builds LogEvent (merges defaults + scope + span + call site)
              ▼
        ┌────────────┐
        │  Pipeline  │   filter → enrich → redact → sample → transform
        └─────┬──────┘
              │
              ▼
        ┌────────────┐
        │   Sinks    │   console, file, rolling, memory, multi, batching
        └────────────┘

Merge precedence is logger defaults < scope fields < span fields < call-site fields — the most local thing always wins.


4. Bootstrap and profiles

There are three predefined profiles. Pick one at startup; you can always customize it with copyWith.

import 'package:steno/steno.dart';

void main() {
  // 4a. Development — colored, verbose, source location enabled.
  Steno.configure(LoggerProfile.development());
  Steno.get('app').info('configured for development');

  // 4b. Testing — fixed clock, in-memory sink, no I/O.
  final memory = MemoryBufferSink();
  Steno.resetWith(LoggerProfile.testing(memorySink: memory));
  Steno.get('app').warn('configured for testing');
  print(memory.events.single.message); // "configured for testing"

  // 4c. Production — JSON output, default credential redaction, burst sampling.
  Steno.resetWith(LoggerProfile.production());
  Steno.get('app').info('configured for production', fields: {
    'userId': 'u_1',
    'password': 'shh', // masked by the production-default redactor
  });
}

Output (truncated):

20:48:34.867 INFO  app | configured for development
{"ts":"2026-04-10T00:48:34.876Z","level":"INFO","logger":"app","msg":"configured for production","fields":{"userId":"u_1","password":"***"}}
Profile Min level Format Source capture Redaction Sampling
development trace Pretty (color) warn+ none none
testing trace Memory only off none none
production info JSON off 17 default keys Burst 100/sec, error-aware

5. Loggers and hierarchy

Loggers are named hierarchically and cached by LoggerFactory. Child loggers inherit default fields and tags.

final root = Steno.get('app');
final auth = root.child('auth', fields: {'module': 'auth'});
final api  = auth.child('api', tags: ['network']);
final http = api.child('http', fields: {'protocol': 'h2'}, tags: ['http']);

http.info('GET /users', fields: {'status': 200, 'duration_ms': 12});

Output:

20:48:34.886 INFO  app.auth.api.http | GET /users
  • module: "auth"
  • protocol: "h2"
  • status: 200
  • duration_ms: 12
  tags: network #http

Per-logger level overrides — most specific prefix wins:

Steno.configure(Steno.config.copyWith(
  levelOverrides: {
    'app.auth':     LogLevel.debug,
    'app.auth.api': LogLevel.warn,
  },
));

Steno.get('app').debug('hidden — root is at info');
Steno.get('app.auth').debug('shown — auth is at debug');
Steno.get('app.auth.api').debug('hidden — api is at warn');
Steno.get('app.auth.api').warn('shown — api warn passes');

6. Levels, isEnabled, and lazy fields

final log = Steno.get('app');
log.trace('trace event');
log.debug('debug event');
log.info('info event');
log.warn('warn event');
log.error('error event');
log.fatal('fatal event');

LogLevel is totally ordered. LogLevel.off is the threshold sentinel that disables a logger entirely:

LogLevel.warn.isAtLeast(LogLevel.info); // true
LogLevel.parse('error');                // LogLevel.error

Guard expensive log construction with isEnabled or — better — with the fieldsBuilder parameter, which is only invoked when the level is enabled:

Steno.configure(Steno.config.copyWith(minLevel: LogLevel.warn));

var calls = 0;
log.debug(
  'expensive payload',
  fieldsBuilder: () { calls++; return {'big': hugePayload()}; },
);
print('debug builder calls: $calls'); // 0  — never invoked

7. Structured fields, nesting, tags

Allowed field types: null, bool, num, String, DateTime, Duration, Uri, lists, and maps with String keys. Nesting is unlimited.

log.info('full field types', fields: {
  'string': 'hello',
  'int': 42,
  'double': 3.14,
  'bool': true,
  'date': DateTime.utc(2026, 4, 9),
  'duration': const Duration(milliseconds: 250),
  'uri': Uri.parse('https://api.example.com/users'),
  'list': [1, 'two', false],
  'nested': {'kind': 'event', 'meta': {'count': 3}},
});

log.info('with tags', tags: ['network', 'auth', 'critical']);

Output:

20:48:34.894 INFO  app | full field types
  • string: "hello"
  • int: 42
  • double: 3.14
  • bool: true
  • date: "2026-04-09T00:00:00.000Z"
  • duration: 250000
  • uri: "https://api.example.com/users"
  • list: [1, "two", false]
  • nested: {
    kind: "event"
    meta: {
      count: 3
    }
  }

DateTime becomes ISO-8601, Duration becomes microseconds, Uri becomes its string form. See LogFieldValue helpers.


8. Errors and stack traces

error, warn, and fatal accept an error and stackTrace. They become first-class fields on the resulting LogEvent and on JSON output appear under an error object.

try {
  throw StateError('something blew up');
} catch (e, st) {
  log.error('caught a state error', error: e, stackTrace: st, fields: {
    'requestId': 'r_001',
  });
}

Output:

20:48:34.895 ERROR app | caught a state error
  • requestId: "r_001"
  ⚠ StateError: Bad state: something blew up
      #0      section5_errorsAndStackTraces (file:///...)

JSON form:

{"ts":"...","level":"ERROR","logger":"app","msg":"caught a state error",
 "fields":{"requestId":"r_001"},
 "error":{"type":"StateError","message":"Bad state: something blew up","stack":"#0 ..."}}

9. Async-safe scopes

LogScope.run installs inherited fields and tags into a child Dart zone. Every log emitted inside the body — including from awaits, futures, streams, and timers — automatically carries them.

await LogScope.run({'requestId': 'r_async', 'userId': 'u_1'}, () async {
  log.info('before await');
  await Future<void>.delayed(const Duration(milliseconds: 5));
  log.info('after await'); // still carries requestId + userId
}, tags: ['scoped']);

Scopes nest, and inner scopes win on key conflicts:

LogScope.run({'a': 1, 'b': 1}, () {
  LogScope.run({'b': 2, 'c': 3}, () {
    log.info('merged scope');
    // a:1, b:2, c:3
  });
});

Scopes propagate into Timers:

LogScope.run({'requestId': 'r_timer'}, () {
  Timer(Duration.zero, () {
    log.info('inside Timer callback'); // carries requestId
  });
});

Inspect the active scope directly:

LogScope.currentFields // Map<String, Object?>
LogScope.currentTags   // List<String>

There is also a shortcut on AppLogger:

log.scope({'k': 'v'}, () => log.info('via logger.scope'));

10. Spans

Spans measure timed operations and propagate trace IDs.

Manual lifecycle

final span = log.startSpan('db.query', fields: {'kind': 'manual'});
span.setField('phase', 'one');
span.addEvent('milestone-a', fields: {'note': 'reached A'});
span.finish(fields: {'extra': 'on-finish'});

startSpan returns immediately and does not install the span as active scope context. Use runSpan for that.

Async runSpan (auto-finish, scoped trace)

final result = await log.runSpan<int>('compute.sum', (span) async {
  span.setField('input.size', 3);
  await Future<void>.delayed(const Duration(milliseconds: 2));
  log.info('inside async span'); // inherits trace + span field
  return 6;
});

If the body throws, the span is auto-failed:

try {
  await log.runSpan('compute.bad', (span) async {
    throw StateError('intentional');
  });
} catch (_) { /* span emitted with span.status=error */ }

Synchronous runSpanSync

log.runSpanSync<void>('render.frame', (span) {
  span.setField('frame.no', 1);
  log.info('inside sync span');
});

runSpanSync throws StateError if you give it an async body — use runSpan for that.

Cancellation

final span = log.startSpan('manual.cancel');
span.cancel(fields: {'reason': 'user-aborted'});

Nested spans become parent/child traces

await log.runSpan('outer', (outer) async {
  await log.runSpan('inner', (inner) async {
    print(inner.trace.parentSpanId); // = outer.trace.spanId
  });
});

Span finish events carry:

span.name, span.status, span.elapsed_us, span.events (if any),
trace { trace_id, span_id, parent_span_id }

11. LoggerConfig and overrides

LoggerConfig is the single immutable configuration object. Build one directly or call .copyWith on a profile.

final config = LoggerConfig(
  minLevel: LogLevel.debug,
  levelOverrides: {'app.noisy': LogLevel.error},
  sinks: [ConsoleSink(formatter: const PrettyConsoleFormatter())],
  filters: const [LevelFilter(LogLevel.debug)],
  enrichers: const [StaticFieldsEnricher({'env': 'dev'})],
  redactors: [KeyPathRedactor(keys: const {'password'})],
  samplers: [ErrorAwareSampler(BurstSampler(burst: 100, window: const Duration(seconds: 1)))],
  transformers: const [],
  captureSource: true,
  captureSourceLevels: const {LogLevel.warn, LogLevel.error, LogLevel.fatal},
  clock: () => DateTime.now(),
);

print(config.effectiveLevelFor('app'));        // LogLevel.debug
print(config.effectiveLevelFor('app.noisy'));  // LogLevel.error
print(config.shouldCaptureSourceFor(LogLevel.warn)); // true

captureSource walks the current stack trace to fill in SourceLocation on the event — expensive, so it is opt-in and typically gated to high-severity levels only.


12. Filters

Filters drop events before they reach later stages. Five built-in filters cover the common cases.

Steno.resetWith(LoggerProfile.testing(memorySink: memory).copyWith(
  filters: [
    const LevelFilter(LogLevel.info),                 // drop below info
    const NameFilter.denyList(['app.spam']),          // drop noisy logger
    NameFilter.allowList(const ['app']),              // only keep app.*
    TagFilter(blocked: const {'pii'}),                // drop PII tags
    PredicateFilter('drop-empty', (e) => e.message.isNotEmpty),
  ],
));

13. Enrichers

Enrichers augment every event with fields or tags. Three built-ins, all composable.

Steno.resetWith(LoggerProfile.testing(memorySink: memory).copyWith(
  enrichers: [
    const StaticFieldsEnricher({
      'env': 'dev', 'region': 'us-east-1', 'service': 'demo',
    }),
    const StaticTagsEnricher(['demo']),
    CallbackEnricher('PidEnricher', (e) {
      return e.copyWith(
        context: LogContext.of({...e.context.fields, 'pid': pid}),
      );
    }),
  ],
));

Steno.get('app').info('enriched event');
// fields: {env: dev, region: us-east-1, service: demo, pid: 43492}
// tags:   [demo]

Static enricher fields sit under call-site fields, so callers can always override them.


14. Redactors

KeyPathRedactor matches either keys (anywhere in the field tree, case-insensitive) or paths (exact dotted path from the root). There are four strategies.

Steno.resetWith(LoggerProfile.testing(memorySink: memory).copyWith(
  redactors: [
    KeyPathRedactor(
      keys: const ['password'],
      paths: const ['headers.authorization'],
    ),
    KeyPathRedactor(
      keys: const ['ssn'],
      strategy: RedactionStrategy.partial,
    ),
    KeyPathRedactor(
      keys: const ['email'],
      strategy: RedactionStrategy.hash,
    ),
    KeyPathRedactor(
      keys: const ['internal_debug'],
      strategy: RedactionStrategy.remove,
    ),
  ],
));

Steno.get('app').info('login attempt', fields: {
  'username': 'alice',
  'password': 'hunter2',
  'ssn': '123-45-6789',
  'email': 'alice@example.com',
  'internal_debug': 'should-disappear',
  'headers': {'authorization': 'Bearer abcdef', 'content-type': 'application/json'},
});

Output:

{username: alice,
 password: ***,
 ssn: 1*********9,
 email: sha:94a4b546,
 headers: {authorization: ***, content-type: application/json}}

RedactionStrategy values: mask, partial, hash, remove.

LoggerProfile.production() ships with a KeyPathRedactor that already covers the 17 most common credential keys (password, token, cookie, authorization, creditcard, ssn, etc.). You can extend it via LoggerProfile.production(extraRedactKeys: {...}).


15. Samplers

Samplers drop excess events. Wrap any sampler in ErrorAwareSampler to guarantee error/fatal events are never dropped.

RateSampler(3)            // keep 1 of every 3 per logger
FirstNSampler(2)          // keep first 2 per logger, then drop
BurstSampler(burst: 2, window: const Duration(seconds: 1)) // 2 per sec
ErrorAwareSampler(inner)  // bypass for error+ events

Output:

RateSampler kept 3/9
FirstNSampler kept 2/5
BurstSampler kept 2/5
ErrorAware kept 1: [kept — error always passes]

16. Transformers

Transformers reshape events between sampling and dispatch. Three built-ins.

Steno.resetWith(LoggerProfile.testing(memorySink: memory).copyWith(
  transformers: [
    const RenameFieldsTransformer({'uid': 'userId'}),
    const GroupFieldsTransformer(
      prefix: 'http',
      keys: {'method', 'path', 'status'},
    ),
    CallbackTransformer('upper-msg', (e) {
      return e.copyWith(message: e.message.toUpperCase());
    }),
  ],
));

Steno.get('app').info('request done', fields: {
  'uid': 'u_1', 'method': 'GET', 'path': '/users',
  'status': 200, 'duration_ms': 12,
});

Output:

message: REQUEST DONE
fields:  {userId: u_1, duration_ms: 12, http: {method: GET, path: /users, status: 200}}

A transformer that returns null drops the event (and increments LoggerHealth.eventsTransformedAway).


17. Formatters

Three built-in formatters, all implementing LogFormatter.

const PrettyConsoleFormatter().format(event);
const JsonFormatter().format(event);
const CompactLineFormatter().format(event);

PrettyConsoleFormatter (no color):

10:22:08.194 INFO  app.auth | user signed in
  • userId: "u_1"
  • method: "oauth"
  tags: auth

JsonFormatter:

{"ts":"2026-04-09T14:22:08.194Z","level":"INFO","logger":"app.auth","msg":"user signed in","fields":{"userId":"u_1","method":"oauth"},"tags":["auth"]}

CompactLineFormatter:

10:22:08.194 INFO  app.auth "user signed in" userId=u_1 method=oauth #auth

The pretty formatter accepts useColor, showLogger, showFields, showSource. The JSON formatter accepts includeStackTrace and includeSource.

You can implement your own by extending LogFormatter — the only requirement is a single String format(LogEvent).


18. Sinks (console, memory, multi, batching)

Sink Use case
ConsoleSink stdout / IDE console / print
MemoryBufferSink tests, in-process crash buffer
MultiSink fan one event out to many sinks
BatchingSink front a slow sink with a queue + batching + backpressure
FileSink append to a single file (io.dart)
RollingFileSink size-rotated file with retention (io.dart)
final ring = MemoryBufferSink(capacity: 3);
for (var i = 0; i < 5; i++) ring.write(_event(i));
print(ring.events.map((e) => e.message)); // (m2, m3, m4)
final lines = <String>[];
final mem = MemoryBufferSink();
final multi = MultiSink([
  ConsoleSink(formatter: const CompactLineFormatter(), writer: lines.add),
  mem,
]);
multi.write(event); // both sinks see it
final batch = BatchingSink(
  target: RollingFileSink(path: 'app.log'),
  maxBatchSize: 64,
  flushInterval: const Duration(seconds: 2),
  queueCapacity: 2048,
  policy: BackpressurePolicy.dropOldest,
  onDropped: (n, _) => print('dropped $n'),
);

Backpressure policies:

BackpressurePolicy.dropOldest    // ring-buffer style
BackpressurePolicy.dropNewest    // refuse incoming
BackpressurePolicy.dropBelowWarn // keep warn+, drop the rest
BackpressurePolicy.block         // (best-effort, see source)

ConsoleSink accepts a custom writer: (String) -> void so you can route output anywhere (a test buffer, a Stream, an IDE channel). Pass a minLevel: to make a sink ignore everything below a threshold without configuring a global filter.


19. File and rolling-file sinks

FileSink and RollingFileSink live in package:steno/io.dart because they depend on dart:io.

import 'package:steno/io.dart';

final file = FileSink(
  path: 'app.log',
  formatter: const JsonFormatter(),
  append: true,
);

final rolling = RollingFileSink(
  path: 'app.log',
  maxBytes: 5 * 1024 * 1024,   // 5 MB per file
  keep: 5,                      // keep app.log.1 .. app.log.5
  formatter: const JsonFormatter(),
);

Steno.configure(LoggerConfig(sinks: [
  ConsoleSink(formatter: const PrettyConsoleFormatter()),
  BatchingSink(target: rolling),
]));

Both sinks are crash-safe: every internal failure is swallowed, and writes are serialized through a tail future so flush() and close() deterministically wait for in-flight writes to complete.

RollingFileSink rotates lazily on the next write that would push the file past maxBytes. You can also force rotation:

await rolling.rotate();

20. Diagnostics: LoggerHealth

Every runtime exposes a LoggerHealth instance. Read it any time:

print(Steno.health.snapshot());
// { accepted: 9, filtered_out: 0, sampled_out: 0,
//   transformed_away: 0, dispatched: 9, sink_failures: {} }

Steno.health.reset();

A throwing sink does not crash the app — it bumps sink_failures[sinkName] and the runtime moves on:

class _BadSink implements LogSink {
  // ...
  @override
  void write(LogEvent e) => throw StateError('intentional');
}

Steno.resetWith(LoggerConfig(sinks: [memorySink, _BadSink()]));
Steno.get('app').info('hi');
print(Steno.health.snapshot());
// sink_failures: {AlwaysFailingSink: 1}

21. Custom runtime / no global facade

The Steno facade is a thin wrapper around a single LoggerRuntime and LoggerFactory. Tests, embedded scenarios, or libraries that need isolation can construct their own:

final runtime = LoggerRuntime(LoggerConfig(sinks: [MemoryBufferSink()]));
final factory = LoggerFactory(runtime);

final log = factory.get('private.app');
log.info('runs through a private runtime');

await runtime.flushAll();
factory.clear();

LoggerRuntime exposes:

runtime.config              // current LoggerConfig
runtime.reconfigure(other)  // hot-swap config
runtime.health              // LoggerHealth instance
runtime.dispatch(event)     // run an event through the pipeline
runtime.isEnabled(name, level)
runtime.flushAll(timeout: ...)
runtime.shutdown()
runtime.sinks

22. Trace context and source location

TraceContext carries W3C-style trace IDs:

final root  = TraceContext.root();
final child = root.child();
print(root.traceId);   // 16-byte hex
print(child.spanId);   // 8-byte hex
print(child.parentSpanId == root.spanId); // true

Steno populates this automatically inside runSpan. You can also use SourceLocation.captureCurrent() to grab the current stack frame, or SourceLocation.captureFrom(stackTrace) to parse a frame.


23. LogFieldValue helpers

Validate and normalize values without going through a logger:

isValidFieldValue('hi')                    // true
isValidFieldValue(DateTime.now())          // true
isValidFieldValue(<int, Object?>{1: 'a'})  // false (non-string key)
isValidFieldValue(Object())                // false

toJsonFieldValue({
  'when': DateTime.utc(2026, 4, 9),
  'duration': const Duration(seconds: 1),
  'url': Uri.parse('https://x.test'),
});
// { when: "2026-04-09T00:00:00.000Z", duration: 1000000, url: "https://x.test" }

24. Testing helpers

import 'package:steno/steno.dart';
import 'package:steno/testing.dart';

test('emits user info', () {
  final memory = MemoryBufferSink();
  Steno.resetWith(LoggerProfile.testing(memorySink: memory));
  Steno.get('app').info('hi', fields: {'userId': 'u_1'});
  expect(memory.events.single.context['userId'], 'u_1');
});

test('time-deterministic', () {
  final clock = FakeClock(DateTime.utc(2030, 6, 15, 10, 0, 0));
  final memory = MemoryBufferSink();
  Steno.resetWith(LoggerProfile.testing(memorySink: memory).copyWith(
    clock: clock.now,
  ));
  Steno.get('app').info('frozen #1');
  clock.advance(const Duration(seconds: 5));
  Steno.get('app').info('frozen #2');
  expect(memory.events.first.timestamp, DateTime.utc(2030, 6, 15, 10, 0, 0));
  expect(memory.events.last.timestamp,  DateTime.utc(2030, 6, 15, 10, 0, 5));
});

25. Direct context API

ContextMerger, ScopeSnapshot, and ZoneContextStore are public so you can build custom integrations on top of the same context system the runtime uses.

final scope = ScopeSnapshot(
  context: LogContext.of({'requestId': 'r_1'}),
  tags: const ['demo'],
  trace: TraceContext.root(),
);

final merged = ContextMerger.merge(
  loggerDefaults: LogContext.empty,
  scope: scope,
  eventFields: {'extra': true},
);
// {requestId: r_1, extra: true}

final tags = ContextMerger.mergeTags(
  loggerTags: const ['app'],
  scope: scope,
  eventTags: const ['call-site'],
);
// [app, demo, call-site]

ZoneContextStore.run(scope, () {
  print(ZoneContextStore.current.context.fields); // {requestId: r_1}
});

26. Flush and shutdown

await Steno.flush();          // flush every sink, default 2s timeout
await Steno.shutdown();       // close every sink

After shutdown() the runtime should be considered drained. Call Steno.resetWith(config) to start over.


27. Pipeline order

Steno guarantees a strict pipeline order, verified by tests:

filter → enrich → redact → sample → transform → dispatch

Each stage is wrapped in a try/catch so a failure in one is contained and never breaks the rest of the pipeline. A throwing filter is treated as "accept" (fail open), a throwing enricher / redactor / transformer is treated as a no-op for that event, and a throwing sink is counted in LoggerHealth.sink_failures.


28. Public API reference

The package:steno/steno.dart library exports:

API

  • AppLogger, LoggerFactory, LogScope, LogSpan, SpanStatus

Event model

  • LogEvent, LogLevel, LogContext, TraceContext, SourceLocation
  • isValidFieldValue, toJsonFieldValue

Context

  • ScopeSnapshot, ZoneContextStore, ContextMerger

Pipeline

  • Interfaces: LogProcessor, LogFilter, LogEnricher, LogRedactor, LogSampler, LogTransformer
  • Filters: LevelFilter, NameFilter, TagFilter, PredicateFilter
  • Enrichers: StaticFieldsEnricher, StaticTagsEnricher, CallbackEnricher
  • Redactors: KeyPathRedactor, RedactionStrategy
  • Samplers: RateSampler, FirstNSampler, BurstSampler, ErrorAwareSampler
  • Transformers: RenameFieldsTransformer, GroupFieldsTransformer, CallbackTransformer

Formatters

  • LogFormatter, PrettyConsoleFormatter, JsonFormatter, CompactLineFormatter

Sinks (pure-Dart)

  • LogSink, BackpressurePolicy
  • ConsoleSink, MemoryBufferSink, MultiSink, BatchingSink

Sinks (package:steno/io.dart)

  • FileSink, RollingFileSink

Test helpers (package:steno/testing.dart)

  • FakeClock, MemoryBufferSink

Config / runtime / facade

  • LoggerConfig, LoggerProfile, LoggerRuntime, LoggerHealth, Steno

Run packages/steno/example/comprehensive_example.dart to see every one of them in action with real output.

License

MIT.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages