structured_log_riverpod
See what every Riverpod provider in your app did — created, updated, failed, removed — as structured log entries, with one line of setup.
Documentation: structured-log.openidealab.com.
“Why is this screen empty?” is a question a Riverpod app keeps asking, and
the answer is usually a provider that failed, or one that never got the value
you expected. Without a record you reproduce the bug under a debugger or add
a ProviderObserver that prints — and on a user’s device you can do
neither.
StructuredLogRiverpodObserver keeps the record for you. Passed to the
container once, it writes the creation, updates, failures and removal of every
provider as structured_log entries — with the provider’s name, its family
argument and the values involved as separate fields, so you can filter the
story of one provider out of everything else.
Features
Section titled “Features”Coverage
Section titled “Coverage”- One line to install —
ProviderScope(observers: [StructuredLogRiverpodObserver()]). - The whole life of a provider — creation, updates, failures with stack traces, removal.
- Providers told apart — by
name, or by type when there is none; a family’s argument is a field of its own. - Each failure once — Riverpod reports a failing future twice, as an
update to
AsyncErrorand as a failure; the observer logs the failure and skips the update. - Works with
flutter_riverpodandhooks_riverpodas is — depends onpackage:riverpodonly, which both are built on, so it also runs in pure Dart.
Control
Section titled “Control”- Its own category — every entry carries
category: 'riverpod', so aLogSinkcan route provider traffic separately and the in-app log viewer offers it as a filter. - A level per hook — or
nullto turn a hook off, usually the updates. - A filter — leave noisy providers out of the log entirely.
- Values under your control — values and family arguments go through a
describer you can replace, to keep secrets out of the log or hand
redactKeysa map.
Safety
Section titled “Safety”- Never breaks a provider — a
toString()or describer that throws costs the entry its values, and a filter that throws costs the entry, but neither reaches the code that changed the state. - Costs nothing when off — a hook that is off, a provider that is filtered out or a level no sink takes never has its value described.
Where it fits
Section titled “Where it fits”The observer 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 riverpod 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”Riverpod 3 only. The observer hooks of Riverpod 2.x have other signatures, and one class cannot override both; for 2.x this package does not work.
Published on pub.dev as a pre-release:
dependencies: flutter_riverpod: ^3.0.0 # or riverpod: ^3.0.0 structured_log: ^0.3.0 structured_log_riverpod: ^0.1.0-dev.1Quick Start
Section titled “Quick Start”import 'package:flutter/widgets.dart';import 'package:flutter_riverpod/flutter_riverpod.dart';import 'package:structured_log/structured_log.dart';import 'package:structured_log_riverpod/structured_log_riverpod.dart';
void main() { StructlogConfiguration.configure( sinks: [LogSink(name: 'console', output: coloredConsoleOutput)], );
runApp( ProviderScope( observers: [StructuredLogRiverpodObserver()], child: const MyApp(), ), );}In pure Dart pass the same observer to the container:
ProviderContainer(observers: [StructuredLogRiverpodObserver()]).
A named counter provider that is read and then set to 1 logs:
[2026-10-13T22:27:13.862226Z] DEBUG: provider_added {"logger":"riverpod","category":"riverpod","provider":"counter","value":"0","value_type":"int"}[2026-10-13T22:27:13.875054Z] DEBUG: provider_updated {"logger":"riverpod","category":"riverpod","provider":"counter","previous_value":"0","value":"1","value_type":"int"}A failing FutureProvider named profile logs once, as an error:
[2026-10-13T22:27:13.884681Z] ERROR: provider_failed {"logger":"riverpod","category":"riverpod","provider":"profile","error":"Bad state: offline","error_type":"StateError","stack_trace":"#0 ..."}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 (riverpod by default), logger (riverpod
by default) and provider: the provider’s name, or its type when it has no
name. For a family, provider_argument holds the argument.
| Hook | event |
Default level | Fields |
|---|---|---|---|
didAddProvider |
provider_added |
debug |
value, value_type |
didUpdateProvider |
provider_updated |
debug |
previous_value, value, value_type |
providerDidFail |
provider_failed |
error |
error, error_type, stack_trace |
didUnmountProvider |
provider_disposed |
debug |
— |
A provider that throws while building is logged as provider_added with no
value and value_type: Null, then as provider_failed.
provider_disposed is written when a provider is removed from memory, for
example an autoDispose provider that lost its last listener. A provider that
merely rebuilds — a dependency changed, refresh, invalidate — is not
disposed, and writes no entry of that name. The hook behind it,
didUnmountProvider, exists from riverpod 3.3.2: on 3.0.0–3.2.1 this
entry is not written at all, and nothing tells you so. The other three
entries are the same on every 3.x. If you need provider_disposed, depend on
riverpod: ^3.3.2.
Mutations (mutationStart and friends, new in Riverpod 3) are not logged:
their API is marked experimental.
Keeping secrets out of the log
Section titled “Keeping secrets out of the log”Values and family arguments are logged through toString() by default
(cut to 1000 characters). If they can carry passwords, tokens or personal
data — and especially if the log leaves the device through
structured_log_remote_sync — pass a describe that withholds them.
Returning null drops the value and keeps its type:
StructuredLogRiverpodObserver( describe: (value) => switch (value) { Session() || Credentials() => null, _ => describeRiverpodValue(value), },)The same describe sees the family argument, which is often a user id or a
token, so one function covers both. To log types only, everywhere:
describe: (_) => null.
A describe can also return a map instead of a string. A map stays a
structure in the entry, so a redactKeys processor reaches its fields, as it
reaches any other field. A string from toString() is opaque to it:
StructlogConfiguration.configure(processors: [redactKeys(), dropNullValues]);
StructuredLogRiverpodObserver( describe: (value) => switch (value) { Session(:final user, :final token) => {'user': user, 'token': token}, _ => describeRiverpodValue(value), },)// value: {"user": "u", "token": "***"}The error of provider_failed is not run through describe: it is written
by structured_log itself, as error, error_type and stack_trace.
Configuration
Section titled “Configuration”StructuredLogRiverpodObserver( // Which hooks are logged, and at what level; null turns one off. levels: const RiverpodLogLevels( added: LogLevel.trace, updated: null, // a provider bound to a text field updates all the time ), // Leave noisy providers out. filter: (provider) => provider.name != 'ticker', category: 'state',)API Reference
Section titled “API Reference”| Symbol | Description |
|---|---|
StructuredLogRiverpodObserver({logger, loggerName, category, levels, describe, filter}) |
The observer, a final class extending ProviderObserver. Without logger, it calls getLogger(loggerName) on every hook, so a later StructlogConfiguration.configure reaches it too. category: null binds no category. |
RiverpodLogLevels({added, updated, failed, disposed}) |
A LogLevel? per hook; null turns the hook off. |
RiverpodValueDescriber |
Object? Function(Object? value) — turns a value or a family argument into an entry value; null omits the field. |
describeRiverpodValue(value) |
The default describer: toString(), cut to defaultRiverpodValueMaxLength (1000) characters; a throwing toString() becomes a placeholder. |
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_logging— bridge that routespackage:loggingrecords intostructured_log
License
Section titled “License”MIT — see LICENSE.