Structured-first, async-safe, pluggable logging for Dart and 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 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.
# 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- Why steno
- Install
- The five concepts
- Bootstrap and profiles
- Loggers and hierarchy
- Levels,
isEnabled, and lazy fields - Structured fields, nesting, tags
- Errors and stack traces
- Async-safe scopes
- Spans
- LoggerConfig and overrides
- Filters
- Enrichers
- Redactors
- Samplers
- Transformers
- Formatters
- Sinks (console, memory, multi, batching)
- File and rolling-file sinks
- Diagnostics:
LoggerHealth - Custom runtime / no global facade
- Trace context and source location
LogFieldValuehelpers- Testing helpers
- Direct context API
- Flush and shutdown
- Pipeline order
- Public API reference
| 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. |
dependencies:
steno: ^0.1.0For Flutter integration (error handlers, route observer,
dart:developer sink, device enricher) also add:
dependencies:
steno_flutter: ^0.1.0To 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| 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.
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 |
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');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.errorGuard 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 invokedAllowed 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.
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 ..."}}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'));Spans measure timed operations and propagate trace IDs.
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.
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 */ }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.
final span = log.startSpan('manual.cancel');
span.cancel(fields: {'reason': 'user-aborted'});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 }
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)); // truecaptureSource 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.
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),
],
));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.
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: {...}).
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+ eventsOutput:
RateSampler kept 3/9
FirstNSampler kept 2/5
BurstSampler kept 2/5
ErrorAware kept 1: [kept — error always passes]
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).
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).
| 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 itfinal 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.
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();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}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.sinksTraceContext 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); // trueSteno 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.
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" }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));
});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}
});await Steno.flush(); // flush every sink, default 2s timeout
await Steno.shutdown(); // close every sinkAfter shutdown() the runtime should be considered drained. Call
Steno.resetWith(config) to start over.
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.
The package:steno/steno.dart library exports:
API
AppLogger,LoggerFactory,LogScope,LogSpan,SpanStatus
Event model
LogEvent,LogLevel,LogContext,TraceContext,SourceLocationisValidFieldValue,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,BackpressurePolicyConsoleSink,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.
MIT.