structured_log_auto_route
Читать на русском.
See every screen your auto_route app opened, closed and switched between
as structured log entries — with tokens in the location redacted, and one
line of setup.
Documentation: structured-log.openidealab.com.
Why
"How did the user get here?" is the first question about any error on a
screen, and the answer is the path they took. Without a record you reproduce
the bug by hand or add a NavigatorObserver that prints — and on a user's
device you can do neither.
StructuredLogAutoRouteObserver keeps the record for you. Passed to the
router once, it writes every screen that was opened, closed or removed and
every tab switch as structured_log entries — with the route's name, its
pattern and its location as separate fields, so you can filter the story of
one screen out of everything else. The same ring of entries is what the
in-app log viewer shows and what structured_log_server stores.
Features
Coverage
- One line to install —
appRouter.config(navigatorObservers: () => [StructuredLogAutoRouteObserver()]). - Pushes, pops, removals, tabs — what
auto_routetells aNavigatorObserver, plus the tab hooks ofAutoRouterObserver. - Screens told apart — by name (
UserRoute), by pattern (/user/:id) and by where they actually are (/user/7?tab=posts). - Nested routes — a screen inside a shell is located below its parents.
- Works with generated and hand-written routers — it depends on
package:auto_routeonly.
Control
- Its own category — every entry carries
category: 'navigation', so aLogSinkcan route navigation separately and the in-app log viewer offers it as a filter.structured_log_go_routeruses the same one. - A level per event — or
nullto turn an event off. - A filter — leave noisy screens out of the log entirely.
Safety
- Tokens never reach the log — the values of
token,code,passwordand the rest of a configurable list are replaced withREDACTEDin path parameters, in the query and in an OAuth-style fragment. - Never breaks navigation — a filter or a logger that throws costs the entry, not the navigation.
- Costs nothing when off — an event that is off, a screen that is filtered out or a level no sink takes is never described.
Where it fits
The observer needs nothing but
structured_log — no server. 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 navigation 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
Works with auto_route 10 and 11. For go_router use
structured_log_go_router.
Published on pub.dev as a pre-release:
dependencies:
auto_route: ">=10.0.0 <12.0.0"
structured_log: ^0.3.0
structured_log_auto_route: ^0.1.0-dev.1
Quick Start
import 'package:flutter/material.dart';
import 'package:structured_log/structured_log.dart';
import 'package:structured_log_auto_route/structured_log_auto_route.dart';
void main() {
StructlogConfiguration.configure(
sinks: [LogSink(name: 'console', output: coloredConsoleOutput)],
);
final appRouter = AppRouter();
runApp(
MaterialApp.router(
routerConfig: appRouter.config(
navigatorObservers: () => [StructuredLogAutoRouteObserver()],
),
),
);
}
navigatorObservers is a builder: auto_route asks it for the observers of
every navigator it creates, nested ones included, so return a new observer
or share one — the observer keeps no state, so either is fine.
Opening a user screen with a token in the query, replacing it with settings and going back logs:
[2026-10-19T07:11:40.802293Z] INFO: route_pushed {"logger":"router","category":"navigation","route":"User","route_path":"/user/:id","location":"/user/42?token=REDACTED&tab=posts","previous_route":"Home","previous_location":"/"}
[2026-10-19T07:11:40.904616Z] INFO: route_pushed {"logger":"router","category":"navigation","route":"Settings","route_path":"/settings","location":"/settings","previous_route":"User","previous_location":"/user/42"}
[2026-10-19T07:11:40.905216Z] INFO: route_removed {"logger":"router","category":"navigation","route":"User","route_path":"/user/:id","location":"/user/42?token=REDACTED&tab=posts","previous_route":"Home","previous_location":"/"}
See example/main.dart for a runnable version.
What gets logged
Every entry carries category (navigation by default), logger (router
by default) and the screen it is about:
route— the route's name, such asUser;route_path— its path pattern, such as/user/:id;location— where it actually is, such as/user/42?tab=posts, with sensitive values redacted. Nested routes are located below their parents.
| Event | When | Default level | Also carries |
|---|---|---|---|
route_pushed |
a screen was opened | info |
previous_route, previous_location |
route_popped |
a screen was closed | info |
the screen that shows again, as previous_* |
route_removed |
a screen left the stack without a pop | info |
previous_* |
route_replaced |
the navigator reported a replacement itself | info |
the old screen, as previous_* |
route_tab_changed |
a tab of a TabsRouter was activated or switched to |
info |
tab_index, tab_event (init or change), previous_route |
previous_location is a path only, without query or fragment. auto_route
keeps the query of the navigation in progress on the router, not on each
route, so a neighbour's own query cannot be told from the one that is moving.
A replace is not route_replaced: the stack is page-based, and the
navigator reports a push of the new screen and a removal of the old one. You
will see route_pushed followed by route_removed. route_replaced is
there for a version that reports it.
A tab entry names the tab by its own path (/feed), not below its shell: the
tab hooks do not carry the shell. Whether a first visit to a second tab is
init or change is auto_route's call and differs between 10 and 11.
A screen opened outside the router — a dialog, a bottom sheet — is logged by
its settings name or its type, and has no route_path or location. The
filter does not see it.
What is not logged
An observer sees what the Navigator sees. Three things never reach it:
- A route updated in place.
navigatePath('/user/9')while/user/7is open keeps the same screen and changes its parameters; the navigator is not told, so there is no entry. - A path no route matches.
auto_routereports it to theonFailureof that one call, nowhere global. - What a guard decides.
AutoRouteGuardruns before the navigator is involved.
Keeping secrets out of the log
The values of these names are replaced with REDACTED, compared without
regard to case, wherever they appear — a path parameter, a query parameter, a
fragment written as a query (#access_token=...):
access_token, refresh_token, id_token, token, api_key, apikey,
password, client_secret, code.
The pattern in route_path carries no values and is logged as it is. Replace
the list with redactedParameters:
StructuredLogAutoRouteObserver(
redactedParameters: {...defaultRedactedParameters, 'session'},
)
Names must be lower case. Nothing else about a route is logged: extra,
args and the page's widgets are never read.
Configuration
StructuredLogAutoRouteObserver(
// Which events are logged, and at what level; null turns one off.
levels: const RouteLogLevels(
pushed: LogLevel.debug,
popped: null,
),
// Leave screens out. A route it rejects is not logged, and does not
// appear as the neighbour of another.
filter: (route) => route.name != 'Splash',
category: 'screens',
)
API Reference
| Symbol | Description |
|---|---|
StructuredLogAutoRouteObserver({logger, loggerName, category, levels, redactedParameters, filter}) |
The observer, an AutoRouterObserver. Without logger, it calls getLogger(loggerName) on every entry, so a later StructlogConfiguration.configure reaches it too. category: null binds no category. |
RouteLogLevels({pushed, popped, removed, replaced, tabChanged}) |
A LogLevel? per event; null turns the event off. |
defaultRedactedParameters |
The default set of names whose values are redacted. |
redactedValue |
'REDACTED'. |
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_riverpod—ProviderObserverforriverpod/flutter_riverpodstructured_log_dio—diointerceptor that logs HTTP callsstructured_log_http_client—package:httpclient wrapper that logs HTTP callsstructured_log_go_router— logsgo_routernavigationstructured_log_cherrypick— observer for thecherrypickDI containerstructured_log_drift— driftQueryInterceptorthat logs queries, failures and transactionsstructured_log_logging— bridge that routespackage:loggingrecords intostructured_log
License
MIT — see LICENSE.
Libraries
- structured_log_auto_route
- Logs what an
auto_routerouter does — pushes, pops, removals, tab changes — throughstructured_log.