3423143: HTTP adapters no longer cap delivery at one 20-entry batch per round trip. Entries that arrive while a request is in flight wait and go out together in the next request, which starts as soon as the one in flight returns if a full batch (maxBatchSize) is waiting and on the flushIntervalMs timer otherwise, so light traffic is batched as before and a 50 ms backend now keeps up with thousands of records per second instead of dropping about half of them. Delivery order is unchanged. The new maxEntriesPerRequest option (default 500; 100 for Sentry) is the most entries one request carries; it applies to every request, so a maxBatchSize above it splits each batch. At most maxPendingBatches × maxBatchSize entries wait behind the request in flight, which carries up to maxEntriesPerRequest more, so an adapter holds at most 1,140 entries by default (740 for Sentry) where it held about 660; beyond that, entries are dropped, counted and reported once per request. Requests can carry more bytes than before, and vendor byte limits are not enforced. A failure of a request the queue started on its own goes to the adapter’s onError (or rate-limited stderr), not to config.onError, because no log() call is awaiting it; a failure of the request started by a log() or flush() call is still rejected to that caller. flush() waits for the entries accepted before the call, not for the queue to go idle, and it resolves rather than rejects when those entries went out in a request the queue started on its own and that request failed, so flushLogixlysia no longer passes that failure to config.onError. maxBatchSize and maxEntriesPerRequest must be positive integers, and the adapter throws at construction otherwise; maxBatchSize: 0 and Infinity, which used to mean one request per entry and timer-only batching, now throw too. The built-in HTTP adapters implement close(): it stops accepting entries, then flushes, so flushLogixlysia(options, { close: true }) releases them. close is now a required member of the exported AdapterTransport type, so an object of your own typed as AdapterTransport needs one as well. A transport wrapped in withRedaction is not closed through the wrapper yet; it forwards flush only.
9ac4a0a: Compressed rotation archives are created with logFileMode (default 0o600) instead of the process umask, so they are no more readable than the live log. An adapter’s “invalid endpoint URL” error no longer echoes the credentials embedded in the URL, in its message or in its cause. File write and rotation failures reported on stderr (when config.onError is not set) are rate limited to one message every 5 seconds, like transport and formatting failures.
ca5fe30: autoRedact no longer throws, and therefore no longer drops the record, when an error’s name or message is an inherited accessor (a DOMException, an app error class with get name()); redacted errors keep message, name and stack non-enumerable, as native errors do. A value with a toJSON method (Bun’s CookieMap, app classes) is redacted as what it serializes to; errors keep their class even when they define toJSON, so an HttpError stays an HttpError. The password of a URL, Bearer and Basic credentials in any string, sensitive query keys inside an absolute URL in a referer, location or content-location header, and a field named cookies are now masked. Redaction and log serialization stop at a depth of 64 with "[Depth]", so a deeply nested client payload no longer costs quadratic time or overflows the stack. Rotation sizes, intervals and retention given with units are rejected when they are zero, as the numeric zero already was.
7b030bc: LogFilter, LogixlysiaConfig, LogRotationConfig, SinkErrorContext and WebSocketLike are exported from the package root as types, so a config object, a rotation config, an onError handler or a wrapWs hook can be typed without reaching into the package’s internals. No runtime export changes.
665b71b: The access line is now written once per request, after Elysia has produced the final response, so it carries the status the client received for every exit: a mapResponse rewrite, an error mapped by an onError hook in either registration order, set.status followed by a thrown Error, a response returned from an onRequest hook, and an auth short-circuit. A generator (streaming) handler is logged when its body finishes, so {duration} covers the whole stream, and a throw inside the body produces an error line. A plain object with code and response keys is no longer mistaken for Elysia’s status() result. A thrown 2xx or 3xx status() now logs at INFO, as an access line without its message, where it was an ERROR line carrying the message. With useAsyncLocalStorage, useLogger() works inside hooks registered after the plugin, inside a generator body, inside a handler that awaits app.handle() for another route, and in work that outlives the response; outside a request it is still a no-op. The X-Request-Id response header is set as soon as the request starts, so short-circuited responses carry it too. The request boundary is app-wide: once the plugin is mounted anywhere, every request of the application gets a line, including routes defined on parent apps (previously those were silently unlogged); mount the plugin once at the root. Apps running with aot: false, or mounting the plugin inside group() or guard(), keep the previous hook-based behaviour, including a second error line for an error thrown after the handler returned. Consequences for existing expectations: an error thrown after the handler returned, by an afterHandle hook or while Elysia builds the response, now produces one error line instead of a success line followed by an error line; when an app-wide onError registered before the plugin answers, the plugin never sees the thrown value, so the line is an access line at the level of the status sent, with an empty message and no error payload, where it was an error line carrying the thrown value; an error that a later onError maps to a 2xx or 3xx (a redirect to a login page, say) is logged as an error line carrying that status and the thrown message, where it was logged as ERROR 500 before; a custom log made from a hook registered after the plugin replaces the access line, as a custom log from the handler always did, where it was a second line before, and a custom log made from a later onError now comes before the error line instead of after it; and with two plugin instances in sibling modules, access lines now go through the first instance’s sinks, while a custom log made through a later instance’s log still reaches that instance’s transports, without a request ID, and replaces the access line for that request (create the plugin once and share it).
0fb85de: store.pino is defined again. Since 6.7.0 the lazy pino Proxy was dropped by Elysia’s state() because its target had no keys, so every handler that read store.pino threw. Assignments such as store.pino.level = 'debug' now reach pino. The plugin rejects at construction a logFilter.level that is not an array of DEBUG, INFO, WARNING or ERROR, useTransportsOnly: true without a transport, and a flushTimeoutMs that is not a finite number of milliseconds at or above 0; each of these used to turn logging off or misreport shutdown silently. useTransportsOnly: true with an empty transports list was previously accepted as an effectively disabled logger; use disableInternalLogger and disableFileLogging for that. geoEnricher ignores non-string fields in a Netlify x-nf-geo header instead of throwing. A requestId.generator that throws or returns an invalid id no longer produces an error line on every request; the plugin falls back to a generated UUID and reports the failure through onError, or on stderr at most once per interval when no hook is set.
c7667f2: The HTTP adapters now take a JSON-safe copy of each record’s meta at the moment it is logged. A later change to a logged object is no longer shipped, which also closes a redaction bypass under autoRedact (an adapter wrapped by withRedaction takes its copy when the wrapper hands the record over, after its own asynchronous redaction, so that window is unchanged). A value that cannot be serialized (a throwing toJSON or getter) is replaced by "[Unserializable]" for that field only, where it previously turned a whole Axiom, Better Stack or Datadog batch into that string or rejected a ClickHouse, HyperDX, OTLP, PostHog or Sentry batch. A BigInt anywhere in meta is shipped as its decimal string by every adapter, so a BigInt user id now resolves to PostHog’s distinct_id, and flattenMeta no longer renders a nested BigInt or cycle as [object Object].
3373fc1: wrapWs no longer discards what a hook returns. A returned string, object, Buffer, promise or generator reaches Elysia and is sent to the client exactly as without the wrapper; a hook that returned a value by accident (for example (ws, m) => ws.send(m), whose result is Bun’s numeric send status) now has that value sent too, as Elysia does for an unwrapped hook. An async hook that rejects is handled by Elysia’s error path instead of becoming an unhandled rejection, and its lifecycle line is written after the hook settles.