diff --git a/.changeset/adapter-encode-reuse.md b/.changeset/adapter-encode-reuse.md deleted file mode 100644 index 388d0d6f..00000000 --- a/.changeset/adapter-encode-reuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -refactor: build each adapter's HTTP request once instead of twice — every HTTP adapter (`axiom`, `better-stack`, `datadog`, `otlp`, `sentry`, `posthog` in `events` mode) constructed its URL, headers and body in two places: the `encode()` passed to `defineHttpDrain()` and again inside its standalone `sendBatchTo*` helper. The two copies had already drifted, reporting the same failure under different names (`axiom API error` from the drain, `Axiom API error` from the helper), and Axiom's and Better Stack's `encode()` ignored the deprecated `token` / `sourceToken` aliases the helper honoured. Both paths now share one encoder per adapter and report failures identically. `defineHttpDrain()` accepts an optional `label` for the human-readable name used in error messages, and `sendEncodedDrainRequest()` is exported from `evlog/toolkit` so custom adapters can reuse their own encoder the same way diff --git a/.changeset/calm-native-redaction.md b/.changeset/calm-native-redaction.md deleted file mode 100644 index 74ecb5f5..00000000 --- a/.changeset/calm-native-redaction.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": patch ---- - -fix(core): redact own enumerable fields only, so getter-only prototype accessors such as `DOMException.code` are never assigned to. A field that still refuses the write now warns instead of throwing, and redaction keeps covering the own fields of class instances. diff --git a/.changeset/clickhouse-adapter.md b/.changeset/clickhouse-adapter.md deleted file mode 100644 index 2c2d300f..00000000 --- a/.changeset/clickhouse-adapter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -feat(clickhouse): add the ClickHouse drain adapter (`evlog/clickhouse`) — `createClickHouseDrain()` inserts wide events over ClickHouse's HTTP interface in `JSONEachRow` format, working with a local instance, a self-managed cluster, and ClickHouse Cloud. The default `evlog_events` schema keeps typed columns for what you filter and aggregate on (`timestamp`, `level`, `service`, `environment`, `request_id`, `trace_id`, `method`, `path`, `status`, …) plus the complete event as JSON in `data`, so adding a field to your events never needs a migration; pass `transform` to target your own schema. Asynchronous inserts are enabled and not awaited by default (`async_insert=1&wait_for_async_insert=0`), so ClickHouse batches server-side instead of creating one MergeTree part per request and draining never blocks on disk writes. Credentials are sent as `X-ClickHouse-User` / `X-ClickHouse-Key` headers rather than query parameters, so they never reach `system.query_log`. Configured via `CLICKHOUSE_ENDPOINT` / `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD` / `CLICKHOUSE_DATABASE` / `CLICKHOUSE_TABLE` or overrides, with `sendToClickHouse` / `sendBatchToClickHouse` for direct use. The adapter docs carry the matching `CREATE TABLE`. diff --git a/.changeset/client-log-error-instance.md b/.changeset/client-log-error-instance.md deleted file mode 100644 index 4794a005..00000000 --- a/.changeset/client-log-error-instance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": patch ---- - -fix(core): serialize Error instances passed to the client `log.error()` — `name`, `message` and `stack` are non-enumerable, so an `Error` spread into a wide event contributed nothing and the call emitted an event with no error at all. The docs teach `log.error(new Error(...))` for Next.js client components, and the type only accepted a tag pair or a plain object, so the pattern failed to type check as well. Errors now land under `error` with the same shape the server logger stores, including `code`, `status`, `cause` and friends diff --git a/.changeset/datadog-trace-correlation.md b/.changeset/datadog-trace-correlation.md deleted file mode 100644 index 6603a626..00000000 --- a/.changeset/datadog-trace-correlation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -fix(datadog): lift `event.traceId` / `event.spanId` into a root `dd` block — Datadog's log-to-trace correlation only reads `dd.trace_id` / `dd.span_id` at the payload root, so the ids `createDefaultEnrichers()` already sets arrived nested under `evlog` and never correlated. The nested copy stays, so `@evlog.*` facets keep working, and `resolveDatadogTraceContext` is exported for custom drains diff --git a/.changeset/duplicate-install-shared-registry.md b/.changeset/duplicate-install-shared-registry.md deleted file mode 100644 index 57d707b9..00000000 --- a/.changeset/duplicate-install-shared-registry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -fix(core): key request scope, logger config and error identity to a versioned `globalThis` registry — evlog declares 18 optional peers, and pnpm/bun (isolated linker) hash resolved peers into store paths, so two workspaces that resolve `ai` or `zod` differently end up with physically distinct copies of the *same* evlog version. Each copy used to carry its own `AsyncLocalStorage` (so `useLogger()` threw inside another copy's `withEvlog()`), its own logger configuration (so events emitted through the second copy were silently undrained and unredacted), and its own `EvlogError` class (so `instanceof` downgraded structured errors to bare 500s). All three are now shared per major version. `createLoggerStorage()` takes an optional storage id, and `EvlogError.isEvlogError()` replaces `instanceof` for cross-copy checks diff --git a/.changeset/elysia-streaming-emit.md b/.changeset/elysia-streaming-emit.md deleted file mode 100644 index 199a49e9..00000000 --- a/.changeset/elysia-streaming-emit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": patch ---- - -fix(elysia): defer the wide event until a streaming body closes — `evlog/elysia` emitted from `onAfterResponse`, which fires as soon as the response is handed off. For an SSE or chunked body that is long before the stream finishes, so anything the handler set mid-stream (AI token counts, tool calls, final status) was dropped with a post-emit `[evlog]` warning. Streaming responses are now claimed in `mapResponse` and their body wrapped, so the emit waits for the stream to close — the same behaviour Hono, oRPC, SvelteKit, React Router and Next already had (#321). Non-streaming responses are unaffected and still emit immediately. diff --git a/.changeset/framework-integration-consistency.md b/.changeset/framework-integration-consistency.md deleted file mode 100644 index 21759fa3..00000000 --- a/.changeset/framework-integration-consistency.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -refactor: route `evlog/nestjs`, `evlog/react-router` and `evlog/sveltekit` through `defineFrameworkIntegration()` — the three integrations each rebuilt the same request extraction, `crypto.randomUUID()` fallback, `attachForkToLogger()` call and `storage.run()` wrapper by hand instead of using the helper Hono, Express, Elysia, Fastify and oRPC already share. Behaviour is unchanged, but they now inherit anything the helper gains (including `waitUntil` extraction) for free. A new `pickBaseEvlogOptions()` toolkit export becomes the single place listing the `BaseEvlogOptions` fields, replacing the copy in `toMiddlewareOptions()` and the hand-written field list in `evlog/eve` — which silently dropped `waitUntil`, and would have dropped every option added later diff --git a/.changeset/friendly-error-merge.md b/.changeset/friendly-error-merge.md deleted file mode 100644 index fe54c67e..00000000 --- a/.changeset/friendly-error-merge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": patch ---- - -fix(core): record the same error twice without crashing. A handler that logs the error it then throws hands the same instance to `log.error()` and to the integration's `finish({ error })`; the second merge walked into the caller's own object and threw on any read-only field it carried. diff --git a/.changeset/hono-use-logger.md b/.changeset/hono-use-logger.md deleted file mode 100644 index 5ed182ac..00000000 --- a/.changeset/hono-use-logger.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"evlog": minor ---- - -feat(hono): export `useLogger()` and enable `log.fork()` — Hono was the only framework integration without `useLogger()`, so reaching the request logger from a service or repository meant threading the Hono `Context` down through every call. `import { useLogger } from 'evlog/hono'` now resolves the same logger `c.get('log')` returns, and `c.get('log')` is unchanged — it stays the idiomatic accessor inside route handlers. Attaching AsyncLocalStorage also enables `log.fork()` on Hono, for background work that emits its own wide event correlated by `_parentRequestId`. - -**Cloudflare Workers:** `useLogger()` is backed by `AsyncLocalStorage`, so `evlog/hono` now imports `node:async_hooks`. Workers deployments need the `nodejs_compat` (or `nodejs_als`) compatibility flag in `wrangler.toml`. If you cannot enable it, use `evlog/workers`, which stays free of `node:async_hooks` by design. diff --git a/.changeset/hybrid-adapter-category.md b/.changeset/hybrid-adapter-category.md deleted file mode 100644 index 0e5309d4..00000000 --- a/.changeset/hybrid-adapter-category.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": patch ---- - -docs: move OTLP and HyperDX into the "Cloud or Self-Hosted" category — both self-host as readily as they run managed, so filing them under `cloud/` alongside Axiom and Datadog was misleading. They join Loki and ClickHouse under `hybrid/`, and each page now splits its setup into explicit self-hosted and managed sections rather than mixing the two. The old `/integrate/adapters/cloud/otlp` and `/integrate/adapters/cloud/hyperdx` URLs 301 to the new locations. diff --git a/.changeset/loki-adapter.md b/.changeset/loki-adapter.md deleted file mode 100644 index 64c771c7..00000000 --- a/.changeset/loki-adapter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -feat(loki): add the Grafana Loki drain adapter (`evlog/loki`) — `createLokiDrain()` pushes wide events to Loki's push API, covering self-hosted single-tenant, multi-tenant (`X-Scope-OrgID`), and Grafana Cloud (instance ID + token as HTTP Basic). Each event is pushed as a JSON log line under a deliberately small label set — `service`, `environment`, `level` by default — so Loki's index stays cheap while everything else (`requestId`, `path`, custom fields) remains queryable with `| json`. Promote extra fields with `labelFields`, add deployment-wide labels with `labels`. Events sharing a label set are grouped into one stream and sorted by timestamp, since Loki rejects out-of-order entries. Configured via `LOKI_ENDPOINT` / `LOKI_API_KEY` / `LOKI_USER` / `LOKI_TENANT_ID` or overrides, with `sendToLoki` / `sendBatchToLoki` for direct use. diff --git a/.changeset/next-shared-pipeline.md b/.changeset/next-shared-pipeline.md deleted file mode 100644 index 526e3537..00000000 --- a/.changeset/next-shared-pipeline.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -refactor(next): run `withEvlog()` through the shared middleware pipeline — `evlog/next` reimplemented the whole request pipeline (route filtering, per-route service, tail sampling, emit, enrich, drain) instead of calling `createMiddlewareLogger`, so it silently drifted from every other integration. Two options declared on `NextEvlogOptions` never did anything: `plugins` was never applied, because Next built no plugin runner at all, and global `sampling.keep` tail conditions were never evaluated — Next only called the user's `keep` callback, and its tail context carried no `duration`, so duration-based keep rules could not match. Both now work. `keep` callbacks also receive `ctx.duration`, and error statuses are derived through the shared `extractErrorStatus`. Next's `after()` is wired as the pipeline's `waitUntil`, so drain work still runs after the response is sent; enrich now runs before the response returns, matching the documented `waitUntil` contract and every other integration — move latency-sensitive work into `drain` if you relied on enrich being deferred diff --git a/.changeset/programmable-redaction.md b/.changeset/programmable-redaction.md deleted file mode 100644 index 711ff940..00000000 --- a/.changeset/programmable-redaction.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"evlog": minor ---- - -Make emit-time redaction programmable - -`RedactConfig.replacement` now accepts a function, so a replacement can be derived from the value it replaces instead of being a constant — a stable fingerprint keeps requests correlatable without exposing the credential: - -```ts -initLogger({ - redact: { - patterns: [/\/public\/claim\/([A-Za-z0-9._-]{12,})/g], - replacement: (_match, ctx) => `/public/claim/[tok:${fingerprint(ctx.groups[0])}]`, - }, -}) -``` - -`RedactConfig.transform` covers policies that cannot be expressed declaratively — conditional on a sibling field, tenant-scoped, or allowlist-shaped. It runs before the declarative stages, so it sees raw values and `paths` / `builtins` / `patterns` still apply to whatever it leaves behind. - -Both run where redaction already runs: after the event is built, before the console write and before any drain. Failures are caught and reported like drain failures — a `replacement` function that throws or returns a non-string falls back to `[REDACTED]` rather than emitting the raw value, and a throwing `transform` is skipped without stopping the event from being logged. - -Function-valued policy cannot survive the build-time config bridges, which serialize to JSON; the Nitro modules now warn instead of dropping it silently. - -Closes #463 diff --git a/.changeset/waituntil-propagation.md b/.changeset/waituntil-propagation.md deleted file mode 100644 index 3d38a11c..00000000 --- a/.changeset/waituntil-propagation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -fix(core): stop dropping `waitUntil` when a `defineEvlog()` config is passed to middleware — `toMiddlewareOptions()` copied every other `BaseEvlogOptions` field but silently omitted `waitUntil`, so `defineEvlog({ waitUntil })` lost the serverless drain hook on its way to the framework integration and drains were awaited inline instead of being registered with the platform. `evlog/hono` now also picks up `c.executionCtx.waitUntil` on its own, so drains on Cloudflare Workers and Vercel Edge complete after the response is returned with no manual wiring; adapters without an `ExecutionContext` (Node, Bun, Deno) keep draining inline, and an explicit `waitUntil` option still wins diff --git a/.changeset/workers-middleware-pipeline.md b/.changeset/workers-middleware-pipeline.md deleted file mode 100644 index 8cb16d48..00000000 --- a/.changeset/workers-middleware-pipeline.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"evlog": minor ---- - -feat(workers): add `withEvlog()` so Cloudflare Workers get the full middleware pipeline — `evlog/workers` was the only integration that never ran `createMiddlewareLogger`, so `include` / `exclude`, per-route `routes` overrides, `redact`, `enrich`, `keep` tail sampling and `plugins` simply had no effect there. Wrap your fetch handler with `withEvlog(handler, options)` and the wide event is emitted for you when the handler returns, with the same option surface every other framework accepts; `ctx.waitUntil` is picked up from the third argument so drains outlive the response, streaming bodies defer the emit until they complete, and `requestId` now honours `x-request-id` before falling back to `cf-ray`. `defineWorkerFetch()` and `createWorkersLogger()` are unchanged and remain the manual-emit path. The entrypoint stays free of `node:async_hooks`, so it still runs without `nodejs_compat` diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 194322d2..66ad1449 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,12 @@ # @evlog/cli +## 0.3.1 + +### Patch Changes + +- Updated dependencies [[`f39ab30`](https://github.com/HugoRCD/evlog/commit/f39ab30d90af608acb1527a766d4823460dc99bd), [`899464a`](https://github.com/HugoRCD/evlog/commit/899464a6c4a2dcf0a2816ddd39eb74203c4d4a82), [`d7f482a`](https://github.com/HugoRCD/evlog/commit/d7f482aa41ad696db21ba07ffaaa355bf7fd0b56), [`f5d7474`](https://github.com/HugoRCD/evlog/commit/f5d7474232379a3346f2dfa8e23335b4a9bfa44a), [`12852d3`](https://github.com/HugoRCD/evlog/commit/12852d31ad10e990091c6cb1740d201fb9fc95ac), [`ecc3ea6`](https://github.com/HugoRCD/evlog/commit/ecc3ea60db28d7513c515958f127af6a1ec6a0d5), [`f662848`](https://github.com/HugoRCD/evlog/commit/f6628484226c11456611543f0930ef9ad6c9c857), [`4e12ebb`](https://github.com/HugoRCD/evlog/commit/4e12ebbbc33a04d8cc77c7bf09edce418466d804), [`2c20be7`](https://github.com/HugoRCD/evlog/commit/2c20be7620e4eeea1bb31cfbca91af66e60e849e), [`35431c2`](https://github.com/HugoRCD/evlog/commit/35431c2685a10b0448e22fd416a9b37e626ec1e0), [`9e3bd96`](https://github.com/HugoRCD/evlog/commit/9e3bd96d401890ff24001da742848b14ce65a4b7), [`1b0edb8`](https://github.com/HugoRCD/evlog/commit/1b0edb80b080b3c03fc2f60e848191fac2a6a2f7), [`c5e85b0`](https://github.com/HugoRCD/evlog/commit/c5e85b0b121a60b69699b4f6f2fe5831dee62f19), [`5d99391`](https://github.com/HugoRCD/evlog/commit/5d99391638a13bb7ea3a8b98f3ac71e07b9b72cb), [`374abfd`](https://github.com/HugoRCD/evlog/commit/374abfdd01522a7e74d26ecfc8f20c2ae8571e1a), [`2540aa5`](https://github.com/HugoRCD/evlog/commit/2540aa5eb526f9cd25a637cde1bc7115575a280e)]: + - evlog@2.23.0 + ## 0.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 5fa57975..d66d93c6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@evlog/cli", - "version": "0.3.0", + "version": "0.3.1", "description": "The official command line for evlog", "author": "HugoRCD ", "homepage": "https://evlog.dev", diff --git a/packages/evlog/CHANGELOG.md b/packages/evlog/CHANGELOG.md index 5dd9c7b9..adb66c21 100644 --- a/packages/evlog/CHANGELOG.md +++ b/packages/evlog/CHANGELOG.md @@ -1,5 +1,65 @@ # evlog +## 2.23.0 + +### Minor Changes + +- [#473](https://github.com/HugoRCD/evlog/pull/473) [`f39ab30`](https://github.com/HugoRCD/evlog/commit/f39ab30d90af608acb1527a766d4823460dc99bd) Thanks [@HugoRCD](https://github.com/HugoRCD)! - refactor: build each adapter's HTTP request once instead of twice — every HTTP adapter (`axiom`, `better-stack`, `datadog`, `otlp`, `sentry`, `posthog` in `events` mode) constructed its URL, headers and body in two places: the `encode()` passed to `defineHttpDrain()` and again inside its standalone `sendBatchTo*` helper. The two copies had already drifted, reporting the same failure under different names (`axiom API error` from the drain, `Axiom API error` from the helper), and Axiom's and Better Stack's `encode()` ignored the deprecated `token` / `sourceToken` aliases the helper honoured. Both paths now share one encoder per adapter and report failures identically. `defineHttpDrain()` accepts an optional `label` for the human-readable name used in error messages, and `sendEncodedDrainRequest()` is exported from `evlog/toolkit` so custom adapters can reuse their own encoder the same way + +- [#481](https://github.com/HugoRCD/evlog/pull/481) [`d7f482a`](https://github.com/HugoRCD/evlog/commit/d7f482aa41ad696db21ba07ffaaa355bf7fd0b56) Thanks [@HugoRCD](https://github.com/HugoRCD)! - feat(clickhouse): add the ClickHouse drain adapter (`evlog/clickhouse`) — `createClickHouseDrain()` inserts wide events over ClickHouse's HTTP interface in `JSONEachRow` format, working with a local instance, a self-managed cluster, and ClickHouse Cloud. The default `evlog_events` schema keeps typed columns for what you filter and aggregate on (`timestamp`, `level`, `service`, `environment`, `request_id`, `trace_id`, `method`, `path`, `status`, …) plus the complete event as JSON in `data`, so adding a field to your events never needs a migration; pass `transform` to target your own schema. Asynchronous inserts are enabled and not awaited by default (`async_insert=1&wait_for_async_insert=0`), so ClickHouse batches server-side instead of creating one MergeTree part per request and draining never blocks on disk writes. Credentials are sent as `X-ClickHouse-User` / `X-ClickHouse-Key` headers rather than query parameters, so they never reach `system.query_log`. Configured via `CLICKHOUSE_ENDPOINT` / `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD` / `CLICKHOUSE_DATABASE` / `CLICKHOUSE_TABLE` or overrides, with `sendToClickHouse` / `sendBatchToClickHouse` for direct use. The adapter docs carry the matching `CREATE TABLE`. + +- [#465](https://github.com/HugoRCD/evlog/pull/465) [`12852d3`](https://github.com/HugoRCD/evlog/commit/12852d31ad10e990091c6cb1740d201fb9fc95ac) Thanks [@HugoRCD](https://github.com/HugoRCD)! - fix(datadog): lift `event.traceId` / `event.spanId` into a root `dd` block — Datadog's log-to-trace correlation only reads `dd.trace_id` / `dd.span_id` at the payload root, so the ids `createDefaultEnrichers()` already sets arrived nested under `evlog` and never correlated. The nested copy stays, so `@evlog.*` facets keep working, and `resolveDatadogTraceContext` is exported for custom drains + +- [#468](https://github.com/HugoRCD/evlog/pull/468) [`ecc3ea6`](https://github.com/HugoRCD/evlog/commit/ecc3ea60db28d7513c515958f127af6a1ec6a0d5) Thanks [@HugoRCD](https://github.com/HugoRCD)! - fix(core): key request scope, logger config and error identity to a versioned `globalThis` registry — evlog declares 18 optional peers, and pnpm/bun (isolated linker) hash resolved peers into store paths, so two workspaces that resolve `ai` or `zod` differently end up with physically distinct copies of the _same_ evlog version. Each copy used to carry its own `AsyncLocalStorage` (so `useLogger()` threw inside another copy's `withEvlog()`), its own logger configuration (so events emitted through the second copy were silently undrained and unredacted), and its own `EvlogError` class (so `instanceof` downgraded structured errors to bare 500s). All three are now shared per major version. `createLoggerStorage()` takes an optional storage id, and `EvlogError.isEvlogError()` replaces `instanceof` for cross-copy checks + +- [#471](https://github.com/HugoRCD/evlog/pull/471) [`4e12ebb`](https://github.com/HugoRCD/evlog/commit/4e12ebbbc33a04d8cc77c7bf09edce418466d804) Thanks [@HugoRCD](https://github.com/HugoRCD)! - refactor: route `evlog/nestjs`, `evlog/react-router` and `evlog/sveltekit` through `defineFrameworkIntegration()` — the three integrations each rebuilt the same request extraction, `crypto.randomUUID()` fallback, `attachForkToLogger()` call and `storage.run()` wrapper by hand instead of using the helper Hono, Express, Elysia, Fastify and oRPC already share. Behaviour is unchanged, but they now inherit anything the helper gains (including `waitUntil` extraction) for free. A new `pickBaseEvlogOptions()` toolkit export becomes the single place listing the `BaseEvlogOptions` fields, replacing the copy in `toMiddlewareOptions()` and the hand-written field list in `evlog/eve` — which silently dropped `waitUntil`, and would have dropped every option added later + +- [#476](https://github.com/HugoRCD/evlog/pull/476) [`35431c2`](https://github.com/HugoRCD/evlog/commit/35431c2685a10b0448e22fd416a9b37e626ec1e0) Thanks [@HugoRCD](https://github.com/HugoRCD)! - feat(hono): export `useLogger()` and enable `log.fork()` — Hono was the only framework integration without `useLogger()`, so reaching the request logger from a service or repository meant threading the Hono `Context` down through every call. `import { useLogger } from 'evlog/hono'` now resolves the same logger `c.get('log')` returns, and `c.get('log')` is unchanged — it stays the idiomatic accessor inside route handlers. Attaching AsyncLocalStorage also enables `log.fork()` on Hono, for background work that emits its own wide event correlated by `_parentRequestId`. + + **Cloudflare Workers:** `useLogger()` is backed by `AsyncLocalStorage`, so `evlog/hono` now imports `node:async_hooks`. Workers deployments need the `nodejs_compat` (or `nodejs_als`) compatibility flag in `wrangler.toml`. If you cannot enable it, use `evlog/workers`, which stays free of `node:async_hooks` by design. + +- [#480](https://github.com/HugoRCD/evlog/pull/480) [`1b0edb8`](https://github.com/HugoRCD/evlog/commit/1b0edb80b080b3c03fc2f60e848191fac2a6a2f7) Thanks [@HugoRCD](https://github.com/HugoRCD)! - feat(loki): add the Grafana Loki drain adapter (`evlog/loki`) — `createLokiDrain()` pushes wide events to Loki's push API, covering self-hosted single-tenant, multi-tenant (`X-Scope-OrgID`), and Grafana Cloud (instance ID + token as HTTP Basic). Each event is pushed as a JSON log line under a deliberately small label set — `service`, `environment`, `level` by default — so Loki's index stays cheap while everything else (`requestId`, `path`, custom fields) remains queryable with `| json`. Promote extra fields with `labelFields`, add deployment-wide labels with `labels`. Events sharing a label set are grouped into one stream and sorted by timestamp, since Loki rejects out-of-order entries. Configured via `LOKI_ENDPOINT` / `LOKI_API_KEY` / `LOKI_USER` / `LOKI_TENANT_ID` or overrides, with `sendToLoki` / `sendBatchToLoki` for direct use. + +- [#474](https://github.com/HugoRCD/evlog/pull/474) [`c5e85b0`](https://github.com/HugoRCD/evlog/commit/c5e85b0b121a60b69699b4f6f2fe5831dee62f19) Thanks [@HugoRCD](https://github.com/HugoRCD)! - refactor(next): run `withEvlog()` through the shared middleware pipeline — `evlog/next` reimplemented the whole request pipeline (route filtering, per-route service, tail sampling, emit, enrich, drain) instead of calling `createMiddlewareLogger`, so it silently drifted from every other integration. Two options declared on `NextEvlogOptions` never did anything: `plugins` was never applied, because Next built no plugin runner at all, and global `sampling.keep` tail conditions were never evaluated — Next only called the user's `keep` callback, and its tail context carried no `duration`, so duration-based keep rules could not match. Both now work. `keep` callbacks also receive `ctx.duration`, and error statuses are derived through the shared `extractErrorStatus`. Next's `after()` is wired as the pipeline's `waitUntil`, so drain work still runs after the response is sent; enrich now runs before the response returns, matching the documented `waitUntil` contract and every other integration — move latency-sensitive work into `drain` if you relied on enrich being deferred + +- [#466](https://github.com/HugoRCD/evlog/pull/466) [`5d99391`](https://github.com/HugoRCD/evlog/commit/5d99391638a13bb7ea3a8b98f3ac71e07b9b72cb) Thanks [@HugoRCD](https://github.com/HugoRCD)! - Make emit-time redaction programmable + + `RedactConfig.replacement` now accepts a function, so a replacement can be derived from the value it replaces instead of being a constant — a stable fingerprint keeps requests correlatable without exposing the credential: + + ```ts + initLogger({ + redact: { + patterns: [/\/public\/claim\/([A-Za-z0-9._-]{12,})/g], + replacement: (_match, ctx) => + `/public/claim/[tok:${fingerprint(ctx.groups[0])}]`, + }, + }); + ``` + + `RedactConfig.transform` covers policies that cannot be expressed declaratively — conditional on a sibling field, tenant-scoped, or allowlist-shaped. It runs before the declarative stages, so it sees raw values and `paths` / `builtins` / `patterns` still apply to whatever it leaves behind. + + Both run where redaction already runs: after the event is built, before the console write and before any drain. Failures are caught and reported like drain failures — a `replacement` function that throws or returns a non-string falls back to `[REDACTED]` rather than emitting the raw value, and a throwing `transform` is skipped without stopping the event from being logged. + + Function-valued policy cannot survive the build-time config bridges, which serialize to JSON; the Nitro modules now warn instead of dropping it silently. + + Closes [#463](https://github.com/HugoRCD/evlog/issues/463) + +- [#470](https://github.com/HugoRCD/evlog/pull/470) [`374abfd`](https://github.com/HugoRCD/evlog/commit/374abfdd01522a7e74d26ecfc8f20c2ae8571e1a) Thanks [@HugoRCD](https://github.com/HugoRCD)! - fix(core): stop dropping `waitUntil` when a `defineEvlog()` config is passed to middleware — `toMiddlewareOptions()` copied every other `BaseEvlogOptions` field but silently omitted `waitUntil`, so `defineEvlog({ waitUntil })` lost the serverless drain hook on its way to the framework integration and drains were awaited inline instead of being registered with the platform. `evlog/hono` now also picks up `c.executionCtx.waitUntil` on its own, so drains on Cloudflare Workers and Vercel Edge complete after the response is returned with no manual wiring; adapters without an `ExecutionContext` (Node, Bun, Deno) keep draining inline, and an explicit `waitUntil` option still wins + +- [#472](https://github.com/HugoRCD/evlog/pull/472) [`2540aa5`](https://github.com/HugoRCD/evlog/commit/2540aa5eb526f9cd25a637cde1bc7115575a280e) Thanks [@HugoRCD](https://github.com/HugoRCD)! - feat(workers): add `withEvlog()` so Cloudflare Workers get the full middleware pipeline — `evlog/workers` was the only integration that never ran `createMiddlewareLogger`, so `include` / `exclude`, per-route `routes` overrides, `redact`, `enrich`, `keep` tail sampling and `plugins` simply had no effect there. Wrap your fetch handler with `withEvlog(handler, options)` and the wide event is emitted for you when the handler returns, with the same option surface every other framework accepts; `ctx.waitUntil` is picked up from the third argument so drains outlive the response, streaming bodies defer the emit until they complete, and `requestId` now honours `x-request-id` before falling back to `cf-ray`. `defineWorkerFetch()` and `createWorkersLogger()` are unchanged and remain the manual-emit path. The entrypoint stays free of `node:async_hooks`, so it still runs without `nodejs_compat` + +### Patch Changes + +- [#457](https://github.com/HugoRCD/evlog/pull/457) [`899464a`](https://github.com/HugoRCD/evlog/commit/899464a6c4a2dcf0a2816ddd39eb74203c4d4a82) Thanks [@EmilGramDK](https://github.com/EmilGramDK)! - fix(core): redact own enumerable fields only, so getter-only prototype accessors such as `DOMException.code` are never assigned to. A field that still refuses the write now warns instead of throwing, and redaction keeps covering the own fields of class instances. + +- [#477](https://github.com/HugoRCD/evlog/pull/477) [`f5d7474`](https://github.com/HugoRCD/evlog/commit/f5d7474232379a3346f2dfa8e23335b4a9bfa44a) Thanks [@HugoRCD](https://github.com/HugoRCD)! - fix(core): serialize Error instances passed to the client `log.error()` — `name`, `message` and `stack` are non-enumerable, so an `Error` spread into a wide event contributed nothing and the call emitted an event with no error at all. The docs teach `log.error(new Error(...))` for Next.js client components, and the type only accepted a tag pair or a plain object, so the pattern failed to type check as well. Errors now land under `error` with the same shape the server logger stores, including `code`, `status`, `cause` and friends + +- [#478](https://github.com/HugoRCD/evlog/pull/478) [`f662848`](https://github.com/HugoRCD/evlog/commit/f6628484226c11456611543f0930ef9ad6c9c857) Thanks [@HugoRCD](https://github.com/HugoRCD)! - fix(elysia): defer the wide event until a streaming body closes — `evlog/elysia` emitted from `onAfterResponse`, which fires as soon as the response is handed off. For an SSE or chunked body that is long before the stream finishes, so anything the handler set mid-stream (AI token counts, tool calls, final status) was dropped with a post-emit `[evlog]` warning. Streaming responses are now claimed in `mapResponse` and their body wrapped, so the emit waits for the stream to close — the same behaviour Hono, oRPC, SvelteKit, React Router and Next already had ([#321](https://github.com/HugoRCD/evlog/issues/321)). Non-streaming responses are unaffected and still emit immediately. + +- [#457](https://github.com/HugoRCD/evlog/pull/457) [`2c20be7`](https://github.com/HugoRCD/evlog/commit/2c20be7620e4eeea1bb31cfbca91af66e60e849e) Thanks [@EmilGramDK](https://github.com/EmilGramDK)! - fix(core): record the same error twice without crashing. A handler that logs the error it then throws hands the same instance to `log.error()` and to the integration's `finish({ error })`; the second merge walked into the caller's own object and threw on any read-only field it carried. + +- [#484](https://github.com/HugoRCD/evlog/pull/484) [`9e3bd96`](https://github.com/HugoRCD/evlog/commit/9e3bd96d401890ff24001da742848b14ce65a4b7) Thanks [@HugoRCD](https://github.com/HugoRCD)! - docs: move OTLP and HyperDX into the "Cloud or Self-Hosted" category — both self-host as readily as they run managed, so filing them under `cloud/` alongside Axiom and Datadog was misleading. They join Loki and ClickHouse under `hybrid/`, and each page now splits its setup into explicit self-hosted and managed sections rather than mixing the two. The old `/integrate/adapters/cloud/otlp` and `/integrate/adapters/cloud/hyperdx` URLs 301 to the new locations. + ## 2.22.4 ### Patch Changes diff --git a/packages/evlog/package.json b/packages/evlog/package.json index cba79eb0..90a7145c 100644 --- a/packages/evlog/package.json +++ b/packages/evlog/package.json @@ -1,6 +1,6 @@ { "name": "evlog", - "version": "2.22.4", + "version": "2.23.0", "description": "Modern TypeScript logger — simple logs, wide events, structured errors. Built for scripts, libraries, jobs, edge, and HTTP. One drain pipeline everywhere.", "author": "HugoRCD ", "homepage": "https://evlog.dev",