NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev
Structured JSONL logging designed to be read by an AI. Trace correlation, causal chains, error fingerprinting, secret redaction and a digest CLI. Zero dependencies.
Last release 2 months ago
27 Jul 2026
Too new to tell
only 1 dated releases
Nearly every release is documented
notes for 1 of 1 stable releases
Nothing withdrawn
no release was ever pulled
2 months old
1 releases · first in 2026
One column per month.
A release build now logs nothing unless you ask it to. enabled on Logger.create defaults to !isReleaseBuild instead of true, so flutter build / dart c
A release build now logs nothing unless you ask it to. enabled on
Logger.create defaults to !isReleaseBuild instead of true, so
flutter build / dart compile exe ship silent by default and only a
debug or profile build logs out of the box. Opt back in with
Logger.create(sink: ..., enabled: true) — a plain argument, so it works
the same in every mode — or with a runtime flag (enabled: userOptedIn)
for a diagnostics toggle in settings. Logger.forTesting() passes
enabled: true itself, so a test compiled AOT still logs.
The tradeoff is real and worth stating: the failures most worth analyzing
are the ones users hit in production, and a silent release cannot describe
them. The default is chosen for the case where nobody has decided yet —
writing to a user's device and printing to their console are both things
to opt into, not out of. When you can retrieve the log,
enabled: true with minimumLevel: byBuildMode(debug: LogLevel.trace, release: LogLevel.info) is the better configuration.
Verified against a real dart compile exe binary: default events=0,
enabled: true events=2, forTesting() events=2; the same four cases
under JIT give 2 / 2 / 2 with only enabled: false at 0.
Span has no end() — the README documented one that never existed. The
real methods are succeed() and fail(). tool/documented_api_check.dart
now references every documented API and is analyzed in CI, so an example
that drifts from the code fails the build.
fnv1a64 (returning int) is replaced by fnv1a64Hex (returning the
16-character hex string). A 64-bit value cannot be represented in a web
int, so no int-returning form can be correct everywhere this package
runs. shortHash also drops its unused seed parameter, whose default
was itself a web-breaking literal.
installDebugSync + ailog_sync: the log syncs itself while you debug.
Every other way to get a file off a device is something you do — adb pull, Xcode's container download, a share button, a tee pipeline with a
grep after it. This is a call at startup and a command on your machine,
after which a local .jsonl grows as you use the app:
final recent = MemorySink(capacity: 20000);
installDebugSync(recent);
dart run ailog:ailog_sync --vm-service <uri from flutter run> \
-o app.jsonl --watch
It registers a VM Service extension and the CLI pulls from the socket
flutter run already opened, asking each time for everything past the
last seq it holds — so it is a pull, not a push: nothing is dropped when
the app is busy, attaching late still gets the buffer's whole history, and
no prefix-stripping is needed because the events never touch a text
stream. Debug and profile only; a release build serves no VM Service, and
installDebugSync refuses to register there so the callback and the
buffer it closes over fold out of the binary.
If the buffer rolls over between polls the CLI says how many events were lost, rather than writing a file with a silent hole in it.
ailog_sync also reads stdin (flutter run | dart run ailog:ailog_sync),
which replaces the documented tee+grep recipe with one command, keeps
non-JSONL output flowing to your terminal, and rejects JSON that is not
ours so an app logging API responses cannot poison the file.
The VM Service client is ~130 lines on dart:io's WebSocket rather than
package:vm_service, so the package stays zero-dependency. Tested against
a real app process over a real socket — a fake client would exercise none
of the parts that actually break.
The digest now reports its own incompleteness. Size-based rotation
deletes older files by design, so a digest built from the survivors is a
partial view — measured: 100,000 events written, 63,686 reported, nothing
saying so. seq makes the loss exactly computable (lowest seq 36315 → 36,314
events missing), and it is now stated directly under the event count, in
both Markdown and JSON, with digest.missingEvents for programmatic use.
Gaps in the middle (a file not supplied) are counted too.
ailog_digest --format pretty — not a digest, a replay: re-renders a
recovered .jsonl file exactly the way ConsoleSink shows events live.
Closes the one gap in the human-readability story: the file itself is
machine-shaped by design, and there was no way to look at one with human
eyes short of reading raw JSON. Colour on a terminal, plain with -o;
interleaved non-JSON lines pass through untouched.
logger.interaction(name) — records what the user did. Defaults to
trace, so at a production level these stay out of the file while being
retained as breadcrumbs, which puts the user's path through the app
directly into the causal chain of whatever fails next. Takes an intent
name rather than a caption, so it survives copy changes and translation.
JsonlPrintSink. Prints the same wire format JsonlFileSink writes,
one line per event, through print. Exists for a real device with no
reachable filesystem path: flutter run already mirrors the app's print
output into your terminal live, so piping that session through tee and
extracting the JSON lines gets you a file ailog_digest can read, with no
adb pull or Xcode device menu involved. Composes safely with
capturePrints — no re-logging loop — verified by test.
Build-mode control. isDebugBuild / isProfileBuild /
isReleaseBuild / currentBuildMode are const, read from the
compiler-defined dart.vm.product and dart.vm.profile — the same values
Flutter's kReleaseMode is built on, with no Flutter dependency.
byBuildMode(debug:, profile:, release:) picks any value per mode.
Logger.create(enabled: false) and Logger.disabled() switch logging off:
measured at 5 ns per call in a release AOT build, ahead of all formatting,
sanitizing and breadcrumb work. Because the constants fold at compile time,
isReleaseBuild ? Logger.disabled() : Logger.create(sink: ...) lets the
AOT compiler drop the sink entirely — verified by compiling and confirming
the dead branch's strings are absent from the binary.
ConsoleSink.usingPrint() / ConsoleSink(write: ...).
benchmark/logging_benchmark.dart, so the README's performance numbers can
be re-run rather than believed.
Whole-log aggregates in the digest. Every message shape counted
(lease acquired ×40 vs lease released ×9), and min/max/last of every
numeric context field. Driven by a blind A/B test: a summarized digest
lost to the raw log on a connection-pool leak because the evidence — the
releases that never happened — lived in the successful requests, which
summarization had discarded. With the counts added, the digest found the
same root cause from a fifth of the bytes.
LogFilter / LogSelection: choose what is worth an AI's context
window — collapseRepeats, aroundErrors, onlyFailedTraces,
level/logger/time/count bounds. Aggregates are computed over the
unfiltered input, and both output formats state what was dropped.
String output. MemorySink.toJsonl()/toMarkdown()/export(),
LogSelection.toReport() (digest + surviving events),
buildDigest(events), digestFromJsonl(text). No filesystem — works on
web.
capturePrints: route ordinary print() calls into the structured
log (tagged print, ambient trace attached), with a re-entrancy guard so
a console sink cannot feed back into the log.
Digest honesty: breadcrumb entries are labeled as breadcrumbs, loggers
that appear only inside causal chains are called out, and a group's
context sample is labeled first of N (with the most recent shown when
it differs).
ids.dart held
0xcbf29ce484222325 as an integer literal, which is a hard dart2js
compile error ("can't be represented exactly in JavaScript") — so every
Flutter web or dart2js build failed outright. Nothing caught it: dart analyze and dart test both run on the VM, where a 64-bit int is
ordinary. FNV-1a is now computed over two 32-bit halves using arithmetic
rather than wide bitwise ops, which is exact on both platforms. Hashes are
byte-identical to before — verified against the previous output, the
canonical FNV-1a 64 vectors, a dart2js build run under Node, and the
Kotlin port compiled and executed for cross-language parity. CI now
compiles for web so this cannot regress.Logger.create crashed on dart2js under Node. Random.secure()
throws a raw JS ReferenceError, not the UnsupportedError the fallback
caught, so the deterministic-Random path written for exactly this case
never ran and construction took the program down with it.main.dart.js. The guard keyed on Flutter web's
default output name, so dart compile js -o app.js and any custom
bundler name reported → app.js:3881 StackTrace_current as the user's
code — confidently wrong, which is the one outcome the guard exists to
prevent. It now tests whether a frame resolves to a Dart source position
at all.redacted among the event keys, though it is a
convention applying to any field rather than a key — a reader could go
looking for an event field that never exists. It is now
_convention:redacted, and a test asserts every key toJson can emit is
documented (the previous test enumerated keys by hand and so could not
catch a newly added one).toString() throws crashed the host program. Passing a
domain object with a buggy override, an uninitialized late field, or a
throwing getter in context: — or throwing one — propagated straight out
of logger.info() / logger.error(). That is precisely the failure this
package promises never to cause. All value paths (context values, map
keys, past-depth-limit values, Uri, and the thrown error itself) now
contain it and record <toString() threw StateError>, naming the
exception rather than silently dropping the field.flush() never exited. JsonlFileSink's
default flushInterval schedules a periodic Timer, and only close()
cancels it — a live Timer keeps the isolate alive, so main() returning
after flush() alone left the process hanging until killed. Found by
running the README's own quick-start example verbatim. The class doc, the
Quick Start in both READMEs, and every example now call close(), and a
subprocess regression test (test/regression/) guards it going forward.includePlatformContext duplicated ~133 bytes on every line — OS,
Dart version, pid and locale, identical each time, in a format whose whole
premise is not wasting a context window. Measured: a 100-event file grew
73%, 182 → 315 bytes per line. JsonlFileSink now writes the platform
into each file's _hdr record once, and the option's documentation states
the per-event cost.JsonlFileSink could silently lose events. Repeated IOSink.flush()
on a handle from File.openWrite(), with writes arriving between flushes,
dropped data — measured at 9 of 15 events lost in an ordinary
request-handler-shaped loop. The sink was rewritten onto a synchronous
RandomAccessFile with an explicit buffer; durability no longer depends
on the event loop getting a turn. New: isHealthy, droppedEvents,
onError, bufferBytes, flushOnErrorLevel.package:ailog frames are now classified as noise, not application
frames — previously a digest's five-frame budget could be spent entirely
on this package's own zone plumbing, and the fingerprint could group
unrelated bugs logged through the same helper.Your coding agent can read these notes before it upgrades. Set up the MCP server →