structured_log_logging 0.1.0-dev.3
structured_log_logging: ^0.1.0-dev.3 copied to clipboard
StructuredLogLoggingBridge — routes every package:logging record into structured_log, so the logs of libraries that write through package:logging reach the same sinks, viewer and server as your own.
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.
Why #
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 #
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 #
- 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 #
- 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 #
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 #
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.1
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 #
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 #
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 #
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 #
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 #
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 #
| 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 #
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 #
MIT — see LICENSE.