@amkisko/json-logging 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +231 -2
  3. package/dist/bun-serve.d.ts +30 -0
  4. package/dist/bun-serve.js +60 -0
  5. package/dist/console-bridge.d.ts +26 -0
  6. package/dist/console-bridge.js +155 -0
  7. package/dist/console-state.d.ts +11 -0
  8. package/dist/console-state.js +40 -0
  9. package/dist/console.d.ts +18 -0
  10. package/dist/console.js +17 -0
  11. package/dist/context.d.ts +40 -0
  12. package/dist/context.js +162 -0
  13. package/dist/express.d.ts +41 -0
  14. package/dist/express.js +48 -0
  15. package/dist/fastify.d.ts +37 -0
  16. package/dist/fastify.js +53 -0
  17. package/dist/hono.d.ts +37 -0
  18. package/dist/hono.js +50 -0
  19. package/dist/http-ambient.d.ts +10 -0
  20. package/dist/http-ambient.js +18 -0
  21. package/dist/http-context.d.ts +49 -0
  22. package/dist/http-context.js +147 -0
  23. package/dist/http.d.ts +24 -0
  24. package/dist/http.js +34 -0
  25. package/dist/index.d.ts +31 -0
  26. package/dist/index.js +26 -0
  27. package/dist/json-logging.d.ts +9 -0
  28. package/dist/json-logging.js +12 -0
  29. package/dist/koa.d.ts +34 -0
  30. package/dist/koa.js +54 -0
  31. package/dist/logger.d.ts +46 -0
  32. package/dist/logger.js +130 -0
  33. package/dist/next.d.ts +40 -0
  34. package/dist/next.js +34 -0
  35. package/dist/objects.d.ts +12 -0
  36. package/dist/objects.js +18 -0
  37. package/dist/redact.d.ts +9 -0
  38. package/dist/redact.js +77 -0
  39. package/dist/remix.d.ts +43 -0
  40. package/dist/remix.js +38 -0
  41. package/dist/sanitize.d.ts +20 -0
  42. package/dist/sanitize.js +148 -0
  43. package/dist/serialize.d.ts +27 -0
  44. package/dist/serialize.js +200 -0
  45. package/dist/severity.d.ts +10 -0
  46. package/dist/severity.js +25 -0
  47. package/dist/sinks.d.ts +14 -0
  48. package/dist/sinks.js +66 -0
  49. package/dist/timestamp.d.ts +15 -0
  50. package/dist/timestamp.js +47 -0
  51. package/dist/types.d.ts +42 -0
  52. package/dist/types.js +2 -0
  53. package/package.json +98 -4
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,232 @@
1
- # Temporary Holding Version
1
+ # @amkisko/json-logging
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Structured JSON logging for Bun, Node, Deno, Next.js, and Remix. The portable API follows the Ruby `activesupport-json_logging` gem style: create a logger, call severity methods, use `tagged` and `withContext`, and emit one JSON object per line.
4
+
5
+ Zero runtime dependencies. Ambient `withContext` / block `tagged` use AsyncLocalStorage when the runtime exposes it through `process.getBuiltinModule("async_hooks")` (Node, Bun, Deno with Node builtins, Cloudflare Workers with `nodejs_compat`). The package never statically imports `node:async_hooks`, so browsers and Workers without Node builtins can still import and emit basic JSON lines; ambient context is a no-op there. Call `isAsyncLocalStorageAvailable()` to detect ALS. Helpers under `@amkisko/json-logging/next` and `@amkisko/json-logging/remix` use Web APIs and do not add framework packages as runtime dependencies.
6
+
7
+ ## Runtime compatibility
8
+
9
+ | Runtime | Support | Notes |
10
+ | --- | --- | --- |
11
+ | Node.js | Compatible | Primary target; ALS context works. |
12
+ | Bun | Compatible | Tested in CI; `@amkisko/json-logging/bun` for `Bun.serve`. |
13
+ | Deno | Compatible | ALS via Deno Node builtins (`getBuiltinModule`). |
14
+ | Cloudflare Workers | Compatible | Full ALS with `nodejs_compat`. Without it: import and basic logging still work; `withContext` does not retain fields. |
15
+ | Browsers | Compatible | Basic logging and sinks work. Ambient context requires a host that provides ALS (unusual in browsers). |
16
+
17
+ On the JSR package settings page, mark Node.js, Bun, Deno, Cloudflare Workers, and Browsers as compatible. Set the package description to the same summary as `package.json`.
18
+
19
+ ## Install
20
+
21
+ From npm:
22
+
23
+ ```bash
24
+ bun add @amkisko/json-logging
25
+ npm install @amkisko/json-logging
26
+ ```
27
+
28
+ From JSR:
29
+
30
+ ```bash
31
+ deno add jsr:@amkisko/json-logging
32
+ npx jsr add @amkisko/json-logging
33
+ ```
34
+
35
+ ## Five-minute usage
36
+
37
+ ```ts
38
+ import { JsonLogging, createLogger, withContext } from "@amkisko/json-logging";
39
+
40
+ const logger = JsonLogging.new();
41
+ // same as createLogger()
42
+
43
+ logger.info("server started");
44
+ logger.info({ event: "signup", userId: 42 });
45
+ logger.error(new Error("payment failed"));
46
+
47
+ logger.tagged("billing", () => {
48
+ logger.info("invoice created");
49
+ });
50
+
51
+ const billing = logger.tagged("billing");
52
+ billing.info("sticky tags on child logger");
53
+
54
+ await withContext({ requestId: "abc" }, async () => {
55
+ logger.info("handled request");
56
+ });
57
+ ```
58
+
59
+ Each line looks like:
60
+
61
+ ```json
62
+ {"message":"server started","severity":"INFO","timestamp":"2026-10-26T05:30:00.000000Z"}
63
+ ```
64
+
65
+ Object fields merge at the root. Tags appear as `tags`. Request fields appear under `context`. System keys (`tags`, `severity`, `timestamp`, `message`, `context`) are reserved.
66
+
67
+ Optional hot-path helpers (RFC-0002):
68
+
69
+ ```ts
70
+ const logger = createLogger({
71
+ level: "info",
72
+ redact: ["user.password", "headers.authorization"],
73
+ serializers: {
74
+ err: (value) => value, // root-field transform
75
+ },
76
+ });
77
+
78
+ if (logger.isLevelEnabled("debug")) {
79
+ logger.debug({ detail: "expensive" });
80
+ }
81
+ ```
82
+
83
+ ## Sinks
84
+
85
+ Console is the default. Pass a custom sink or a `WritableStream`:
86
+
87
+ ```ts
88
+ import { createLogger, consoleSink, streamSink } from "@amkisko/json-logging";
89
+
90
+ createLogger({ sink: consoleSink() });
91
+ createLogger({ sink: streamSink(myWritableStream) });
92
+ createLogger({
93
+ sink: {
94
+ write(line, record) {
95
+ // forward somewhere else
96
+ },
97
+ },
98
+ });
99
+ ```
100
+
101
+ ## Capture console and app logs (opt-in)
102
+
103
+ `JsonLogging.new()` alone does not rewrite `console` or framework logs. For app code plus `console.*`, use the opt-in bridge:
104
+
105
+ ```ts
106
+ import { installStructuredLogging } from "@amkisko/json-logging/console";
107
+
108
+ const { logger, uninstall } = installStructuredLogging({
109
+ base: { service: "api" },
110
+ });
111
+
112
+ logger.info("structured");
113
+ console.log("also structured"); // routed through the same logger
114
+ ```
115
+
116
+ Or attach a bridge to an existing logger:
117
+
118
+ ```ts
119
+ import { createLogger } from "@amkisko/json-logging";
120
+ import { installConsoleBridge } from "@amkisko/json-logging/console";
121
+
122
+ const logger = createLogger({ base: { service: "api" } });
123
+ const uninstall = installConsoleBridge(logger);
124
+ ```
125
+
126
+ Runnable patterns live under `examples/`:
127
+
128
+ - `examples/bun-entry.ts`
129
+ - `examples/next-instrumentation.ts`
130
+ - `examples/remix-handle-request.ts`
131
+
132
+ ### What becomes structured
133
+
134
+ - Calls on the package logger (`info`, `tagged`, `withContext`, ...)
135
+ - `console.log` / `info` / `warn` / `error` / `debug` after `installConsoleBridge` or `installStructuredLogging`
136
+ - Code that already logs through those console methods
137
+
138
+ ### What does not
139
+
140
+ - Next.js / Remix / Vite / Bun CLI and framework internals
141
+ - Browser DevTools console
142
+ - Libraries that write straight to `process.stdout` / `stderr` or their own transports (Pino destinations, Winston file transports, etc.) without going through `console`
143
+ - Middleware ALS context continuing into a later Node App Router handler (forward `x-request-id` and call `withContext` again)
144
+
145
+ ## HTTP adapters
146
+
147
+ Shared helpers live under `@amkisko/json-logging/http`. Framework subpaths are duck-typed and add no runtime dependencies (RFC-0003):
148
+
149
+ - `@amkisko/json-logging/next`
150
+ - `@amkisko/json-logging/remix`
151
+ - `@amkisko/json-logging/hono`
152
+ - `@amkisko/json-logging/express`
153
+ - `@amkisko/json-logging/fastify` (route handler wrap)
154
+ - `@amkisko/json-logging/koa`
155
+ - `@amkisko/json-logging/bun`
156
+
157
+ CI runs end-to-end adapter checks under `tests/e2e/` against real packages as development dependencies: Hono (`app.request`), Express and Koa (listen + `fetch`), Fastify (`inject`), `Bun.serve`, Next.js (`NextRequest` from `next/server` through `withRequestLogging`), Remix (`createRequestHandler` from `@remix-run/node` with a loader under `withRemixContext`), and NestJS composition (Express `app.use(withExpressLogging)` and Fastify `withHttpContext` interceptor). CI also runs a Node runtime smoke (`make node-smoke`) and an Edge import-graph check (`make edge-import`) so Edge-oriented entrypoints do not statically import forbidden Node builtins. CI does not boot a full Next, Remix, or Nest production app process; it uses the same request and handler APIs those apps call.
158
+
159
+ ```ts
160
+ import { bindHttpLogger, httpLogger } from "@amkisko/json-logging/http";
161
+ import { withHonoLogging } from "@amkisko/json-logging/hono";
162
+
163
+ bindHttpLogger(httpLogger({ loggerOptions: { base: { service: "api" } } }));
164
+ app.use(withHonoLogging());
165
+ ```
166
+
167
+ There is no dedicated `./nest` or `./sails` export. NestJS on the Express platform uses `app.use(withExpressLogging(...))`. NestJS on Fastify uses a request interceptor (or equivalent) that calls `withHttpContext` from `./http` around the handler. Sails.js sits on Express: mount `withExpressLogging` on the underlying Express app (for example via custom middleware / `sails.hooks.http.app`).
168
+
169
+ Ambient logger helpers live only on `./http` so framework entrypoints stay slim. Run microbenchmarks with `make bench` (ceilings in `usr/budgets/bench.json`).
170
+
171
+ ## Next.js
172
+
173
+ Boot once (for example from `instrumentation.ts`), then wrap middleware:
174
+
175
+ ```ts
176
+ import { installStructuredLogging } from "@amkisko/json-logging/console";
177
+ import { bindHttpLogger, httpLogger } from "@amkisko/json-logging/http";
178
+ import { withRequestLogging } from "@amkisko/json-logging/next";
179
+
180
+ const { logger } = installStructuredLogging({ base: { service: "next-app" } });
181
+ bindHttpLogger(logger);
182
+
183
+ export default withRequestLogging(async (req) => {
184
+ httpLogger().info({ event: "middleware", path: req.nextUrl?.pathname });
185
+ // return NextResponse.next()
186
+ });
187
+ ```
188
+
189
+ See `examples/next-instrumentation.ts`.
190
+
191
+ ## Remix
192
+
193
+ ```ts
194
+ import { installStructuredLogging } from "@amkisko/json-logging/console";
195
+ import { bindHttpLogger, httpLogger } from "@amkisko/json-logging/http";
196
+ import { withRemixContext } from "@amkisko/json-logging/remix";
197
+
198
+ const { logger } = installStructuredLogging({ base: { service: "remix-app" } });
199
+ bindHttpLogger(logger);
200
+
201
+ return withRemixContext({ request, context }, () => {
202
+ httpLogger().info("loading");
203
+ return handle(request);
204
+ });
205
+ ```
206
+
207
+ See `examples/remix-handle-request.ts`.
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ bun install
213
+ make check
214
+ ```
215
+
216
+ `make check` runs Biome (`biome.json`), prose checks, typecheck, type-coverage (100% strict), tests with coverage thresholds at 100%, mutation checks, dependency audit, LOC gate, bundle size budget, example smoke, Edge import-graph check, and Node runtime smoke. `make ci` also builds `dist/` and runs `make bench` (ceilings in `usr/budgets/bench.json`).
217
+
218
+ Release:
219
+
220
+ ```bash
221
+ make release
222
+ make release DRY_RUN=1
223
+ make release VERSION=0.1.0
224
+ ```
225
+
226
+ `make release` reads the first `## X.Y.Z` heading in `CHANGELOG.md`, runs quality gates, publishes to npmjs, pushes a version tag, and leaves JSR publishing to GitHub Actions (OIDC provenance). Optional `VERSION=` overrides the changelog heading. Keep `package.json` and `jsr.json` version fields in sync before release. Create the `@amkisko` scope and `json-logging` package on [jsr.io](https://jsr.io/new), then link this GitHub repository in the JSR package settings so tag publishes can authenticate.
227
+
228
+ Quality scripts live under `usr/bin/` (same layout as other amkisko libraries). Bundle ceilings are in `usr/budgets/bundle.json`. Product-facing notes live in `CHANGELOG.md`; engineering drafts under `docs/changelogs/`.
229
+
230
+ ## License
231
+
232
+ MIT (see `LICENSE.md`)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bun.serve fetch-handler wrap for request-scoped log context (and optional completion).
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { withBunLogging } from "@amkisko/json-logging/bun";
7
+ * Bun.serve({ fetch: withBunLogging(async (req) => new Response("ok")) });
8
+ * ```
9
+ *
10
+ * @module
11
+ */
12
+ import type { Logger } from "./logger.js";
13
+ import type { LoggerOptions } from "./types.js";
14
+ /** Minimal fetch Request shape used with Bun.serve. */
15
+ export type BunFetchRequest = {
16
+ method: string;
17
+ url: string;
18
+ headers: Headers;
19
+ };
20
+ /** Options for {@link withBunLogging}. */
21
+ export type WithBunLoggingOptions = {
22
+ headerName?: string;
23
+ logger?: Logger;
24
+ loggerOptions?: LoggerOptions;
25
+ headersAllowlist?: readonly string[];
26
+ logCompletion?: boolean;
27
+ };
28
+ /** Wrap a `Bun.serve` fetch handler. Bind loggers via `./http`. */
29
+ export declare function withBunLogging<TArgs extends unknown[], TResult>(handler: (request: BunFetchRequest, ...rest: TArgs) => TResult | Promise<TResult>, options?: WithBunLoggingOptions): (request: BunFetchRequest, ...rest: TArgs) => Promise<TResult>;
30
+ //# sourceMappingURL=bun-serve.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Bun.serve fetch-handler wrap for request-scoped log context (and optional completion).
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { withBunLogging } from "@amkisko/json-logging/bun";
7
+ * Bun.serve({ fetch: withBunLogging(async (req) => new Response("ok")) });
8
+ * ```
9
+ *
10
+ * @module
11
+ */
12
+ import { bindLoggerFromOptions, getAmbientHttpLogger } from "./http-ambient.js";
13
+ import { httpFieldsFrom, logRequestCompletion, withHttpContext, } from "./http-context.js";
14
+ function responseStatus(value) {
15
+ if (typeof value === "object" &&
16
+ value !== null &&
17
+ typeof Reflect.get(value, "status") === "number") {
18
+ return Reflect.get(value, "status");
19
+ }
20
+ return 200;
21
+ }
22
+ /** Wrap a `Bun.serve` fetch handler. Bind loggers via `./http`. */
23
+ export function withBunLogging(handler, options = {}) {
24
+ bindLoggerFromOptions(options);
25
+ const headerName = options.headerName ?? "x-request-id";
26
+ return async (request, ...rest) => {
27
+ const input = {
28
+ method: request.method,
29
+ url: request.url,
30
+ headers: request.headers,
31
+ };
32
+ const started = Date.now();
33
+ const fields = httpFieldsFrom(input, headerName);
34
+ let error;
35
+ let result;
36
+ try {
37
+ result = (await withHttpContext(input, () => handler(request, ...rest), options));
38
+ return result;
39
+ }
40
+ catch (caught) {
41
+ error = caught;
42
+ throw caught;
43
+ }
44
+ finally {
45
+ if (options.logCompletion) {
46
+ const logger = options.logger ?? getAmbientHttpLogger();
47
+ if (logger) {
48
+ logRequestCompletion(logger, {
49
+ method: typeof fields.method === "string" ? fields.method : undefined,
50
+ path: typeof fields.path === "string" ? fields.path : undefined,
51
+ status: error !== undefined ? 500 : responseStatus(result),
52
+ elapsedMs: Date.now() - started,
53
+ error,
54
+ });
55
+ }
56
+ }
57
+ }
58
+ };
59
+ }
60
+ //# sourceMappingURL=bun-serve.js.map
@@ -0,0 +1,26 @@
1
+ import { type ConsoleMethodName, type ConsoleMethods } from "./console-state.js";
2
+ import type { Logger } from "./logger.js";
3
+ import type { LoggerOptions, LogMessage } from "./types.js";
4
+ export type ConsoleBridgeOptions = {
5
+ methods?: readonly ConsoleMethodName[];
6
+ };
7
+ export type StructuredLoggingInstall = {
8
+ logger: Logger;
9
+ uninstall: () => void;
10
+ };
11
+ export declare function isConsoleBridgeInstalled(): boolean;
12
+ export declare function uninstallConsoleBridge(): void;
13
+ /**
14
+ * Routes console.log/info/warn/error/debug through `logger` as structured JSON.
15
+ * Opt-in only. Does not capture framework internals or libraries that bypass console.
16
+ * Safe with the default console sink: writes use the pre-patch console methods.
17
+ */
18
+ export declare function installConsoleBridge(logger: Logger, options?: ConsoleBridgeOptions): () => void;
19
+ /**
20
+ * One-call setup: logger that writes via captured console methods, plus console bridge.
21
+ * Prefer this when the goal is "app + console.* become structured JSON lines".
22
+ */
23
+ export declare function installStructuredLogging(options?: LoggerOptions & ConsoleBridgeOptions): StructuredLoggingInstall;
24
+ export declare function messageFromConsoleArgs(args: unknown[]): LogMessage;
25
+ export type { ConsoleMethods, ConsoleMethodName };
26
+ //# sourceMappingURL=console-bridge.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { captureConsoleMethods, enterBridge, getBridgeOriginals, isInBridge, leaveBridge, setBridgeOriginals, } from "./console-state.js";
2
+ import { createLogger } from "./json-logging.js";
3
+ import { isPlainObject } from "./objects.js";
4
+ import { consoleSinkFrom } from "./sinks.js";
5
+ const DEFAULT_METHODS = ["log", "info", "warn", "error", "debug"];
6
+ const LEVEL_BY_METHOD = {
7
+ log: "info",
8
+ info: "info",
9
+ warn: "warn",
10
+ error: "error",
11
+ debug: "debug",
12
+ };
13
+ let installedMethods = null;
14
+ export function isConsoleBridgeInstalled() {
15
+ return getBridgeOriginals() !== null;
16
+ }
17
+ export function uninstallConsoleBridge() {
18
+ const originals = getBridgeOriginals();
19
+ if (!originals || !installedMethods) {
20
+ setBridgeOriginals(null);
21
+ installedMethods = null;
22
+ return;
23
+ }
24
+ for (const name of installedMethods) {
25
+ Reflect.set(console, name, originals[name]);
26
+ }
27
+ setBridgeOriginals(null);
28
+ installedMethods = null;
29
+ }
30
+ /**
31
+ * Routes console.log/info/warn/error/debug through `logger` as structured JSON.
32
+ * Opt-in only. Does not capture framework internals or libraries that bypass console.
33
+ * Safe with the default console sink: writes use the pre-patch console methods.
34
+ */
35
+ export function installConsoleBridge(logger, options = {}) {
36
+ uninstallConsoleBridge();
37
+ const originals = captureConsoleMethods(console);
38
+ setBridgeOriginals(originals);
39
+ const methods = options.methods ?? DEFAULT_METHODS;
40
+ installedMethods = [...methods];
41
+ for (const name of methods) {
42
+ const level = LEVEL_BY_METHOD[name];
43
+ Reflect.set(console, name, (...args) => {
44
+ if (isInBridge()) {
45
+ originals[name](...args);
46
+ return;
47
+ }
48
+ enterBridge();
49
+ try {
50
+ writeThroughLogger(logger, level, args);
51
+ }
52
+ finally {
53
+ leaveBridge();
54
+ }
55
+ });
56
+ }
57
+ return uninstallConsoleBridge;
58
+ }
59
+ /**
60
+ * One-call setup: logger that writes via captured console methods, plus console bridge.
61
+ * Prefer this when the goal is "app + console.* become structured JSON lines".
62
+ */
63
+ export function installStructuredLogging(options = {}) {
64
+ const { methods, ...loggerOptions } = options;
65
+ // Restore real console methods before capture so a re-install does not
66
+ // bind the sink to an already patched console.
67
+ uninstallConsoleBridge();
68
+ const originals = captureConsoleMethods(console);
69
+ const logger = createLogger({
70
+ ...loggerOptions,
71
+ sink: loggerOptions.sink ?? consoleSinkFrom(originals),
72
+ });
73
+ const uninstall = installConsoleBridge(logger, methods ? { methods } : {});
74
+ return { logger, uninstall };
75
+ }
76
+ export function messageFromConsoleArgs(args) {
77
+ if (args.length === 0) {
78
+ return "";
79
+ }
80
+ if (args.length === 1) {
81
+ return singleArgMessage(args[0]);
82
+ }
83
+ return {
84
+ message: args.map(stringifyArg).join(" "),
85
+ console_args: args.map(snapshotArg),
86
+ };
87
+ }
88
+ function writeThroughLogger(logger, level, args) {
89
+ const message = messageFromConsoleArgs(args);
90
+ switch (level) {
91
+ case "debug":
92
+ logger.debug(message);
93
+ break;
94
+ case "warn":
95
+ logger.warn(message);
96
+ break;
97
+ case "error":
98
+ case "fatal":
99
+ logger.error(message);
100
+ break;
101
+ default:
102
+ logger.info(message);
103
+ }
104
+ }
105
+ function singleArgMessage(value) {
106
+ if (typeof value === "string") {
107
+ return value;
108
+ }
109
+ if (value instanceof Error) {
110
+ return value;
111
+ }
112
+ if (isPlainObject(value)) {
113
+ return value;
114
+ }
115
+ return stringifyArg(value);
116
+ }
117
+ function stringifyArg(value) {
118
+ if (typeof value === "string") {
119
+ return value;
120
+ }
121
+ if (value instanceof Error) {
122
+ return value.message;
123
+ }
124
+ if (typeof value === "number" || typeof value === "boolean") {
125
+ return String(value);
126
+ }
127
+ if (value === null) {
128
+ return "null";
129
+ }
130
+ if (value === undefined) {
131
+ return "undefined";
132
+ }
133
+ try {
134
+ return JSON.stringify(value) ?? String(value);
135
+ }
136
+ catch {
137
+ return "[unstringifiable]";
138
+ }
139
+ }
140
+ function snapshotArg(value) {
141
+ if (typeof value === "string" ||
142
+ typeof value === "number" ||
143
+ typeof value === "boolean" ||
144
+ value === null) {
145
+ return value;
146
+ }
147
+ if (value instanceof Error) {
148
+ return { name: value.name, message: value.message };
149
+ }
150
+ if (isPlainObject(value)) {
151
+ return value;
152
+ }
153
+ return stringifyArg(value);
154
+ }
155
+ //# sourceMappingURL=console-bridge.js.map
@@ -0,0 +1,11 @@
1
+ export type ConsoleMethodName = "log" | "info" | "warn" | "error" | "debug";
2
+ export type ConsoleMethods = {
3
+ [K in ConsoleMethodName]: (...args: unknown[]) => void;
4
+ };
5
+ export declare function setBridgeOriginals(methods: ConsoleMethods | null): void;
6
+ export declare function getBridgeOriginals(): ConsoleMethods | null;
7
+ export declare function enterBridge(): void;
8
+ export declare function leaveBridge(): void;
9
+ export declare function isInBridge(): boolean;
10
+ export declare function captureConsoleMethods(target?: Partial<ConsoleMethods> & object): ConsoleMethods;
11
+ //# sourceMappingURL=console-state.d.ts.map
@@ -0,0 +1,40 @@
1
+ let bridgeOriginals = null;
2
+ let bridgeDepth = 0;
3
+ export function setBridgeOriginals(methods) {
4
+ bridgeOriginals = methods;
5
+ }
6
+ export function getBridgeOriginals() {
7
+ return bridgeOriginals;
8
+ }
9
+ export function enterBridge() {
10
+ bridgeDepth += 1;
11
+ }
12
+ export function leaveBridge() {
13
+ if (bridgeDepth > 0) {
14
+ bridgeDepth -= 1;
15
+ }
16
+ }
17
+ export function isInBridge() {
18
+ return bridgeDepth > 0;
19
+ }
20
+ export function captureConsoleMethods(target = console) {
21
+ return {
22
+ log: bindMethod(target, "log"),
23
+ info: bindMethod(target, "info"),
24
+ warn: bindMethod(target, "warn"),
25
+ error: bindMethod(target, "error"),
26
+ debug: bindMethod(target, "debug"),
27
+ };
28
+ }
29
+ function bindMethod(target, name) {
30
+ const method = Reflect.get(target, name);
31
+ if (typeof method === "function") {
32
+ return (...args) => {
33
+ Reflect.apply(method, target, args);
34
+ };
35
+ }
36
+ return (...args) => {
37
+ Reflect.apply(console.log, console, args);
38
+ };
39
+ }
40
+ //# sourceMappingURL=console-state.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Opt-in console bridge: route `console.log` / `info` / `warn` / `error` / `debug`
3
+ * through a structured {@link Logger}.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { installStructuredLogging } from "@amkisko/json-logging/console";
8
+ *
9
+ * const { logger, uninstall } = installStructuredLogging({ base: { service: "api" } });
10
+ * console.log("also structured");
11
+ * uninstall();
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ export type { ConsoleBridgeOptions, StructuredLoggingInstall, } from "./console-bridge.js";
17
+ export { installConsoleBridge, installStructuredLogging, isConsoleBridgeInstalled, messageFromConsoleArgs, uninstallConsoleBridge, } from "./console-bridge.js";
18
+ //# sourceMappingURL=console.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Opt-in console bridge: route `console.log` / `info` / `warn` / `error` / `debug`
3
+ * through a structured {@link Logger}.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { installStructuredLogging } from "@amkisko/json-logging/console";
8
+ *
9
+ * const { logger, uninstall } = installStructuredLogging({ base: { service: "api" } });
10
+ * console.log("also structured");
11
+ * uninstall();
12
+ * ```
13
+ *
14
+ * @module
15
+ */
16
+ export { installConsoleBridge, installStructuredLogging, isConsoleBridgeInstalled, messageFromConsoleArgs, uninstallConsoleBridge, } from "./console-bridge.js";
17
+ //# sourceMappingURL=console.js.map
@@ -0,0 +1,40 @@
1
+ /** Key/value fields stored in the ambient request context. */
2
+ export type ContextFields = Record<string, unknown>;
3
+ type Store = {
4
+ context: ContextFields;
5
+ tags: string[];
6
+ };
7
+ /** Minimal ALS surface used for request/block context (run + getStore only). */
8
+ export type AlsInstance<T> = {
9
+ run<R>(store: T, callback: () => R): R;
10
+ getStore(): T | undefined;
11
+ };
12
+ /** Constructor for {@link AlsInstance}, matching `node:async_hooks`.AsyncLocalStorage. */
13
+ export type AlsConstructor = new <T>() => AlsInstance<T>;
14
+ /** True when this runtime can construct AsyncLocalStorage for ambient context. */
15
+ export declare function isAsyncLocalStorageAvailable(): boolean;
16
+ /** Test helper: force ALS constructor resolution (`null` = unavailable). */
17
+ export declare function setAlsConstructorForTests(value: AlsConstructor | null | undefined): void;
18
+ /** Test helper: pass null to simulate a runtime without AsyncLocalStorage. */
19
+ export declare function setAlsOverrideForTests(value: AlsInstance<Store> | null | undefined): void;
20
+ export declare function readStore(als: AlsInstance<Store> | undefined): Store | undefined;
21
+ /** Copy of the current ambient context fields, or undefined outside a scope. */
22
+ export declare function getContext(): ContextFields | undefined;
23
+ /** Copy of nested `tagged` block tags for the current ALS scope. */
24
+ export declare function getBlockTags(): string[];
25
+ /** One store read for the log hot path. Returns live references; do not mutate. */
26
+ export declare function peekLogStore(): {
27
+ context: ContextFields | undefined;
28
+ tags: readonly string[];
29
+ };
30
+ /**
31
+ * Run `fn` with ambient context fields merged for the duration of the call.
32
+ * Without AsyncLocalStorage the callback still runs; context is not retained.
33
+ */
34
+ export declare function withContext<T>(fields: ContextFields, fn: () => T | Promise<T>): T | Promise<T>;
35
+ /** Nested tag scope used by {@link Logger.tagged} block form. */
36
+ export declare function withBlockTags<T>(tags: readonly string[], fn: () => T | Promise<T>): T | Promise<T>;
37
+ /** Test helper: clear the global ALS singleton between suites. */
38
+ export declare function resetContextStorageForTests(): void;
39
+ export {};
40
+ //# sourceMappingURL=context.d.ts.map