structured_log_drift
See every query your drift database runs — its SQL, how long it took, what it returned, and why it failed — as a structured log entry, with one line of setup and no argument values leaking.
Documentation: structured-log.openidealab.com.
A screen that hangs or comes up empty is often a query that was slow or
failed, and nothing in the log says so. drift’s own logStatements: true
prints every statement with its argument values through print: password
hashes, tokens and personal data go to the console, without a level, a
duration or the error. So it stays off in production, and the queries stay
invisible.
StructuredLogDriftInterceptor is a drift QueryInterceptor. Passed to
interceptWith once, it writes every query, batch, failure and transaction
end as a structured_log entry — the kind of query, its SQL, its duration,
the rows it returned or changed — and flags the slow ones. Argument values
stay out unless you ask for them.
Features
Section titled “Features”Coverage
Section titled “Coverage”- One line to install —
NativeDatabase(file).interceptWith(StructuredLogDriftInterceptor()). - Every query —
select,insert,update,deleteand custom statements, withrows,affected_rowsorinsert_id. - Durations that mean something —
duration_mskeeps microsecond precision, since a query against a local database usually takes less than a millisecond. - Slow queries flagged — at or above a threshold (500 ms by default) a
query is a
warningwithslow: true. - Batches as one entry — a
batchis onedb_batch, not one entry per statement. - Failures with their stack — and the exception still reaches your code exactly as drift threw it.
- Transactions — commits and rollbacks, nested ones timed on their own.
- Any executor — native, web, an isolate: it is drift’s own interception point.
Control
Section titled “Control”- Its own category — every entry carries
category: 'db', so aLogSinkcan route database traffic separately and the in-app log viewer offers it as a filter. - A level per kind of entry — or
nullto turn it off. - A filter — by kind and statement, to leave out
PRAGMAs or a table.
Safety
Section titled “Safety”- No argument values by default — not in the entry, and not in the error’s text either (see below).
- Never changes a query — logging that fails costs the entry, never the query’s result or exception.
- Costs little when off — an entry no sink would take is never built.
Where it fits
Section titled “Where it fits”The interceptor needs nothing but
structured_log — no server, no
Flutter. 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 db 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. It needs drift 2.14.0 or later, where
QueryInterceptor appeared:
dependencies: drift: ^2.14.0 structured_log: ^0.3.0 structured_log_drift: ^0.1.0-dev.1Quick Start
Section titled “Quick Start”import 'package:drift/native.dart';import 'package:structured_log/structured_log.dart';import 'package:structured_log_drift/structured_log_drift.dart';
void main() { StructlogConfiguration.configure( sinks: [LogSink(name: 'console', output: coloredConsoleOutput)], );
final db = AppDatabase( NativeDatabase(file).interceptWith(StructuredLogDriftInterceptor()), ); // ...}The same interceptWith works on any QueryExecutor or
DatabaseConnection — a WasmDatabase on the web, or the connection of a
DriftIsolate.
An insert with a token argument, a batch of two inserts, a select and a failing transaction then log (timestamps dropped, error and stack trace shortened):
{"logger":"drift","category":"db","kind":"insert","statement":"INSERT INTO users (name, token) VALUES (?, ?)","duration_ms":1.796,"insert_id":1,"event":"db_query","level":"debug"}{"logger":"drift","category":"db","statement_count":1,"execution_count":2,"duration_ms":0.491,"event":"db_batch","level":"debug"}{"logger":"drift","category":"db","kind":"select","statement":"SELECT * FROM users","duration_ms":1.525,"rows":3,"event":"db_query","level":"debug"}{"logger":"drift","category":"db","kind":"custom","statement":"INSERT INTO missing VALUES (1)","duration_ms":2.476,"error":"SqliteException(1): while executing, no such table: missing, …","error_type":"SqliteException","stack_trace":"…","event":"db_query_failed","level":"error"}{"logger":"drift","category":"db","duration_ms":4.853,"event":"db_transaction_rolled_back","level":"warning"}See example/main.dart for a runnable version
(dart run example/main.dart).
What gets logged
Section titled “What gets logged”Every entry carries category (db by default) and logger (drift by
default).
event |
When | Default level | Fields |
|---|---|---|---|
db_query |
a query ran | debug; warning if slow |
kind, statement, duration_ms; rows (select), affected_rows (update, delete), insert_id (insert); slow: true if slow; arguments if enabled |
db_batch |
a batch ran | debug; warning if slow |
statement_count (distinct statements), execution_count, duration_ms; slow: true if slow |
db_query_failed |
a query or batch threw | error |
kind (batch for a batch), statement, duration_ms, error, error_type, stack_trace |
db_transaction_committed |
a transaction committed | trace |
duration_ms since it began |
db_transaction_rolled_back |
a transaction rolled back | warning |
duration_ms since it began |
kind is select, insert, update, delete or custom. statement is
cut to 2000 characters.
drift runs every batch in a transaction of its own, so a batch is followed
by a db_transaction_committed. Commits are at trace, which a sink’s
default minLevel (debug) filters out, so a batch is not logged twice.
Lower a sink’s minLevel to see them.
What is not logged: migrations. drift runs them on the executor underneath the interceptor before the database opens; a failing migration shows up as the exception from opening it.
Keeping secrets out of the log
Section titled “Keeping secrets out of the log”The statements drift builds carry placeholders (?, $1), not values, and
argument values are not written by default. They are where password
hashes, tokens and personal data live.
They would also leak through an error: sqlite3 ends its exception text
with every parameter of the failing statement, and a PostgreSQL unique
violation quotes the duplicate value. So while arguments are off, the
interceptor writes error itself, with the text scrubbed:
- the
parameters: …thatsqlite3appends becomesparameters: <hidden>; - every string argument of 4 characters or more becomes
<argument>wherever it appears. Shorter strings, numbers, dates and blobs are left alone — hiding every1would garble the message.
To write arguments anyway, pass logArguments: true: strings are cut to
1000 characters and blobs written as <N bytes>. A batch never writes its
arguments — there can be thousands of sets.
What the interceptor cannot see into is SQL you wrote yourself with values
inlined, such as customStatement("UPDATE users SET token = 'abc'"). Use
variables, or leave such statements out with filter.
Configuration
Section titled “Configuration”StructuredLogDriftInterceptor( // Which entries are written, and at what level; null turns one off. levels: const DriftLogLevels( query: LogLevel.trace, committed: null, ), // From how long a query is flagged slow; null never flags one. slowQueryThreshold: const Duration(milliseconds: 200), // Leave out drift's own housekeeping and a sensitive table. filter: (kind, statement) => !statement.startsWith('PRAGMA') && !statement.contains('sessions'), // Off by default; see above. logArguments: false, category: 'storage',);A stream from watch() reruns its query every time its tables change, and
each rerun is a db_query. If that is too much, lower query to trace or
filter the statement out: slow queries and failures still show.
With a DriftIsolate, the interceptor runs where you call interceptWith —
usually on the side that sends queries — so duration_ms includes the trip
to the isolate: how long your code waited.
API Reference
Section titled “API Reference”| Symbol | Description |
|---|---|
StructuredLogDriftInterceptor({logger, loggerName, category, levels, slowQueryThreshold, logArguments, filter}) |
The interceptor; pass it to interceptWith. Without logger, it calls getLogger(loggerName) on every entry. category: null binds no category. |
DriftLogLevels({query, batch, slow, failed, committed, rolledBack}) |
A LogLevel? per kind of entry; null turns it off. |
defaultStatementMaxLength |
2000 — the longest statement written. |
defaultArgumentMaxLength |
1000 — the longest string argument written, with logArguments on. |
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_logging— bridge that routespackage:loggingrecords intostructured_logstructured_log_riverpod—ProviderObserverforriverpod/flutter_riverpod
License
Section titled “License”MIT — see LICENSE.