structured_log_logging
Bring the logs of every library that writes through package:logging into
structured_log — the same sinks, in-app viewer and server as your own
entries — with one line of setup.
Documentation: structured-log.openidealab.com.
package:logging is what most of the Dart ecosystem logs through: database
drivers, code generators, HTTP stacks, plenty of application libraries. Their
records go nowhere on their own — only to whoever listens on
Logger.root.onRecord, usually a print in main. So they never reach your
log file, the in-app log viewer or a self-hosted structured_log_server, and
the one log you look at after an incident is missing exactly the driver that
failed.
StructuredLogLoggingBridge closes that gap. Attached once, it writes every
package:logging record as a structured_log entry, so it is routed,
filtered, redacted and shipped like any other — without touching the
libraries that wrote it.
Features
Section titled “Features”Coverage
Section titled “Coverage”- One line to attach —
StructuredLogLoggingBridge().attach(). - Every library at once — it listens on
Logger.root, the one stream everypackage:loggingrecord goes through. - Entries in the shape of your own — an error and a stack trace become
error,error_typeandstack_trace, exactly as withBoundLogger.error. - Levels by value —
FINEisdebug,SEVEREiserror, and a customLevel('NOTICE', 850)lands atinfowithout any setup.
Control
Section titled “Control”- Its own category — every entry carries
category: 'logging', so aLogSinkcan take, or leave, everything other libraries wrote. - Your level mapping — replace it, or return
nullto leave a level out. - A filter — leave noisy loggers out entirely.
- Fields of your own — add a request id kept in the record’s zone, or anything else a record carries.
Safety
Section titled “Safety”- Never throws into the caller — it runs inside the library’s own
Logger.logcall. A filter or level mapping that throws costs the record — it is where you keep records out, so the bridge errs on that side; a context callback that throws costs only its fields, and the entry names its type inbridge_failed. - Leaves
package:loggingalone — it never changes a logger’s level orhierarchicalLoggingEnabled.
Where it fits
Section titled “Where it fits”The bridge needs nothing but
structured_log and
package:logging — no server, no Flutter, and it works on the web. Its
entries flow to whatever sinks you have configured: the console, a file, an
in-app log viewer
(structured_log_material,
structured_log_fluent or
structured_log_cupertino),
where the logging category becomes a filter, or a self-hosted
structured_log_server through
structured_log_remote_sync.
It is one of nine adapters that log what your app’s libraries already do;
the whole project is at
structured-log.openidealab.com.
Installation
Section titled “Installation”Published on pub.dev as a pre-release:
dependencies: logging: ^1.2.0 structured_log: ^0.3.0 structured_log_logging: ^0.1.0-dev.1Quick Start
Section titled “Quick Start”import 'package:logging/logging.dart';import 'package:structured_log/structured_log.dart';import 'package:structured_log_logging/structured_log_logging.dart';
void main() { StructlogConfiguration.configure( sinks: [LogSink(name: 'console', output: coloredConsoleOutput)], ); Logger.root.level = Level.ALL; // see "Which records arrive" below StructuredLogLoggingBridge().attach();
// ...}A database driver that writes through package:logging then logs:
[2026-10-05T09:39:05.575330Z] INFO: connection opened {"logger":"db","category":"logging"}[2026-10-05T09:39:05.578816Z] DEBUG: query took 3 ms {"logger":"db","category":"logging"}[2026-10-05T09:39:05.579002Z] ERROR: query failed {"logger":"db","category":"logging","error":"Bad state: connection closed","error_type":"StateError"}See example/main.dart for a runnable version
(dart run example/main.dart).
What gets logged
Section titled “What gets logged”Each LogRecord becomes one entry:
| Record | Entry |
|---|---|
message |
event |
loggerName |
logger (root for the root logger) |
level |
level, mapped as below |
error |
error (its toString()) and error_type |
stackTrace |
stack_trace |
| — | category: 'logging' |
The record’s object is not written: its toString() already is the
message. Neither are its time, sequenceNumber or zone; structured_log
stamps its own timestamp, and context adds whatever else you want.
Level.value |
Level | Standard levels |
|---|---|---|
| below 500 | trace |
FINEST, FINER |
| 500–699 | debug |
FINE |
| 700–899 | info |
CONFIG, INFO |
| 900–999 | warning |
WARNING |
| 1000–1199 | error |
SEVERE |
| 1200 and up | critical |
SHOUT |
With recordStackTraceAtLevel set, package:logging adds a stack trace of
its own and, when there is no error, the string autogenerated stack trace for … in its place. The bridge keeps the stack trace and leaves that string
out.
Which records arrive
Section titled “Which records arrive”The bridge sees only the records package:logging creates, and
Logger.root creates INFO and above by default — a fine(...) call below
that is dropped before any listener hears of it. The bridge does not change
that for you: lowering Logger.root.level makes every library in the app
build the messages it was skipping, which is your call, not the bridge’s.
Logger.root.level = Level.ALL;A sink’s own minLevel then decides what is kept, as for any entry.
Keeping secrets out of the log
Section titled “Keeping secrets out of the log”Libraries put whatever they like in a message: a URL with its query, an SQL
statement with its arguments, a header. A message is the entry’s event, a
plain string, so redactKeys cannot look inside it. Leave out what you
don’t trust:
StructuredLogLoggingBridge( // A logger that is too talkative or too revealing. filter: (record) => record.loggerName != 'http.wire', // A level you never want, from anyone. levelOf: (level) => level < Level.FINE ? null : defaultLogLevelOf(level),).attach();and, for what stays, a processor of your own that rewrites event where
category is logging.
Logging from a sink
Section titled “Logging from a sink”package:logging publishes each record synchronously, and its stream does
not start a new record while it is still handing out the current one. The
bridge delivers during that hand-out, so code run by a structured_log
sink or processor that itself writes through package:logging — a sink
built on a client that logs through it, say — gets a StateError from that
call. The bridge cannot loop because of it, but the library making the call
sees the exception. Write from a sink asynchronously, or not through
package:logging.
Configuration
Section titled “Configuration”StructuredLogLoggingBridge( // Another logger than Logger.root; only matters with // hierarchicalLoggingEnabled, where each logger has a stream of its own. source: Logger('app'), // Your level mapping; null leaves a level out. levelOf: defaultLogLevelOf, // Leave records out. filter: (record) => !record.loggerName.startsWith('build'), // null writes no category. category: 'libraries', // Extra fields, such as a request id kept in the record's zone. context: (record) => {'request_id': record.zone?[#requestId]},).attach();attach again does nothing while attached; detach stops, and attach
starts again.
API Reference
Section titled “API Reference”| Symbol | Description |
|---|---|
StructuredLogLoggingBridge({source, levelOf, filter, category, context}) |
The bridge. It calls getLogger(loggerName) for each record, so a later StructlogConfiguration.configure reaches it too. category: null writes no category. |
attach() / detach() / isAttached |
Start bridging source’s records, stop, or ask which. |
LogLevelOf |
LogLevel? Function(Level level) — the level a record is written at; null leaves it out. |
defaultLogLevelOf(level) |
The default mapping, by Level.value. |
Related packages
Section titled “Related packages”The rest of the structured_log family:
Core
structured_log— structured JSON logging with context binding, processors and multi-sink routing
In-app log viewer
structured_log_flutter— headless viewer core:LogBufferandLogViewerControllerstructured_log_material— Material 3 log viewerstructured_log_fluent— Fluent UI (WinUI-style) log viewerstructured_log_cupertino— Cupertino (iOS-style) log viewer
Shipping logs to a server
structured_log_remote_sync— batching, retrying sink for a self-hostedstructured_log_server
Integrations
structured_log_bloc—BlocObserverforbloc/flutter_blocstructured_log_dio—diointerceptor that logs HTTP callsstructured_log_http_client—package:httpclient wrapper that logs HTTP callsstructured_log_go_router— logsgo_routernavigationstructured_log_auto_route— logsauto_routenavigationstructured_log_cherrypick— observer for thecherrypickDI containerstructured_log_drift— driftQueryInterceptorthat logs queries, failures and transactionsstructured_log_riverpod—ProviderObserverforriverpod/flutter_riverpod
License
Section titled “License”MIT — see LICENSE.