structured_log_cherrypick

CI

Читать на русском.

See how your cherrypick dependency graph was actually put together — scopes, modules, cycles and resolve errors as structured log entries, without ever printing an instance.

Documentation: structured-log.openidealab.com.

Why

DI wiring fails opaquely. A binding that was never registered surfaces as a StateError far from the module that should have provided it; a scope that was never opened, or a module that was never installed, does not fail at all — the app just quietly gets something else, or nothing. Finding out means stepping through the container in a debugger.

StructuredLogCherryPickObserver lets the container tell you instead. Installed as cherrypick's observer, it writes every scope opened and closed, every set of modules installed, every cycle and every resolve error as a structured_log entry — so the wiring that did, and did not, happen is in the log next to everything else your app did. It never prints the instances themselves: a container holds configuration, API clients and token stores.

Features

Coverage

  • One line to install — CherryPick.setGlobalObserver(StructuredLogCherryPickObserver()), or hand it to a single scope.
  • The graph's shape — scopes opened and closed, modules installed and removed, instances disposed.
  • Wiring mistakes — dependency cycles with their chain, container warnings, resolve errors with a stack trace.
  • Resolution tracing on demand — registrations, requests, creations and cache hits and misses are off by default; turn any of them on when you need it.

Control

  • Quiet by default, loud when it matters — scopes, modules and disposals at debug; cycles and errors at error, warnings at warning.
  • A level per hook — or null to turn one off.
  • Its own category — every entry carries category: 'di', so a LogSink can route it separately and the in-app log viewer offers it as a filter.

Safety

  • Never prints an instance — only the name and type it is bound under; an error by its type, not its text.
  • Never breaks a resolve — a logger that throws costs the entry, not the container's work.
  • Works with cherrypick 3.x and 4.x — pure Dart, no Flutter needed.

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), 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, and the server and admin client of this project use it themselves; the whole project is at structured-log.openidealab.com.

Installation

Published on pub.dev as a pre-release:

dependencies:
  cherrypick: ">=3.0.0 <5.0.0"
  structured_log: ^0.3.0
  structured_log_cherrypick: ^0.1.0-dev.3

Quick Start

import 'package:cherrypick/cherrypick.dart';
import 'package:structured_log/structured_log.dart';
import 'package:structured_log_cherrypick/structured_log_cherrypick.dart';

void main() {
  StructlogConfiguration.configure(
    sinks: [LogSink(name: 'console', output: coloredConsoleOutput)],
  );
  // Before the first scope: a scope takes the global observer when it is
  // created.
  CherryPick.setGlobalObserver(StructuredLogCherryPickObserver());

  CherryPick.openRootScope().installModules([AppModule()]);
}
[2026-10-02T16:04:54.174431Z] DEBUG: di.scope_opened {"logger":"di","category":"di","scope":"scope_1790957094171_5885"}
[2026-10-02T16:04:54.178631Z] DEBUG: di.modules_installed {"logger":"di","category":"di","modules":["AppModule"],"scope":"scope_1790957094171_5885"}

To hear one scope only, hand the observer to it: Scope(null, observer: StructuredLogCherryPickObserver()).

example/main.dart opens and closes a feature scope and resolves a binding nobody registered (dart run example/main.dart).

What gets logged

Entry Default level Fields
di.scope_opened debug scope
di.scope_closed debug scope
di.modules_installed debug modules, scope
di.modules_removed debug modules, scope
di.instance_disposed debug type, name, scope
di.cycle_detected error chain, scope
di.warning warning message
di.error error message, error (its type), stack_trace
di.binding_registered, di.instance_requested, di.instance_created, di.cache_hit, di.cache_miss off type, name, scope
di.diagnostic off message

Every entry also carries category (di) and logger (di). scope is left out when the container does not name one.

What the container itself does and does not report — the same in 3.0 and 4.0-dev:

  • a scope is named by the id the container gives it (scope_1790957094171_5885), not the name it was opened under;
  • di.scope_closed comes only for a sub-scope closed through its parent (closeSubScope), not for the root scope;
  • onInstanceDisposed, onCacheHit and onCacheMiss are part of the interface but are never called; the observer handles them for the day they are;
  • a failed resolve is reported twice, once without the error object and once with it.

Keeping secrets out of the log

A container holds the app's configuration, API clients and token stores, and an object printed is whatever it holds printed. So:

  • an instance is never logged — not on creation, not on disposal — only the name and type it is bound under;
  • an error is logged by its type (StateError), not its text, and with its stack trace;
  • the details of a warning or a diagnostic are left out; only the container's message is kept.

Configuration

StructuredLogCherryPickObserver(
  // Trace every resolve; null turns a hook off.
  levels: const DiLogLevels(
    instanceRequested: LogLevel.trace,
    instanceCreated: LogLevel.trace,
    scopeOpened: null,
  ),
  logger: getLogger('server'),
  category: 'wiring',
);

It implements CherryPickObserver rather than extending SilentCherryPickObserver: the container skips calling an observer that is one, which would silence it too.

API Reference

Symbol Description
StructuredLogCherryPickObserver({logger, loggerName, category, levels}) The observer. Without logger it calls getLogger(loggerName) (di) on every entry, so a later StructlogConfiguration.configure reaches it too. category: null binds no category.
DiLogLevels({scopeOpened, scopeClosed, modulesInstalled, modulesRemoved, bindingRegistered, instanceRequested, instanceCreated, instanceDisposed, cacheHit, cacheMiss, cycleDetected, diagnostic, warning, error}) A LogLevel? per hook; null turns it off.

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

Shipping logs to a server

Integrations

License

MIT — see LICENSE.

Libraries

structured_log_cherrypick
Logs what the cherrypick DI container does through structured_log.