telemetry 0.3.5
telemetry: ^0.3.5 copied to clipboard
One event model for every telemetry channel (console, journal, crash reporting, toasts, analytics), with sinks the application supplies.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.3.5 - 2026-09-05 #
An architectural review of the whole package: module boundaries, the public surface, the OTel mapping, and a hunt for failures under re-entrancy, zones and the asynchronous fan-out. The shape held; these are the defects it found.
Changed #
LogEvent.areaandsiteare empty for a body with no|. They used to answer with the whole line, so a bridged record or a capturedprintbecame its own crash-reporter category, one per message. A body without a separator carries no subsystem to name.ReportThrottletakes its clock in the constructor,ReportThrottle({Duration Function()? clock}), instead of anow:argument per call. Mixing the two scales was anassert, and in release it produced negative durations that suppressed every report for the life of the process.LogEvent.attributesis unmodifiable. It is the map sinks store, on an immutable record, and one sink could rewrite what the next one wrote.- A local
LogEvent.timestampis normalised to UTC rather than asserted. The assert said nothing in release, and a bridge composing its own record is the one caller that can pass local time. LogBuffer(limit: 0)keeps nothing, the waytraceLimit: 0already did. It used to reachremoveFirston an empty ring and throw out of every log call in release.
Fixed #
- A sink whose
enabledthrows no longer takes the failure to the call site oflog.i(...)and no longer skips the sinks after it.Telemetry.isEnabledtreats a sink that cannot answer as one that does not want the event. - A channel action named after the fan-out microtask ran, on a draft held across an
await, was recorded and never fired. The fan-out re-arms, and the second pass reuses the event. - The two rings of
LogBuffermerge by sequence and then by timestamp, so hand-built records left at the default sequence no longer sort ahead of everything. - The console sink looks its delegate up rather than calling
putIfAbsent, which allocated a closure per line for a map that holds one entry. wrapForPrintis@visibleForTesting: it was public only because the tests needed it.
Docs #
- The re-entrancy cap covers synchronous re-entry only: an
eventslistener or a channel action that logs comes back a microtask later, at depth zero, and nothing bounds that. - The buffer hand-over is synchronous: read
events,markDrained, register the sink, then write. ReportingSink.reportis not throttled;capturespends the identity before the vendor call, so a call that throws costs one dedupe window.- A sink removed during a dispatch still receives that event.
- An exporter reads
name,metaandresourcerather thanattributes, and writesTraceFlagsas zero. README gains the two-registration snippet for a reporter.
0.3.3 - 2026-09-05 #
Changed #
- The browser console gets one plain string per level, the way
package:lwrites it, with the level choosingconsole.debug,info,warnorerror. 0.3.0 translated the ANSI escapes into the console's%cstyling, which only Chrome DevTools understands: the debug proxy that carries a browser console call to an IDE forwards the first argument and drops the rest, so the VS Code debug console printed lines like%c14:39:02%c %cI%c load | ready. An escape survives that trip and is rendered by Chrome's console and by the debug console alike; Firefox and Safari show it as text, which is whatprintColors: falseis for.dart:js_interop_unsafeis no longer used, so the console stops attributing every line to the interop patch.
0.3.1 - 2026-09-05 #
A review round against the OpenTelemetry logs spec, the Dart and Flutter sources, and the practice
of slog, Serilog, tracing, tint, zerolog and package:l. It corrects statements that were
not true, widens the severity range to the one the spec defines, and removes the last place where
the package guessed something only the application knows.
Changed #
printColorsreaches every destination. The sink used to turn colours off forLogOutput.developerand forprinton the web, which is the same guess that 0.3.0 removed for terminals: Chrome renders escapes inconsole.log, and nothing here can know what reads thedart:developerstream. PairLogOutput.developerwithprintColors: false, since the DevTools Logging view stores the escapes in the message.LogLevel.fromValuereads the whole 1 to 24 range, four numbers per level, so a row written by another exporter or fromLogEvent.severityNumbercomes back as the level it was. It used to match the six lower bounds only, and readTRACE2asinfo.LogBuffer.eventsreturns aListsnapshot in every case. It handed out the live queue when one ring was empty, so a sink that logged while a journal drained could break the iteration.
Added #
LogEvent.severityNumber: the number to store or export.LogLevel.severityNumberfor every level buttrace, which spends the four numbers of its range on the verbosity tiers, as the spec asks of a source with several severities in one range.LogEvent.site:Area | operation, the body without the segment a call site writes freely. What a breadcrumb or a category wants when the message may carry a user-authored label.Telemetry.sinks: the registered sinks, unmodifiable. Both applications kept their own bookkeeping to answer whether the sink they built was still the live one.
Fixed #
package:lis MIT, Copyright (c) 2023 Matiunin Mikhail, not WTFPL. The notice now sits with the code carried over, inlib/src/console/ansi.dartandlib/src/zone.dart.debugPrintThrottledpaces 12K characters per second, not 12 KB.dart:developer'slog()is a no-op under dart2js and dart2wasm alike; they share one patch.ReportingSinkdocuments two floors, not three.LogEvent.namesaidReportThrottlereads a copy edit as a new failure. The throttle is per-process memory; what a name does is group lines that say the same thing in different words, and pin the crash reporter's fingerprint.errorandfatalare no longer documented as "auto-reported": the line is the reporting sink's capture floor, which is that level by default.
0.3.0 - 2026-09-05 #
A correction release: 0.2.x got the shape right and several details wrong. Everything here came out
of reviewing it against the OpenTelemetry logs data model, log/slog, tracing, Serilog and
Microsoft.Extensions.Logging, Sentry's structured logs and package:l.
Changed #
- The console line has one colour and two dims. The level tag is coloured, the time and the
attribute keys are faint (
ESC[2m), and the body, the values and the error stay plain. That is the layout oftint,zerolog's console writer andcharmbracelet/log. Dim follows colour. Telemetry.resourceis a field on the record, not a copy in every event'smeta. OpenTelemetry keepsResourceapart from recordAttributesbecause it does not vary per occurrence.LogEvent.resourceholds the launch map by reference,metais the scope and the call site, andevent.attributesis the flat projection a sink stores. A console line no longer repeats the app version. A sink that readsevent.metadirectly and expected the launch attributes there must readevent.attributes.LogDraft.escalate()always forwards; the sink decides. It used to swallow an escalation of an event aterroror above, guessing the reporting sink had captured it. That isReportingSink.captureLevel's decision, and with a higher floor the request reached the reporter zero times.ReportThrottle, shared by both paths under one identity, makes a second send free without spending a per-minute slot.ReportingSink.enabledconsults both floors. It gated onbreadcrumbLevelalone, socaptureLevelcould only narrow what the trail admitted and a quiet-trail-loud-capture reporter captured nothing.handlenow applies each floor separately.- The unused-draft guard is the analyzer. Every builder (
meta,cause,description,name,verbosity) andTelemetry.callare@useResult, so a dropped draft is anunused_resultwhere it is written. The runtime guard is gone: it could not tell a draft held across anawaitfrom a forgotten one, and reported the difference a microtask later, into the pipeline. - The body convention is checked at the call site, synchronously and whatever the level, rather
than at snapshot. For a channel-only draft that was inside a microtask, and it was skipped
entirely when nothing consumed the level.
name, analytics names and trace tiers are checked too, and every check is a plainassertrather than anArgumentError. - A channel that throws is isolated, reported once to the root zone like a failing sink. A toast whose messenger had gone away used to escape as an uncaught error, which the app's own handler filed as a defect, and cancel the channels after it.
toast()resolves its text at the fan-out, so a description set after it is still the one the user sees, and a toast with no text at all falls back to the body with a diagnostic rather than asserting from a microtask.Telemetry.eventsis an observer. A listener no longer makes every level and tier "enabled", which had quietly undoneLogBuffer.maxVerbosityfor any app with a debug overlay.LogBufferkeeps two rings,limitforminLeveland up and the newtraceLimit(default- for
trace, merged bysequencewhen read. Onev1per frame used to evict the boot from a shared ring before the journal could drain it.traceLimit: 0refuses trace outright, whichmaxVerbositynever could.
- for
LogOutput.developerreaches the browser console on the web, wheredart:developer'slog()is a no-op and every line was being dropped.printColorsis honoured as written. 0.2 suppressed colours wherever it guessed the destination could not render them, and the guess said no for every Flutter app on a desktop, which has no terminal of its own. Now the app decides, the waypackage:ldoes it; a CLI passesstdout.supportsAnsiEscapes. Only DevTools andprinton the web, which store the escapes as text, stay plain.- Console values are escaped, not just quoted. A carriage return, a tab, a backslash and every other control character are escaped; an ESC in a value can no longer drive the terminal. The body and the error text get the same treatment.
printoutput wraps at 1000 code units, never through a surrogate pair. The old 800 citeddebugPrintThrottled, which paces 12K characters per second and does not wrap unless asked; the real limit is Android's ~4 KB per call.removeSinkandflushcompare by identity, so two sinks that happen to be equal are still two destinations.ReportThrottleprunes its dedupe map before the ceiling check rather than after, so it prunes during the storm it exists to survive; it asserts that one instance measures on one clock.- The print capture re-enters the zone
printwas called in, so a captured line keeps the scope it was printed in. - A lazy body may return any
Object, not only aString. track/notifycopy their payload at the call rather than reading it at the fan-out.Telemetry.clockmay answer in local time:now()normalises to UTC.
Added #
TelemetryOptions.levelTag: the text that says the level, in its level's colour where the destination renders colour.LevelTag.bracketed([I], the default),.letter(I),.word(INFO),.glyph(💡), or a map of the app's own.TelemetryOptions.icon: the subsystem's glyph after the level tag.AreaIcons({'Boot': '🏗', ...})looks it up by area and drops the word it already says:I 🪢 lifecycle | disposed. The console only: the body a journal and a crash reporter are given is unchanged.kAttributeKey,kEventNameandkTrackNameare exported, so an application's source-scanning test can assert the rule the runtime asserts rather than a copy of it that drifts.LogEvent.resourceandcopyWith(resource:);LogEvent.attributesis computed once and cached.LogBuffer.traceLimit.- The browser console shows the colours:
JsConsoleDelegateturns the escapes into%cstyling. PrintConsoleDelegate,IgnoreConsoleDelegate,DeveloperConsoleDelegate,wrapForPrintandkPrintWrapWidthare exported. The package's own tests had to reach intosrc/for them.make bench, a compiled micro-benchmark of the hot paths, and the numbers in the README.test/analyzer_guard_test.dart: runs the analyzer over a fixture to proveunused_resultfires on a dropped builder and not on a closed draft;dart_test.yamldeclares theanalyzertag it runs under, and.pubignorekeepstool/and that fixture out of the published archive.
Removed #
LogDraft.sentry(), deprecated in 0.2.0.- The runtime unused-draft report and
Telemetry.guardUnused.
Fixed #
- The disabled fluent path read the clock before the gate.
- The quick path asked every sink twice whether it wanted a level.
- A builder called after the log action was silently ignored; it now asserts, and a second log action returns the first event instead of emitting a second one in release.
traceContextwas read at snapshot, so a channel-only draft got the trace of a microtask later, when the span it belonged to may have ended.- Every draft allocated a channel list it usually never used.
0.2.1 - 2026-09-05 #
Changed #
Telemetry.nextSequence()is public. A bridge that composes its ownLogEventforemitcould not number it, and an event left at the default zero sorts ahead of everything sharing its timestamp.
0.2.0 - 2026-09-05 #
Added #
Telemetry.scoped(attributes, body): zone-carried attributes on every event logged inside it, across awaits.currentTelemetryContext()reads them; an inner scope wins over an outer one and a call site's own.metawins over both.Telemetry.resource: the attributes that identify the launch (app.version,app.environment), merged under the scope and the call site.LogEvent.nameandLogDraft.name(...): OpenTelemetry'sEventName, a stable identity independent of the body. Surfaced asevent.nameinattributes;ReportThrottlekeys on it when it is set.LogEvent.traceId/LogEvent.spanId, filled fromTelemetry.traceContext.LogEvent.sequence, a per-launch counter, andTelemetry.clock, so a test can pin time.LogEvent.copyWith, for a bridge that adopts a foreign record.Telemetry.emit(LogEvent): the OpenTelemetry bridge API'sEmit, for a record this pipeline did not compose.Telemetry.dispatchis gone; it was internal.FlushableandTelemetry.flush(), the SDK'sForceFlush.close()flushes first.ReportingSink: the crash-reporting policy: breadcrumb floor, throttled capture of failures, escalation as a structured log - withbreadcrumbLevelandcaptureLeveland three vendor hooks.Telemetry.stackTraceAtLevel,package:logging'srecordStackTraceAtLevel.Telemetry.strict: debug-only checks that an attribute key is OpenTelemetry-named and a body carries at leastArea | operation.- An attribute value may be an
Object Function(), resolved once, only if the event is built. LogBuffer.maxVerbosity, the ceiling on the trace tiers the ring keeps.TelemetryOptions.showMillis,developerName,copyWith, andrenders(..., release:).ConsoleSink(format:)withConsoleSink.renderas the built-in renderer;ConsoleSink.optionsis settable, so a dev menu can raise the floor at runtime.LogLevelgained>and<=.currentTelemetryOptions,currentTelemetryContext,runTelemetryScopeand the zone keys are exported;runTelemetry'sonPrintis optional.make test-webandmake compile-check(the example, compiled to JS and to Wasm).
Changed #
LogDraft.sentry()is nowescalate(); the old name remains as a deprecated alias until 0.3.0.- A draft that names no channel is reported through the pipeline as
Telemetry | draft | unusedatwarn, in debug builds, instead of throwing anAssertionErrorfrom a bare microtask - which arrived as an uncaught zone error and became a defect-severity line. One microtask per burst now, not one per draft. - Console colours are suppressed where the destination cannot render them (browser console,
DevTools,
NO_COLOR,TERM=dumb). - Console attribute values containing whitespace,
=or a quote are quoted. - The
printdestination splits output at 800 characters and at every newline, so Android's log limits cannot swallow a stack trace. - Sinks added or removed while an event is being dispatched no longer corrupt the iteration;
removeSinkclears the sink's failure mark; dispatch re-entered more than three levels deep drops the event and says so once. ReportThrottlemeasures on a monotonic clock, so moving the device clock backwards cannot suppress reports.LogEvent.attributesgainedevent.name.
Fixed #
- A draft that named only a channel was stamped with the microtask's time, not the moment the action ran.
- The console sink's documented format did not match what it rendered.
- A
tracewhisper above the console's ceiling was still built and still evicted the ring.
0.1.0 - 2026-09-04 #
First release: the LogEvent model, the log draft with its independent channel actions, the
console sink and its per-platform delegates, the ring buffer, ReportThrottle and the telemetry
zone.