logger_builder 0.8.0
logger_builder: ^0.8.0 copied to clipboard
A toolkit for creating your own customizable and hierarchical loggers in Dart.
0.8.0 #
[breaking changes]
-
HasFlushis removed. It was the nameFlushablecarried before 0.4.0 and has been a deprecated alias ever since; three minor versions later nothing referenced it — not the package, not its examples, not its known consumers. Replace any remaining use withFlushable.The removal happens now because the public API is about to be frozen at 1.0, and an alias carried into 1.0 would have to be carried until 2.0.
Documentation
- The subclassing contract — the six
@protectedmembers — is documented as a contract rather than as an implementation.processLognow says when it is read: on the transition into the enabled state, not per call, and for a sublogger of an already-enabled parent during construction, before the subclass constructor body has run.loggernames the owning logger instead of "the parent", which in this package means something else.registerLevelssays what is initialized by the time it runs — field initializers yes, constructor body no — and that no level is enabled yet. - The README's list of mistakes gains the same rule as an entry: a
latefield assigned in the constructor body and read fromregisterLevelsorprocessLogthrows, because both run before that body does.
0.7.0 #
The 0.6.2 work below was never published; the per-level publisher pin landed on top of it and needed a breaking-change bump, so the unreleased section became 0.7.0 instead.
[breaking changes]
logger[level].publisher = ...no longer detaches the whole logger from its parent. It pins that one level; the others keep following the parent, andpublisherLinkedstays up. The loudest consequence is further down the tree: the coarse flag used to cut the branch off whole, so every sublogger under such a logger follows the parent again too.- The common
logger.publisher = ...setter no longer overwrites a pinned level. This applies to a lone logger as well as a sublogger, so the order of a common assignment and its per-level exceptions no longer matters. publisherLinkedtherefore reportstruein cases where it used to reportfalse— the getter itself is unchanged, the rule that clears it is.- There is no idiom left for detaching every publisher at once without
changing a value.
child[level].publisher = child[level].publisheris not it: after this release that assignment pins the level and leavespublisherLinkedup, and so does looping it overlevels. Assign a common publisher if you want the link dropped. CustomLogger.relink()now drops every per-level pin, not just the logger's own links, so a relinked logger follows its parent whole.- A linked sublogger takes the parent's publisher for that level rather than the parent's common one, so a parent with per-level exceptions passes those exceptions down instead of flattening them.
- A per-level
relink()propagates to linked subloggers, including the case where it has nothing to take: the reset travels down instead of stopping at the logger it was called on. hasPublishercan therefore return tofalseon a level that once had a real publisher. Code that read it as "was ever configured" needs to read it as "publishes somewhere right now", which is what it says.CustomLevelLoggergainshasOwnPublisherandrelink(). A subclass that already declares a member of either name silently overrides it, which forrelink()means the per-level relink stops working. Only an incompatible signature breaks the build, so grep for both names rather than waiting for the compiler.- Buffered publishers no longer retry a handed-back batch for ever.
maxRetriesbounds a run of failures — a batch that gets through pays the whole budget back — and when it is spent the batch goes toonDropped. The default is 100. Unbounded retrying never delivered a deterministically failing batch and never dropped it either: measured, one log with a throwingformatproduced 242 820 handler calls and as manyonErrorcalls in half a second. A pending retry timer is also a live root for the event loop, so a worker with a dead sink returned frommainand never exited. retryDelaynow doubles with each attempt, capped at 32 times the base. A flat delay spent the whole budget in the first fraction of a second, which is no use against the case the delay exists for.flush()called while aclose()is still draining now returns that close instead of an already-completed future.AsyncPublisherWithBufferandAsyncFormatterWithBufferalready behaved this way; the other six publishers reported an empty queue with logs still in flight, andMultiPublisherdemoted a correct wrapped publisher to that answer.TransformPublisherroutes a throw from the wrapped publisher to its ownonErrorwhen one is set, asMultiPublisherdoes. Without a handler the error still reaches the logging call site, unchanged.- The queues of all eight asynchronous publishers are bounded:
maxQueueSizedefaults to 100 000 entries accepted and not yet handled. Past that the incoming log is refused — it goes toonDroppedand never enters the queue, so nothing already accepted is lost andflush()andclose()keep their meaning. Before this an unreachable sink grew the queue until the process ran out of memory, with no limit, no policy and nothing counting the cost.maxQueueSize: nullrestores the old unbounded behaviour for the code that wants it. - A publisher with no
onDroppedno longer loses logs in silence: it prints the first loss at once and then a count, at most once every five seconds and widening to a minute while the losses keep coming. This is the first thing the package ever writes on its own.onDropped: (_) {}makes it quiet again, and any realonDroppedreplaces it.
New
CustomLevelLogger.hasOwnPublishertells a pinned level from one that takes its publisher from above — next tohasPublisher, which tells a real publisher from the no-op one.CustomLevelLogger.relink()drops the pin and takes from the chain again. UnlikeCustomLogger.relink(), it works on a root logger too: the level returns under that logger's common publisher — or, when that logger never assigned one, under the no-op publisher.maxRetrieson all four buffered publishers. Zero drops a handed-back batch at once; there is no unbounded setting, on purpose.maxQueueSizeon all eight,nullfor a queue that is not bounded.onDroppedon the four unbuffered publishers, which had no way to see a loss at all. It reports the log the full queue refused, one at a time and with itsparamwhere there is one; on the buffered four the same callback now also reports overflow alongside the retry budget and the close.
Fixes
- A partial retry no longer reorders the batch. Both halves of a buffered
formatter receive the retry buffer, so what came back was "what
formathanded back" followed by "whatoutputhanded back" —three, one, two, fourfor a batch published asone, two, three, four— and the queue used it in that order. outputis no longer called whenformathanded the whole batch back. It used to run with an empty list of logs and whatever payloadformathad built, which for a network or file sink is an empty request on every retry.- A buffered publisher now reports handler errors into the zone that
built it. The queue is created lazily, on the first
publish, and it used to capture the zone there — so every later error went to whichever scope happened to log first, usually a request rather than the top level where the logger was made. The unbuffered family already worked this way. TypedLazy.valuememoizes a throwingconvert, asLazy.resolvedalready did for a throwing factory.LazyString.convertis atoString(), and one with a side effect used to run again on every access.- Registering a sublogger from inside
processLogno longer throwsConcurrentModificationError. The propagation walks now iterate a snapshot;processLogis the one documented hook that runs inside them. - A
CustomLogger.onErrorhandler that logs no longer recurses until the stack is gone. Both reentrancy guards are latched while the handler runs and reported the violation through that same handler; measured,log.i(...)returned normally after 1677 nested calls and then the isolate died. Reporting into an unrelated logger is untouched. CustomLogger.relink()no longer leaves a level on the publisher it was detached with. It copied what the parent had, so a parent configured purely per level left an unconfigured level stale whilepublisherLinkedwent back up — and neither the per-levelrelink()nor anything else could undo it.onErroris no longer resolved on the way to a successful publish. It is the one setting resolved by walking the parent chain, and an enabled log paid for the walk on every call: 9.4 ns at depth 0 against 57.7 ns at depth 20 in AOT, now flat.TransformPublisher.close()andflush()no longer let a wrapped publisher's synchronous throw escape to the caller's call site: the future they handed back fails instead, asMultiPublisherhas always done.close()is terminal even when the wrappedclose()throws before its firstawait. The synchronous throw escaped before the future was recorded, soisClosedstayedfalseand the publisher went on accepting logs and handing them to the publisher the application had just tried to close.MultiPublisherwas never affected: it already materialises the call withFuture.sync.
0.6.2 (unreleased, folded into 0.7.0) #
Documentation and the example only: lib/ is untouched, so there is nothing
to migrate. The example moves to ansi_escape_codes 4.x, and the README
snippets that colour a log follow it there.
Documentation
- The example depends on
ansi_escape_codes: ^4.0.1, up from^3.0.2. The major release renamed and removed a good deal —MatchtoPiece,LinktoOscLink,rgbandgraytorgb256andgray256, every name deprecated earlier — but nothing the example used: it analysed clean before a line of it was touched. The SDK floor of 4.0.1 is^3.6.0, the same as this package's, so the floor of the example is where it was. - The five README snippets that print errors in red call
Styles.red(...)where they calledred(...).ansi_escape_codes4.0.0 moved its 530 top-level style names into constants of a single class,Styles, so the old form no longer compiles against the version the example now uses. The import is unchanged:Stylescomes frompackage:ansi_escape_codes/style.dartlike the names it replaces. console.dartin the example measures the width of a line withlengthWithoutEscapeCodes, which 4.x has and 3.0.2 did not, instead of building a cleaned copy of the string and asking for its length. Same answer, one copy of the string fewer.
0.6.1 #
Documentation only: lib/ is untouched, so there is nothing to migrate.
Three README claims that had never been measured now are, and the benchmark
sections behind them ship with the example.
Documentation
- The paragraph on
processLogas a closure versus as a method no longer says the method avoids "creating a closure on each call" — nothing does.processLogis read once per level toggle, so the closure is allocated when a level is switched on, not when a log is written. Measured over 1M calls per form, the two land within 2 ns of each other, and which one is ahead depends on the compiler: AOT gave 10.4 ns for the closure against 11.9 ns for the method, the JIT gave 11.9 against 11.7. - "Use closures in all cases" now carries the asymmetry that justifies it: with the level enabled a closure adds about 3 ns to a call that costs ~135 ns anyway, and with the level disabled it turns 41 ns into 4 ns. The two lazy forms are also told apart — a tear-off of an existing function allocates nothing per call and comes to 1.9 ns on a disabled level, while a closure literal allocates one every call, on or off.
- "
printalways writes to stdout" is scoped to native targets. On the webprintgoes todartPrintif the embedder defines one and toconsole.logotherwise — the same code indart compile jsand indart compile wasm— anddart:iothere compiles only to throwUnsupportedErrorat runtime, so the stdout/stderr recipe builds and then fails at the user's. A note under that section says all of this, including that Dart never callsconsole.error, so the two streams cannot be split on the web at all. benchmarks.dartgains the three sections those numbers come from: the same cheap payload through eager interpolation, a tear-off and a closure literal, at an enabled and at a disabled level, plus two loggers differing in exactly one line — whetherprocessLogis a closure or a method.- The hierarchical example renames
withAddedNametochild. The README stopped teachingwithAddedNamein 0.6.0, because no published API has that name, but the example it links to for that very section still defined it.
0.6.0 #
The 0.5.1 work below was never published; an independent review of the whole code base then found defects that need behaviour changes, so the unreleased section became a minor bump instead.
[breaking changes]
environment.sdkis now^3.6.0, up from^3.2.0, matching theoldest-supportedCI job and the example package — the declared floor is the one actually exercised.^3.2.0was true for pure Dart, verified by running the package on a real 3.2.0 SDK, but nothing in CI proved it.metais now^1.15.0, down from^1.16.0. This is what made the package usable from Flutter at all: Flutter pinsmetafrom its own SDK, with an exact version in older releases (3.24 and 3.27 both pin 1.15.0), so^1.16.0failed version solving there — on a Flutter whose Dart satisfied the declared floor. Nothing in the package needs a newer meta: only@protected,@visibleForTestingand@immutableare used. With both changes the first usable Flutter is 3.27 instead of 3.29.CustomLevelLoggernow rejects aleveloutside(Levels.all, Levels.off)with anArgumentError, in every build mode. Those two are thresholds, not levels: a level logger registered atLevels.offstayed enabled withlogger.level = Levels.off, silently defeating "logging is completely disabled".- Registering one
CustomLevelLoggerinstance in two loggers now throws aStateError. It used to succeed and hand the first logger's logs to the second one's publisher and transformer. TransformPublisher.close()is terminal and idempotent, like every other publisher:publishafterwards throws aStateErrorand repeated calls return the same future. Previously logs still went through, unless the wrapped publisher happened to implementClosable.- A sublogger now holds its parent with a strong reference (the parent
still holds subloggers weakly). An intermediate logger the caller did not
keep used to be collected, after which
level,publisherandtransformerchanges stopped reaching its descendants while they still reported themselves linked andrelink()returnedfalseforever.
New
AsyncPublisherWithBufferBase.onDroppedand the same onAsyncPublisherWithBufferAndParamBase— called with the entries dropped because they were handed back to the retry buffer afterclose(). That loss is by design (they can never be processed), but it used to be invisible: no error, no callback, no counter. A randomized stress run dropped 163 entries without a trace.CustomLevelLogger.hasPublisher— whether a level has a publisher that goes somewhere, or is still on the no-op one every level starts on. The second state is indistinguishable from a working level otherwise: the log function returns normally andisEnabledistrue, because the level is enabled. The no-op publisher is private, so nothing outside the package could tell.CustomLogger.onError— one hook for every error the logger catches on the publish path: a throwingtransformer, a reentrancy guard violation, and a throwing publisher. With no handler set each case keeps its previous behaviour, so nothing changes for existing code: the first two go to the current zone, and a publisher error keeps propagating out of the logging call. Setting it is what makes logging unable to break the application that logs — in a plain Dart program without an error zone, the zone route terminates the isolate, so a bug in a masking transformer used to take the process down with no way to opt out. Unlikelevel, the publishers andtransformer, it is resolved through the parent chain instead of being copied down: a sublogger with no handler of its own uses its parent's, there is no link flag, andrelink()does not affect it.
Fixes
CustomLogger.isLoggableno longer contradicts itself at the thresholds:isLoggable(Levels.off)wastruefor a logger set toLevels.off.Levels.allandLevels.offare thresholds, not levels, and no level logger can be registered on either, so both now answerfalse.CustomLogger.levelsreturns a snapshot instead of a live view of the map keys, so registering a level while iterating it no longer throwsConcurrentModificationError.TransformPublisher.flush()afterclose()completes without touching the wrapped publisher, like every sibling. With an inner publisher that isFlushablebut notClosableit kept flushing through one it had already disowned.- Reading
isClosed(or callingflush) on a buffered publisher no longer creates its queue. Asking whether a publisher was closed used to materialise aStreamControllerwith a live subscription nobody would ever close, and pinned the zone that receives handler errors to whoever asked first. flush()on the unbuffered publishers no longer moves error routing: re-creating the internal listener happens in the zone the publisher was constructed in, not the zone that flushed.- A throwing
Lazyfactory is memoized likelate final— the error is stored, the closure released, and later accesses rethrow it. It used to run again on each access, so a factory with a side effect ran once per publisher in aMultiPublisher, while the class promised the source had been replaced by the result. - Toggling a level that is already in the requested state is a no-op.
processLogallocates a fresh closure in every documented pattern, so re-toggling was pure waste (3M closures for 50 level assignments over 20k linked subloggers) and it changed the identity of a function a caller may have hoisted. close()on a buffered publisher no longer sleeps through a pendingretryDelay. The retryTimerwas not kept anywhere, so shutdown latency scaled withretryDelay— and the entries it waited for were dropped afterwards anyway. The timer is now cancelled and one prompt final attempt is made instead.flush()on a buffered publisher no longer reports an empty queue while aclose()is still draining.isClosedflips whenclose()is called, soflush()short-circuited to an already-completed future: a false all-clear at exactly the moment durability matters. It now returns the close.- A level registered on a sublogger that still follows its parent takes the
parent's publisher for that level, instead of the parent's common
publisher. Parent and child used to publish the same level to different
destinations while
publisherLinkedreportedtrue. - A buffered publisher whose handler keeps returning its batch through the
retry buffer no longer starves the event loop. Retries were re-ticked
through the microtask queue, which never yields: with the sink down,
timers, I/O and the application's own
close()never got a turn and the isolate wedged. Retries now go through the event loop, and the newretryDelay(defaultDuration.zero) spaces them out. - A synchronous publisher that logs through the level it publishes for is
now caught the same way a reentrant transformer is: the nested log is
dropped and a
StateErroris reported. It used to recurse about 2570 frames into aStackOverflowError, running the transformer and the publisher's side effects once per frame. The guard is per level logger, so a publisher that logs at a different level of the same logger is allowed — a cycle still trips it on the way back. An asynchronous publisher is outside the guard entirely: itshandleruns afterpublishreturned, so a handler that logs into its own logger grows the queue without bound instead of overflowing the stack. That limit is now documented rather than glossed over. - A throwing
formatinAsyncFormatterWithBufferandAsyncFormatterWithBufferAndParamreturns the whole batch to the retry buffer instead of dropping it. There was no other point at which the caller could hand it back, so the batch vanished silently. - Retrying one copy of a log that appears twice in a batch no longer
withdraws the other copy from
output. - A level registered after
publisherwas assigned inherits it, andrelink()applies the parent's common publisher to levels the parent does not have. Such a level reported itself enabled while publishing into the no-op publisher. CustomLogger.subandrelink()no longer dispatch through the overridablelevel/publisher/transformersetters, so a subclass that overrides one no longer crashes while the superclass is still constructing.- Creating subloggers is no longer quadratic: registration pruned the whole list every time. Creating 16k subloggers under one parent went from 916ms to 6ms.
MultiPublisher.flush()afterclose()completes immediately instead of cascading into the wrapped publishers.- The publisher returned by
withParam()now implementsFlushableandClosable, delegating both to the publisher that owns the shared queue. It implemented neither, andMultiPublisherandTransformPublisherselect members with a type test — so the adapter was skipped:flush()andclose()completed successfully,isClosedon the real publisher stayedfalse, and every queued log was lost at shutdown with no error, no callback and no diagnostic. Because the queue is shared, closing any adapter closes it for all of them. AsyncFormatterWithBufferAndParammatches the log half of an entry by identity when computing what is left foroutput, likeAsyncFormatterWithBufferalready did. It used a structurally keyed map, so aCustomLogsubclass with value equality made two distinct logs interchangeable: the entry handed back to the retry buffer was passed tooutputand re-queued, while the other one was silently withdrawn and never published.
Documentation
CustomLevelLogger.logwarns against hoisting it into a variable: enabling and disabling a level swaps the field, so a stored function is a snapshot that keeps publishing afterlogger.level = Levels.off.CustomLevelLogger.publishersays that reading it and publishing yourself bypassesCustomLogger.transformer— the transformer is a convenience on the library's own path, not an enforced boundary.- The
Lazywarning coversasyncfunctions, which the type test also calls:Instance of 'Future<...>'gets logged and the future's error becomes an unhandled zone error. One mistake, two failures. pruneSubloggers()and the three*Linkedgetters are no longer marked@visibleForTesting. The first is the library's own memory-management primitive, called on every propagation; the others answer "is this sublogger still following its parent?", which is a fair question in production givenrelink()is public.analysis_options.yaml:require_trailing_commascarries a note that it only works while the language version stays below 3.7 — the 3.7 formatter's tall style removes the very commas it demands, measured at 43 issues against a freshly formatted tree. Also dropped two deprecated rules (one of which was suppressed at every single trigger site), commented out three Flutter-only rules and three non-existent excludes, moved eight ruleslints/recommendedhas since absorbed into the inherited block, marked the rules that are still experimental,.unnecessary_ignoreis left off on purpose: it does not exist in the 3.6.0 analyzer theoldest-supportedjob pins, and suppressing theundefined_lintwarning that would cause costs more than the rule is worth.- The
*Linkedflags double as in-progress markers — they are cleared before propagation recurses and restored after — and that is the only thing stopping a cycle in the sublogger graph from recursing until the stack is exhausted.registerSubloggeris protected, so a subclass can build one. Nothing said so and no test covered it; both now do. - CI now validates the published archive, runs every example, and smoke-tests
the web and wasm targets pub.dev advertises. Dependabot watches the
pubdependencies as well as the actions,actions/checkoutis SHA-pinned likesetup-dartalready was, and a weekly scheduled run compensates for the absence of a committed lockfile. - The example package declared
sdk: ^3.2.0, mirroring the library, and could not resolve there:loggingneeds ^3.4.0,ansi_escape_codesandlintsneed ^3.6.0. It also declared atestdev dependency with notest/directory. - New README sections: "Hierarchical Loggers" (including
CustomLogger.sub, which was never documented) and "Transformers" (the 0.5.0 headline feature, previously mentioned only under "Common Mistakes"). - The "How to make your own logger?" tutorial did not compile: step 4
defined
debug/info/errorgetters while step 5 calledi/e. The prose also swappedLevels.allwithLevels.offand named theCustomLogfieldlevelShortNameinstead ofshortLevelName. - The reentrancy guard is documented at its real reach: it is synchronous — per logger for the transformer, per level logger for the publisher — so it does not catch a cycle through a sublogger that inherited the same transformer, nor one that crosses an asynchronous hop.
- The unbounded queues, the dropped retry buffer on
close, the lazily created buffered queue and its zone, andLazycalling any zero-argument value are all documented now. - The "Several Publishers" example did not compile, in the README and in the
MultiPublisherdartdoc alike: with the publishers bound to locals first there is no context type, soLogwas inferred asCustomLogand the assignment tologger.publisherwas rejected. All three constructors now carry the type argument, with a note on why it is needed. - A throwing
handle/outputin the buffered publishers is documented at its real reach: only what it placed in the retry buffer survives, the rest is dropped, and reporting the error does not preserve it.formatis the one exception (it retries the whole batch) becauseoutputnever ran. - The two parameterized publisher bases carry the same warnings as their
twins: the unbounded queue on
AsyncPublisherWithParamBase, and the unbounded buffer, the dropped-at-close entries and the lazily created queue onAsyncPublisherWithBufferAndParamBase. Both omissions were the same contracts, just undocumented on half the family. - The class documentation of
CustomLoggerandCustomLevelLoggerno longer describes "message builders and printers" — an API removed in 0.3.0 and replaced byCustomLogPublisher. It was the first prose on the pub.dev API page for the two most important classes in the package. AsyncPublisherWithBufferBaseno longer contradicts its ownclose(): the class note said logs in the retry buffer whencloseis called are dropped; only logs handed back after it are.- The README no longer teaches
withAddedName, which does not exist in the package and was never defined in the README either — the "Using logger_builder in your own package" section was uncompilable end to end. It now uses thechild(...)the README itself builds, and says that the name is yours to choose over the protectedCustomLogger.sub. - The README's opening note said a logger needs only
..level = ...before it says a word. It also needs..publisher = ...: every level starts on a no-op publisher, and an unconfigured level still reportsisEnabledastrue, so the two mistakes look identical. - The README said
close()"refuses new logs". It throws aStateErrorat the logging call site, which is now stated, along with the two different meanings offlush()— snapshot for the unbuffered publishers, drain for the buffered ones. - New README section "The full set": the 2x2 table of the eight asynchronous
classes, what separates the
AsyncFormatter*half from the rest, and the three shared arguments.AsyncFormatter,retryDelay,Flushable,Closable,syncandisClosedappeared in the README zero times before this.
0.5.1 (unreleased, folded into 0.6.0) #
- A log transformer that logs through its own logger — or into its own
TransformPublisher— no longer recurses: the reentrant call is detected, the nested log is dropped and aStateErroris reported toonErroror the current zone. Previously such a call recursed until the stack was exhausted; on the way out every frame published its own log (measured: ~2700 duplicates from one logging call), and theStackOverflowErrorcould escapeZone.handleUncaughtErrorunhandled and terminate the isolate. Any cycle that comes back to a logger whose transformer is already running is covered; logging into an unrelated logger is unaffected, and chained transform publishers do not trip the guard. There is no cost whentransformerisnull(the default). (The README sections "Why not justif (logging)?", "Common Scenarios", "Common Mistakes" and "Using logger_builder in your own package" were originally listed here. They landed after theRelease 0.5.1commit, so they belong to 0.6.0 and are credited there.) - The README and the bundled examples now publish via
CustomLevelLogger.publishLoginstead ofpublisher.publish. They had been left on the pre-0.5.0 form, so loggers copied from them ignoredCustomLogger.transformer— exactly the pitfall the 0.5.0 note described.
0.5.0 #
- Pre-publication log processing: the new
LogTransformertypedef (Log? Function(Log)), theCustomLogger.transformerproperty applied to every log right before publishing (inherited by subloggers likelevel/publisher, same link/unlink/relinksemantics), and theTransformPublisherwrapper for per-destination transformation. Returningnulldrops the log. Fail-closed: a throwing transformer drops the log and reports the error toonError(TransformPublisher) or the current zone. - [breaking changes]
CustomLevelLoggergains the protectedpublishLog:processLogimplementations must call it instead ofpublisher.publish(...), otherwiseCustomLogger.transformeris ignored. - The new protected
CustomLog.copycopies level fields and the zone from an existing log and assignserror/stackTraceverbatim — the building block forcopyWithin subclasses (a copy keeps the log's identity: no new number or time should be minted).
0.4.0 #
- [breaking changes] Publisher lifecycle interfaces:
HasFlushis renamed toFlushable(HasFlushremains as a deprecated alias), and the newClosableinterface exposesclose(). All async publishers implement both;MultiPublisher.close()closes everyClosablepublisher in its list. - [breaking changes]
MultiPublisher.flushwithonErrorset now routes each publisher's error to the callback and completes normally; withoutonErrorthe previous behavior (ParallelWaitError) is preserved.closeerrors are routed the same way. - [breaking changes]
MultiPublishercopies the publisher list at construction; mutating the original list no longer affects the publisher. - [breaking changes]
CustomLevelLoggeris now anabstract base class: it can be extended, but no longer implemented outside the package. - New hierarchy management API:
CustomLogger.levelslists the registered level values (a live view);CustomLogger.relink()re-attaches an unlinked sublogger to its parent (re-inheriting the level and publishers). The unlink idiom (child.level = child.level) is now documented. Note: a subclass that already declares a compatible member namedlevelsorrelinkwill silently override the new API — rename such members. MultiPublishernow has a closed state:publishafterclose()throws aStateError, repeatedclose()calls return the same future, and a newisClosedgetter reports the state. A throwingonErrorcallback no longer escapes to the logging call site or replaces the original error inflush/close— the secondary error is reported to the current zone.- The
public_member_api_docslint is enabled; every public member is documented.
0.3.3 #
AsyncPublisherWithBuffer/AsyncPublisherWithBufferAndParam:flush()no longer hangs on an idle queue — it completes immediately. Flush has drain semantics: it also waits for logs published after the call, until the buffer becomes empty.- All async publishers: a new optional
onErrorcallback receives errors thrown by the handler; without it, the error is reported to the current zone (same asMultiPublisher). A throwing handler no longer stalls the queue, loses the retry buffer, or leavesflush()hanging. - All async publishers: a new
isClosedgetter.publishafterclose()now always throws aStateError(buffered publishers used to silently accept logs into a dead buffer);flush()afterclose()completes immediately instead of resurrecting the publisher; repeatedclose()calls are no-ops returning the same future. AsyncPublisher/AsyncPublisherWithParam: concurrentflush()calls are now serialized — an overlapping flush no longer hangs forever and no longer loses queued logs.- Buffered publishers:
close()now drains the queue completely — logs published while a batch was in flight are processed instead of being silently dropped. Entries returned to the retry buffer after closing are dropped by design (documented). - All async publishers: an
onErrorcallback that itself throws can no longer stall the queue; the secondary error is reported to the current zone and processing continues. AsyncFormatterfamily: whenOutisObject?/dynamic, an asynchronousformatresult is now awaited instead of being passed tooutputas an unresolvedFuture.AsyncFormatterWithBuffer/AsyncFormatterWithBufferAndParam: the batch passed tooutputnow reflects retry-buffer additions made during an asynchronousformat.TypedLazy(LazyString,LazyStringOrNull): readingresolvedaftervaluenow returns the converted value instead of leaking an internal sentinel object.LazyString.resolved: the defaultfallbackValueis now'null', same as the main constructor.CustomLevelLogger: an emptynamenow throws anArgumentErrorin all build modes (previously an assert, and aRangeErrorin release); the defaultshortNameis the first code point of the name, correct for non-BMP characters.- Hierarchy: setting a per-level publisher on a logger no longer throws mid-propagation when a sublogger did not register that level.
- Hierarchy: sublogger bookkeeping no longer uses a
Finalizerthat kept the parent logger strongly reachable through its live subloggers; dead weak references are pruned automatically during traversals. - Internal stream subscriptions are now stored and cancelled on
close(). - Docs: dartdoc for
HasFlush, async publisher members, theLazyfamily,Levelsconstants andCustomLog.zone. - Tests: the async publisher family, the
Lazyfamily, and level/hierarchy edge cases are now covered (63 new tests).
0.3.2 #
MultiPublisher: an exception thrown by one publisher no longer interrupts publishing to the remaining publishers and no longer propagates to the logging call site. The newonErrorcallback receives the failing publisher along with the error; without it, the error is reported to the current zone as an uncaught asynchronous error.MultiPublisher.flush: a synchronous throw from one publisher'sflushno longer prevents flushing the others.
0.3.0-0.3.1 #
- [breaking changes] Refactor a builder and a printer to one publisher.
- Add the async publisher family:
AsyncPublisher,AsyncPublisherWithParam,AsyncPublisherWithBuffer,AsyncPublisherWithBufferAndParamandMultiPublisher(recorded retroactively). - Upgrade ansi_escape_codes to 3.0.2 for examples.
0.2.0 #
- [breaking changes] Rename
LazyStringtoLazyStringOrNullandLazyNonNullableStringtoLazyString. - Refactor
hierarchical_logger.dartexample to useLazyStringfor path.
0.1.3-0.1.4 #
- Fix bug with builder and printer inheritance in subloggers.
0.1.2 #
- Add a CI badge to README.
- Remove vm_service from example.
- Add GitHub Actions for CI.
- Downgrade Dart SDK constraint to 3.2.0.
- Downgrade meta to 1.16.0.
0.1.0-0.1.1 #
- Initial version.