ailog
Structured JSONL logging designed to be read by an AI. Trace correlation, causal chains, error fingerprinting, secret redaction and a digest CLI. Zero dependencies.
What this package is like to depend on
Last release 28 days 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
0 months old
1 releases · first in 2026
1 release in the last 12 months
see the full history below
Release timeline
1 releases · Jul 2026 to Jul 2026Releases
latest 1-
0.4.027 Jul 2026Release notes
Open source →Changed
-
A release build now logs nothing unless you ask it to.
enabledonLogger.createdefaults to!isReleaseBuildinstead oftrue, soflutter build/dart compile exeship silent by default and only a debug or profile build logs out of the box. Opt back in withLogger.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()passesenabled: trueitself, 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: truewithminimumLevel: byBuildMode(debug: LogLevel.trace, release: LogLevel.info)is the better configuration.Verified against a real
dart compile exebinary: defaultevents=0,enabled: trueevents=2,forTesting()events=2; the same four cases under JIT give 2 / 2 / 2 with onlyenabled: falseat 0. -
Spanhas noend()— the README documented one that never existed. The real methods aresucceed()andfail().tool/documented_api_check.dartnow references every documented API and is analyzed in CI, so an example that drifts from the code fails the build. -
fnv1a64(returningint) is replaced byfnv1a64Hex(returning the 16-character hex string). A 64-bit value cannot be represented in a webint, so noint-returning form can be correct everywhere this package runs.shortHashalso drops its unusedseedparameter, whose default was itself a web-breaking literal.
Added
-
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, ateepipeline with agrepafter it. This is a call at startup and a command on your machine, after which a local.jsonlgrows 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 --watchIt registers a VM Service extension and the CLI pulls from the socket
flutter runalready opened, asking each time for everything past the lastseqit 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, andinstallDebugSyncrefuses 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_syncalso reads stdin (flutter run | dart run ailog:ailog_sync), which replaces the documentedtee+greprecipe 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'sWebSocketrather thanpackage: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.
seqmakes 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, withdigest.missingEventsfor 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.jsonlfile exactly the wayConsoleSinkshows 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 totrace, 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 formatJsonlFileSinkwrites, one line per event, throughprint. Exists for a real device with no reachable filesystem path:flutter runalready mirrors the app's print output into your terminal live, so piping that session throughteeand extracting the JSON lines gets you a fileailog_digestcan read, with noadb pullor Xcode device menu involved. Composes safely withcapturePrints— no re-logging loop — verified by test. -
Build-mode control.
isDebugBuild/isProfileBuild/isReleaseBuild/currentBuildModeareconst, read from the compiler-defineddart.vm.productanddart.vm.profile— the same values Flutter'skReleaseModeis built on, with no Flutter dependency.byBuildMode(debug:, profile:, release:)picks any value per mode.Logger.create(enabled: false)andLogger.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 ×40vslease 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 ordinaryprint()calls into the structured log (taggedprint, 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).
Fixed
- The package did not compile for web at all.
ids.dartheld0xcbf29ce484222325as 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 analyzeanddart testboth 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.createcrashed on dart2js under Node.Random.secure()throws a raw JSReferenceError, not theUnsupportedErrorthe fallback caught, so the deterministic-Randompath written for exactly this case never ran and construction took the program down with it.- Checkpoints named the dart2js runtime as the caller whenever the
bundle wasn't called
main.dart.js. The guard keyed on Flutter web's default output name, sodart compile js -o app.jsand any custom bundler name reported→ app.js:3881 StackTrace_currentas 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. - The schema legend listed
redactedamong 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 keytoJsoncan emit is documented (the previous test enumerated keys by hand and so could not catch a newly added one). - A value whose
toString()throws crashed the host program. Passing a domain object with a buggy override, an uninitializedlatefield, or a throwing getter incontext:— or throwing one — propagated straight out oflogger.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. - A script that only called
flush()never exited.JsonlFileSink's defaultflushIntervalschedules a periodicTimer, and onlyclose()cancels it — a liveTimerkeeps the isolate alive, somain()returning afterflush()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 callclose(), and a subprocess regression test (test/regression/) guards it going forward. includePlatformContextduplicated ~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.JsonlFileSinknow writes the platform into each file's_hdrrecord once, and the option's documentation states the per-event cost.JsonlFileSinkcould silently lose events. RepeatedIOSink.flush()on a handle fromFile.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 synchronousRandomAccessFilewith an explicit buffer; durability no longer depends on the event loop getting a turn. New:isHealthy,droppedEvents,onError,bufferBytes,flushOnErrorLevel.package:ailogframes 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.- Digest timestamps rendered in local time while the JSONL is UTC.
-