structured_log_bloc
See what every bloc and cubit in your app did — each event, state change and error as a structured log entry — with one line of setup.
Documentation: structured-log.openidealab.com.
“Why did the screen show that?” is the question a bloc app keeps asking,
and answering it means reconstructing what happened: which event arrived,
what state it led to, which handler threw. Without a record you add print
calls, reproduce the bug, and remove them again — and on a user’s device you
can’t do even that.
StructuredLogBlocObserver keeps the record for you. Installed once as
Bloc.observer, it writes the creation, events, transitions, errors and
closing of every bloc and cubit as structured_log entries — with the
bloc’s type, its instance and the values involved as separate fields, so you
can filter the story of one bloc out of everything else.
Features
Section titled “Features”Coverage
Section titled “Coverage”- One line to install —
Bloc.observer = StructuredLogBlocObserver(). - Every hook — creation, events, transitions, cubit changes, errors with stack traces, handler completion, closing.
- Each state change once — a bloc’s change is logged as a transition (with its event), a cubit’s as a change; never both.
- Instances told apart —
bloc_instanceseparates two blocs of the same type. - Works with
flutter_blocas is — depends onpackage:bloconly, whichflutter_blocis built on, so it also runs in pure Dart.
Control
Section titled “Control”- Its own category — every entry carries
category: 'bloc', so aLogSinkcan route bloc traffic separately and the in-app log viewer offers it as a filter. - A level per hook — or
nullto turn a hook off. - A filter — leave noisy blocs out of the log entirely.
- Values under your control — states and events 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 bloc — a
toString()or describer that throws costs the entry its values, and a filter that throws costs the entry, but neither costs the bloc itsemit. - Costs nothing when off — a hook that is off or a bloc that is filtered out never has its state 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 bloc 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: flutter_bloc: ^9.0.0 # or bloc: ^9.0.0 structured_log: ^0.3.0 structured_log_bloc: ^0.1.0-dev.3Quick Start
Section titled “Quick Start”import 'package:flutter/widgets.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:structured_log/structured_log.dart';import 'package:structured_log_bloc/structured_log_bloc.dart';
void main() { StructlogConfiguration.configure( sinks: [LogSink(name: 'console', output: coloredConsoleOutput)], ); Bloc.observer = StructuredLogBlocObserver();
runApp(const MyApp());}A CounterBloc receiving one Incremented event then logs:
[2026-10-02T16:04:41.519257Z] DEBUG: bloc_created {"logger":"bloc","category":"bloc","bloc":"CounterBloc","bloc_instance":787006492,"state":"0","state_type":"int"}[2026-10-02T16:04:41.527204Z] DEBUG: bloc_event_added {"logger":"bloc","category":"bloc","bloc":"CounterBloc","bloc_instance":787006492,"bloc_event":"Incremented()","bloc_event_type":"Incremented"}[2026-10-02T16:04:41.531119Z] DEBUG: bloc_transition {"logger":"bloc","category":"bloc","bloc":"CounterBloc","bloc_instance":787006492,"bloc_event":"Incremented()","bloc_event_type":"Incremented","current_state":"0","next_state":"1","state_type":"int"}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 (bloc by default), logger (bloc by
default), bloc — the bloc’s type — and bloc_instance, its identity, which
tells apart two instances of the same type.
| Hook | event |
Default level | Fields |
|---|---|---|---|
onCreate |
bloc_created |
debug |
state, state_type |
onEvent |
bloc_event_added |
debug |
bloc_event, bloc_event_type |
onChange |
bloc_change |
debug |
current_state, next_state, state_type — cubits only |
onTransition |
bloc_transition |
debug |
bloc_event, bloc_event_type, current_state, next_state, state_type |
onError |
bloc_error |
error |
error, error_type, stack_trace |
onDone |
bloc_event_done |
trace |
bloc_event, bloc_event_type; error, error_type if the handler failed |
onClose |
bloc_closed |
debug |
— |
The bloc’s event sits under bloc_event, not event: in structured_log
event is the entry’s own name.
bloc_event_done is at trace, which a sink’s default minLevel (debug)
filters out — it repeats the event of every handler, which is rarely worth
the volume. Lower a sink’s minLevel to see it.
Keeping secrets out of the log
Section titled “Keeping secrets out of the log”States and events 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:
Bloc.observer = StructuredLogBlocObserver( describe: (value) => switch (value) { AuthState() || LoginSubmitted() => null, _ => describeBlocValue(value), },);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]);
Bloc.observer = StructuredLogBlocObserver( describe: (value) => switch (value) { AuthState(:final user, :final token) => {'user': user, 'token': token}, _ => describeBlocValue(value), },);// next_state: {"user": "u", "token": "***"}Configuration
Section titled “Configuration”Bloc.observer = StructuredLogBlocObserver( // Which hooks are logged, and at what level; null turns one off. levels: const BlocLogLevels( event: LogLevel.trace, transition: LogLevel.info, done: null, ), // Leave noisy blocs out. filter: (bloc) => bloc is! TickerCubit, category: 'state',);Several observers at once — this one and, say, a crash reporter — combine
through bloc’s own MultiBlocObserver (bloc 9.2.0 or later).
API Reference
Section titled “API Reference”| Symbol | Description |
|---|---|
StructuredLogBlocObserver({logger, loggerName, category, levels, describe, filter}) |
The observer. Without logger, it calls getLogger(loggerName) on every hook, so a later StructlogConfiguration.configure reaches it too. category: null binds no category. |
BlocLogLevels({create, event, change, transition, error, done, close}) |
A LogLevel? per hook; null turns the hook off. |
BlocValueDescriber |
Object? Function(Object? value) — turns a state, event or error into an entry value; null omits the field. |
describeBlocValue(value) |
The default describer: toString(), cut to defaultBlocValueMaxLength (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_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_logstructured_log_riverpod—ProviderObserverforriverpod/flutter_riverpod
License
Section titled “License”MIT — see LICENSE.