@tanngo11/log 0.3.0 → 0.4.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ Fixed:
6
+ - `withLogging` now returns a function with exactly the handler's parameters (`()`, `(request)`
7
+ or `(request, { params })`), so Next.js route type checks accept the wrapped export. When it is
8
+ called without a request (a direct call in a unit test), it runs the handler without logging.
9
+
10
+ ## 0.4.0
11
+
12
+ Added:
13
+ - `@tanngo11/log/strapi` for Strapi 5:
14
+ - `strapiLoggerConfig(log)` for `config/logger.ts`, so `strapi.log.*` becomes contract JSON;
15
+ - `strapiRequestLogger`, which replaces `strapi::logger` and writes one `http.request` line per
16
+ request;
17
+ - `strapiErrorCapture`, which makes an unhandled error appear once instead of twice.
18
+ - `upstream_request_id`: the caller's request id when the service issues its own id.
19
+
3
20
  ## 0.3.0
4
21
 
5
22
  Added:
package/CONTRACT.md CHANGED
@@ -30,7 +30,7 @@ stream, nested fields flattened with `_`) → MCP for AI agents.
30
30
  | Group | Fields |
31
31
  |---|---|
32
32
  | Core | `level` (`trace\|debug\|info\|warn\|error\|fatal`), `message`, `event` (required for warn/error/fatal) |
33
- | Correlation | `request_id`, `trace_id`, `span_id`, `job_name`, `job_id`, `job_attempt`, `user_id` (internal id, never an email), `tenant_id` |
33
+ | Correlation | `request_id`, `upstream_request_id` (the caller's id when this service issues its own), `trace_id`, `span_id`, `job_name`, `job_id`, `job_attempt`, `user_id` (internal id, never an email), `tenant_id` |
34
34
  | Inbound HTTP | `http_method`, `http_route` (template, e.g. `/orders/:id`), `http_path` (no query string), `http_status`, `duration_ms` |
35
35
  | Outbound HTTP | `peer_service`, `http_method`, `http_url` (no query string or credentials), `http_status`, `duration_ms` |
36
36
  | Error | `err_type`, `err_message`, `err_code` (string), `err_stack`, `err_cause` (cause chain joined with ` <- `, max 5) |
@@ -88,6 +88,7 @@ System events shared by all services:
88
88
  | `action.completed` | info | a server action finished, including a redirect or not-found (with `action_name`, `duration_ms`) |
89
89
  | `action.failed` | error | a server action threw |
90
90
  | `console` | by console method | a legacy `console.*` call (with `console_method`) |
91
+ | `strapi.log` | warn/error | a framework or app `strapi.log` warn/error call that carries no `event` of its own |
91
92
 
92
93
  ## 5. What to log
93
94
 
package/README.md CHANGED
@@ -256,6 +256,55 @@ new Worker("invoices", loggedProcessor(log, "send_invoice_email", async (job) =>
256
256
  - When enqueueing from a request, use `queue.add(name, jobData(data))`. The job's logs then carry
257
257
  that request's `request_id`.
258
258
 
259
+ ## Strapi 5
260
+
261
+ `config/logger.ts` routes every `strapi.log.*` call (app code and framework) through the logger:
262
+
263
+ ```ts
264
+ import { strapiLoggerConfig } from "@tanngo11/log/strapi";
265
+ import { log } from "../src/shared/log";
266
+
267
+ export default strapiLoggerConfig(log);
268
+ ```
269
+
270
+ `config/middlewares.ts` replaces `'strapi::logger'` and adds the error capture right after
271
+ `'strapi::errors'`:
272
+
273
+ ```ts
274
+ // src/middlewares/http-log/index.ts
275
+ import { strapiRequestLogger } from "@tanngo11/log/strapi";
276
+ import { log } from "../../shared/log";
277
+ export default () => strapiRequestLogger({ log });
278
+
279
+ // src/middlewares/error-capture/index.ts
280
+ import { strapiErrorCapture } from "@tanngo11/log/strapi";
281
+ export default () => strapiErrorCapture();
282
+
283
+ // config/middlewares.ts
284
+ return [
285
+ { resolve: "./src/middlewares/http-log" }, // instead of 'strapi::logger', first in the list
286
+ "strapi::errors",
287
+ { resolve: "./src/middlewares/error-capture" }, // right after strapi::errors
288
+ // ...
289
+ ];
290
+ ```
291
+
292
+ - Each request gets one `http.request` line with the full route template (`/api/articles/:id`,
293
+ read from `@koa/router`), its status and duration.
294
+ - An unhandled error is written once, on that line. The copy that `strapi::errors` logs through
295
+ `strapi.log.error` is not written again.
296
+ - Errors the app logs itself through `strapi.log.error(err)` stay as their own lines, with
297
+ `event: "strapi.log"`.
298
+ - `request_id` reuses `ctx.state.requestId` when the app sets one (configurable with `stateKey`).
299
+ Otherwise a UUID is put there, so the app's own correlation middleware keeps the same id.
300
+ - The caller's `x-request-id` (for example from a Next.js BFF) is not trusted as the request id.
301
+ It is recorded as `upstream_request_id`, which links both services' logs. Set
302
+ `trustIncomingRequestId: true` to adopt it instead.
303
+ - Winston levels are mapped: `http` and `verbose` become `debug`, `silly` becomes `trace`.
304
+ `LOG_LEVEL` filters.
305
+
306
+ Tested against the versions Strapi 5.51 uses: Koa 2, `@koa/router` 12 and winston 3.10.
307
+
259
308
  ## Prisma (5 and 6)
260
309
 
261
310
  ```ts
package/dist/next.cjs CHANGED
@@ -320,7 +320,9 @@ function trySetHeader(res, name, value) {
320
320
  }
321
321
  }
322
322
  function withLogging(log, handler, opts = {}) {
323
- return (request, context) => {
323
+ return async (...args) => {
324
+ const request = args[0];
325
+ if (!(request instanceof Request)) return handler(...args);
324
326
  const ctx = requestContextFields(request.headers);
325
327
  const path = new URL(request.url).pathname;
326
328
  const start = performance.now();
@@ -339,7 +341,7 @@ function withLogging(log, handler, opts = {}) {
339
341
  opts
340
342
  );
341
343
  try {
342
- const res = await handler(request, context);
344
+ const res = await handler(...args);
343
345
  summary(res.status, store.error);
344
346
  trySetHeader(res, "x-request-id", ctx.request_id);
345
347
  return res;
package/dist/next.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts","../src/context.ts","../src/global.ts","../src/redact.ts","../src/error.ts","../src/normalize.ts","../src/request-id.ts","../src/http.ts","../src/process.ts"],"sourcesContent":["import { runWithStore } from \"./context\";\nimport { isLogged, markLogged } from \"./error\";\nimport { logHttpRequest, requestContextFields, type HeaderSource, type HttpLogOptions } from \"./http\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\nimport { installProcessHandlers, logStartup } from \"./process\";\n\nexport interface WithLoggingOptions extends HttpLogOptions {\n /** Route template, e.g. \"/api/orders/[id]\". Left empty when not given. */\n route?: string;\n}\n\n/** Status for Next's control-flow errors (redirect(), notFound(), forbidden()...), else undefined. */\nexport function nextControlStatus(err: unknown): number | undefined {\n const digest = (err as { digest?: unknown } | null)?.digest;\n if (typeof digest !== \"string\") return undefined;\n if (digest.startsWith(\"NEXT_REDIRECT\")) {\n const status = Number(digest.split(\";\")[3]);\n return Number.isInteger(status) && status >= 300 ? status : 307;\n }\n if (digest === \"NEXT_NOT_FOUND\") return 404;\n if (digest.startsWith(\"NEXT_HTTP_ERROR_FALLBACK;\")) return Number(digest.split(\";\")[1]) || 404;\n return undefined;\n}\n\nfunction asError(value: unknown): unknown {\n if (typeof value === \"object\" && value !== null) return value;\n return Object.assign(new Error(String(value)), { name: \"NonErrorThrown\", thrown: value });\n}\n\nfunction trySetHeader(res: Response, name: string, value: string): void {\n try {\n res.headers.set(name, value);\n } catch {\n // Some responses (Response.redirect, proxied fetch responses) have immutable headers.\n }\n}\n\n/** Wraps an App Router route handler: request context + one http.request line. */\nexport function withLogging<R extends Request, C>(\n log: Logger,\n handler: (request: R, context: C) => Response | Promise<Response>,\n opts: WithLoggingOptions = {},\n): (request: R, context: C) => Promise<Response> {\n return (request, context) => {\n const ctx = requestContextFields(request.headers);\n const path = new URL(request.url).pathname;\n const start = performance.now();\n return runWithStore(ctx, async (store) => {\n const summary = (status: number, err: unknown) =>\n logHttpRequest(\n log,\n {\n method: request.method,\n route: opts.route,\n path,\n status,\n duration_ms: performance.now() - start,\n err,\n fields: store.fields,\n },\n opts,\n );\n try {\n const res = await handler(request, context);\n summary(res.status, store.error);\n trySetHeader(res, \"x-request-id\", ctx.request_id);\n return res;\n } catch (caught) {\n const control = nextControlStatus(caught);\n // Non-objects cannot be marked as logged, so onRequestError would log them again.\n const err = control === undefined ? asError(caught) : caught;\n summary(control ?? 500, control === undefined ? err : store.error);\n throw err;\n }\n });\n };\n}\n\nexport interface NextRequestInfo {\n path?: string;\n method?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\nexport interface NextErrorContext {\n routerKind?: string;\n routePath?: string;\n routeType?: string;\n}\n\n/** `export const onRequestError = nextOnRequestError(log)` in instrumentation.ts. */\nexport function nextOnRequestError(log: Logger, opts: HttpLogOptions = {}) {\n return async (err: unknown, request: NextRequestInfo, context: NextErrorContext): Promise<void> => {\n if (isLogged(err) || nextControlStatus(err) !== undefined) return;\n const digest = (err as { digest?: unknown } | null)?.digest;\n logHttpRequest(\n log,\n {\n method: request?.method ?? \"GET\",\n route: context?.routePath,\n path: request?.path ?? \"\",\n status: 500,\n err,\n fields: {\n ...requestContextFields(request?.headers ?? {}),\n next_route_type: context?.routeType,\n next_router_kind: context?.routerKind,\n next_digest: typeof digest === \"string\" ? digest : undefined,\n },\n },\n opts,\n );\n };\n}\n\n/** Call from `register()` in instrumentation.ts. Does nothing on the edge runtime. */\nexport function registerNext(log: Logger, fields?: Fields): void {\n if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== \"nodejs\") return;\n // Next keeps serving after an unhandled rejection, so do not exit.\n installProcessHandlers(log, { exitOnCrash: false });\n logStartup(log, { port: process.env.PORT ? Number(process.env.PORT) : undefined, ...fields });\n}\n\nexport interface WithActionOptions {\n /** Request headers source. Defaults to `headers()` from next/headers. */\n headers?: () => HeaderSource | Promise<HeaderSource>;\n}\n\nasync function nextHeaders(): Promise<HeaderSource> {\n try {\n const mod = (await import(\"next/headers\")) as { headers: () => HeaderSource | Promise<HeaderSource> };\n return await mod.headers();\n } catch {\n // Called outside a request (tests, scripts) or Next is not installed.\n return {};\n }\n}\n\n/**\n * Wraps a server action: request context (request_id from x-request-id) plus one\n * `action.completed` / `action.failed` line. redirect()/notFound() count as completed.\n */\nexport function withAction<A extends unknown[], R>(\n log: Logger,\n name: string,\n action: (...args: A) => Promise<R>,\n opts: WithActionOptions = {},\n): (...args: A) => Promise<R> {\n return async (...args) => {\n const headers = await (opts.headers ?? nextHeaders)();\n const start = performance.now();\n return runWithStore({ ...requestContextFields(headers), action_name: name }, async (store) => {\n const fields = () => ({ ...store.fields, duration_ms: Math.round(performance.now() - start) });\n try {\n const result = await action(...args);\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", err: store.error });\n return result;\n } catch (caught) {\n const control = nextControlStatus(caught);\n if (control !== undefined) {\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", next_control_status: control });\n throw caught;\n }\n const err = asError(caught);\n log.error(\"server action failed\", { ...fields(), event: \"action.failed\", err });\n markLogged(err);\n throw err;\n }\n });\n };\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { globalSingleton } from \"./global\";\nimport type { Fields } from \"./normalize\";\n\nexport interface ContextStore {\n fields: Fields;\n error?: unknown;\n /** Route template captured while the router still knew it (Express error path). */\n route?: string;\n parent?: ContextStore;\n}\n\nconst als = globalSingleton(\"als\", () => new AsyncLocalStorage<ContextStore>());\n\nexport const STORE_KEY: symbol = Symbol.for(\"@tanngo11/log.store\");\n\nexport function runWithStore<T>(fields: Fields, fn: (store: ContextStore) => T): T {\n const parent = als.getStore();\n const store: ContextStore = { fields: { ...parent?.fields, ...fields }, parent };\n return als.run(store, () => fn(store));\n}\n\nexport function withContext<T>(fields: Fields, fn: () => T): T {\n return runWithStore(fields, () => fn());\n}\n\nexport function currentStore(): ContextStore | undefined {\n return als.getStore();\n}\n\nexport function getContext(): Fields {\n return als.getStore()?.fields ?? {};\n}\n\n// Applies to the current context and every enclosing one, so a field added inside a nested\n// withContext/runJob still reaches the request's http.request line.\nexport function addContext(fields: Fields): void {\n for (let s = als.getStore(); s; s = s.parent) Object.assign(s.fields, fields);\n}\n\n// Sets the error on the store and on enclosing stores that have none yet.\nexport function recordErrorIn(store: ContextStore, err: unknown): void {\n store.error = err;\n for (let s = store.parent; s; s = s.parent) if (s.error === undefined) s.error = err;\n}\n\n// Marks the error that the current request/job summary line should carry. Does not write a line.\nexport function recordError(err: unknown): void {\n const store = als.getStore();\n if (store) recordErrorIn(store, err);\n}\n\nexport function attachStore(target: object, store: ContextStore): void {\n (target as Record<symbol, ContextStore>)[STORE_KEY] = store;\n}\n\nexport function storeOf(target: unknown): ContextStore | undefined {\n if (typeof target !== \"object\" || target === null) return undefined;\n return (target as Record<symbol, ContextStore | undefined>)[STORE_KEY];\n}\n","// Singletons shared by every copy of this package loaded in the process (ESM and CJS builds,\n// or duplicated installs), so context and dedupe state never split.\nexport function globalSingleton<T>(name: string, make: () => T): T {\n const key = Symbol.for(`@tanngo11/log.${name}`);\n const g = globalThis as unknown as Record<symbol, T | undefined>;\n let value = g[key];\n if (value === undefined) {\n value = make();\n g[key] = value;\n }\n return value;\n}\n","// Redaction shared by the logger and the error serializer. The collector redacts again,\n// but it cannot see secrets inside JSON-encoded strings, so the app side must catch them.\n\nexport const DEFAULT_REDACT_KEYS = [\n \"password\", \"passwd\", \"pwd\", \"passphrase\", \"secret\", \"client_secret\",\n \"token\", \"access_token\", \"refresh_token\", \"id_token\", \"auth_token\",\n \"api_key\", \"apikey\", \"access_key\", \"secret_key\", \"private_key\",\n \"authorization\", \"cookie\", \"set_cookie\", \"session_id\", \"sessionid\",\n \"credential\", \"credentials\", \"dsn\", \"connection_string\", \"conn_string\", \"jwt\", \"bearer\",\n];\n\n/** Opt-in value patterns for `redactValues`. Not on by default: they can hit ordinary numbers. */\nexport const REDACT_PATTERNS = {\n /** Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. */\n phoneVN: /(?<![\\d+])(?:\\+?84|0)[35789]\\d{8}(?!\\d)/g,\n /** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */\n paymentCard: /(?<!\\d)(?:4\\d{3}|5[1-5]\\d{2}|2[2-7]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))(?:[ -]?\\d{4}){2}[ -]?\\d{1,4}(?!\\d)/g,\n /** IPv4 addresses. */\n ipv4: /(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])/g,\n} as const;\n\n// Keys that only describe a secret (count, type, timestamps, flags) stay readable.\nconst SAFE_SUFFIXES = [\"_count\", \"_type\", \"_length\", \"_expires_at\", \"_expires_in\", \"_ttl\", \"_at\", \"_ms\", \"_enabled\"];\n\nexport const REDACTED = \"[REDACTED]\";\n\nexport function toSnakeCase(key: string): string {\n return key\n .replace(/([a-z0-9])([A-Z])/g, \"$1_$2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1_$2\")\n .replace(/[^A-Za-z0-9_]+/g, \"_\")\n .toLowerCase();\n}\n\nconst URL_CREDENTIALS_RE = /([a-z][a-z0-9+.-]*:\\/\\/)[^\\s/@:\"']+:[^\\s/@\"']+@/gi;\nconst BEARER_RE = /\\bBearer\\s+[A-Za-z0-9._~+/=-]+/gi;\nconst JWT_RE = /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g;\nconst EMAIL_RE = /[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,})/g;\n\nexport type KeyPattern = string | RegExp;\n\nexport interface RedactorOptions {\n /** Extra sensitive keys: names (matched like the defaults) or patterns tested on the snake_case key. */\n keys?: readonly KeyPattern[];\n /** Extra value patterns; every match inside any string becomes [REDACTED]. */\n values?: readonly RegExp[];\n}\n\nexport interface Redactor {\n isSensitive(key: string): boolean;\n /** Masks secrets and emails that appear inside free text. */\n scrub(s: string): string;\n /** JSON.stringify that redacts sensitive keys at any depth and never throws. */\n json(value: unknown): string;\n}\n\nfunction globalize(re: RegExp): RegExp {\n return re.flags.includes(\"g\") ? new RegExp(re.source, re.flags) : new RegExp(re.source, `${re.flags}g`);\n}\n\nexport function createRedactor(opts: RedactorOptions = {}): Redactor {\n const words = [...DEFAULT_REDACT_KEYS];\n const keyPatterns: RegExp[] = [];\n for (const k of opts.keys ?? []) {\n if (typeof k === \"string\") words.push(toSnakeCase(k));\n else keyPatterns.push(new RegExp(k.source, k.flags.replace(\"g\", \"\")));\n }\n const valuePatterns = (opts.values ?? []).map(globalize);\n\n const isSensitive = (key: string): boolean => {\n const k = toSnakeCase(key);\n if (keyPatterns.some((re) => re.test(k))) return true;\n if (!words.includes(k) && SAFE_SUFFIXES.some((s) => k.endsWith(s))) return false;\n return words.some((w) => k === w || k.startsWith(`${w}_`) || k.endsWith(`_${w}`) || k.includes(`_${w}_`));\n };\n\n const scrub = (s: string): string => {\n let out = s\n .replace(URL_CREDENTIALS_RE, `$1${REDACTED}@`)\n .replace(BEARER_RE, `Bearer ${REDACTED}`)\n .replace(JWT_RE, \"[REDACTED_JWT]\")\n .replace(EMAIL_RE, \"***@$1\");\n for (const re of valuePatterns) out = out.replace(re, REDACTED);\n return out;\n };\n\n const json = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n try {\n const text = JSON.stringify(value, (key, val: unknown) => {\n if (key && val !== null && val !== undefined && isSensitive(key)) return REDACTED;\n if (typeof val === \"bigint\") return val.toString();\n if (val instanceof Error) return { name: val.name, message: val.message };\n return val;\n });\n return text ?? String(value);\n } catch {\n return String(value);\n }\n };\n\n return { isSensitive, scrub, json };\n}\n\nexport const defaultRedactor: Redactor = createRedactor();\n","import { globalSingleton } from \"./global\";\nimport { defaultRedactor } from \"./redact\";\n\nexport interface ErrFields {\n err_type?: string;\n err_message?: string;\n err_code?: string;\n err_stack?: string;\n err_cause?: string;\n}\n\nconst MAX_CAUSE_DEPTH = 5;\n\nexport function isErrorLike(v: unknown): v is Error {\n if (v instanceof Error) return true;\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as { message?: unknown }).message === \"string\" &&\n (\"stack\" in v || \"name\" in v)\n );\n}\n\nfunction errorType(e: Error): string {\n if (e.name && e.name !== \"Error\") return e.name;\n const ctor = (e as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== \"Object\" ? ctor : \"Error\";\n}\n\nexport function serializeError(err: unknown): ErrFields {\n if (!isErrorLike(err)) {\n if (typeof err === \"string\") return { err_type: \"string\", err_message: err };\n return { err_type: typeof err, err_message: defaultRedactor.json(err) };\n }\n const e = err as Error & { code?: unknown; cause?: unknown };\n const out: ErrFields = { err_type: errorType(e), err_message: String(e.message ?? \"\") };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n if (typeof e.stack === \"string\") out.err_stack = e.stack;\n\n const causes: string[] = [];\n let cause = e.cause;\n while (cause !== undefined && cause !== null && causes.length < MAX_CAUSE_DEPTH) {\n if (isErrorLike(cause)) {\n causes.push(`${errorType(cause)}: ${cause.message}`);\n cause = (cause as { cause?: unknown }).cause;\n } else {\n causes.push(defaultRedactor.json(cause));\n cause = undefined;\n }\n }\n if (causes.length > 0) out.err_cause = causes.join(\" <- \");\n return out;\n}\n\nconst logged = globalSingleton(\"logged\", () => new WeakSet<object>());\n\n// Marks an error as already written, so a later boundary (e.g. Next's onRequestError)\n// does not log it a second time.\nexport function markLogged(err: unknown): void {\n if (typeof err === \"object\" && err !== null) logged.add(err);\n}\n\nexport function isLogged(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && logged.has(err);\n}\n","import { isErrorLike, serializeError } from \"./error\";\nimport type { Level } from \"./levels\";\nimport { REDACTED, toSnakeCase, type Redactor } from \"./redact\";\n\nexport { DEFAULT_REDACT_KEYS, toSnakeCase } from \"./redact\";\n\n/** Final per-field hook: return a replacement, or undefined to drop the field. */\nexport type RedactHook = (key: string, value: Primitive) => unknown;\n\nexport type Fields = Record<string, unknown>;\nexport type Primitive = string | number | boolean | null;\nexport type LogRecord = Record<string, Primitive>;\n\nexport interface NormalizeOptions {\n snakeCase: boolean;\n redactor: Redactor;\n hook?: RedactHook;\n}\n\n// Fields the redact hook never sees, so every line keeps its level, message and event.\nconst HOOK_EXEMPT = new Set([\"level\", \"message\", \"event\"]);\n\n// Keys owned by Vector (overwritten or deleted on ingest) or by the logger itself.\nexport const RESERVED_KEYS = new Set([\n \"project\", \"service\", \"vps\", \"version\", \"container_name\", \"container_id\", \"image\", \"stream\",\n \"_timestamp\", \"label\", \"host\", \"source_type\", \"msg\", \"lvl\", \"severity\", \"level\", \"message\",\n]);\n\nexport const MAX_LINE_BYTES = 16000;\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 4000;\nconst MAX_STRING = 1000;\nconst MAX_DEPTH = 3;\nconst TRUNCATED = \"…[truncated]\";\n\n// Fields kept when a line is too large.\nconst KEEP_ON_SHRINK = new Set([\n \"level\", \"message\", \"event\", \"request_id\", \"trace_id\", \"span_id\", \"job_name\", \"job_id\", \"job_attempt\",\n \"user_id\", \"tenant_id\", \"err_type\", \"err_message\", \"err_code\",\n]);\n\nexport function stripUrl(url: string): string {\n const noQuery = url.split(/[?#]/, 1)[0] ?? \"\";\n return noQuery.replace(/^([a-z][a-z0-9+.-]*:\\/\\/)[^/@\\s]*@/i, \"$1\");\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max - TRUNCATED.length) + TRUNCATED : s;\n}\n\nfunction isPlainObject(v: unknown): v is Fields {\n if (typeof v !== \"object\" || v === null) return false;\n const proto = Object.getPrototypeOf(v);\n return proto === Object.prototype || proto === null;\n}\n\nfunction isUrlKey(key: string): boolean {\n return key === \"http_path\" || key === \"http_url\" || key.endsWith(\"_url\");\n}\n\nfunction flattenInto(out: LogRecord, prefix: string, obj: Fields, depth: number, opts: NormalizeOptions): void {\n for (const [rawKey, value] of Object.entries(obj)) {\n const key = opts.snakeCase ? toSnakeCase(rawKey) : rawKey;\n const full = prefix ? `${prefix}_${key}` : key;\n if (value === undefined || typeof value === \"function\" || typeof value === \"symbol\") continue;\n if (value !== null && (opts.redactor.isSensitive(rawKey) || opts.redactor.isSensitive(full))) {\n out[full] = REDACTED;\n continue;\n }\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") {\n out[full] = value;\n } else if (typeof value === \"number\") {\n out[full] = Number.isFinite(value) ? value : String(value);\n } else if (typeof value === \"bigint\") {\n out[full] = value.toString();\n } else if (value instanceof Date) {\n out[full] = Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n } else if (isErrorLike(value)) {\n out[full] = `${value.name}: ${value.message}`;\n } else if (isPlainObject(value) && depth < MAX_DEPTH - 1) {\n flattenInto(out, full, value, depth + 1, opts);\n } else {\n // Arrays, deep objects, class instances: stringified with sensitive keys redacted.\n out[full] = opts.redactor.json(value);\n }\n }\n}\n\nfunction finalize(key: string, value: Primitive, opts: NormalizeOptions): Primitive {\n if (typeof value !== \"string\") return value;\n const v = opts.redactor.scrub(isUrlKey(key) ? stripUrl(value) : value);\n return truncate(v, key === \"err_stack\" ? MAX_STACK : MAX_STRING);\n}\n\nexport function buildRecord(\n level: Level,\n message: unknown,\n sources: (Fields | undefined)[],\n opts: NormalizeOptions,\n): LogRecord {\n const merged: Fields = {};\n for (const source of sources) if (source) Object.assign(merged, source);\n\n let err: unknown;\n if (merged.err !== undefined && merged.err !== null) {\n err = merged.err;\n delete merged.err;\n } else if (isErrorLike(merged.error)) {\n err = merged.error;\n delete merged.error;\n }\n\n const flat: LogRecord = {};\n flattenInto(flat, \"\", merged, 0, opts);\n if (err !== undefined) Object.assign(flat, serializeError(err));\n\n const text = typeof message === \"string\" ? message : opts.redactor.json(message);\n const out: LogRecord = { level, message: truncate(opts.redactor.scrub(text), MAX_MESSAGE) };\n for (const [k, v] of Object.entries(flat)) {\n const key = RESERVED_KEYS.has(k) ? `app_${k}` : k;\n let value = finalize(key, v, opts);\n if (opts.hook && !HOOK_EXEMPT.has(key)) {\n const replaced = opts.hook(key, value);\n if (replaced === undefined) continue;\n value = typeof replaced === \"object\" && replaced !== null ? opts.redactor.json(replaced) : (replaced as Primitive);\n }\n out[key] = value;\n }\n return out;\n}\n\nfunction byteLength(s: string): number {\n // UTF-8 uses at most 3 bytes per UTF-16 code unit: skip encoding short lines.\n if (s.length * 3 <= MAX_LINE_BYTES) return s.length;\n return new TextEncoder().encode(s).length;\n}\n\nexport function serializeRecord(record: LogRecord): string {\n const line = JSON.stringify(record);\n if (byteLength(line) <= MAX_LINE_BYTES) return line;\n const small: LogRecord = {};\n for (const [k, v] of Object.entries(record)) {\n if (KEEP_ON_SHRINK.has(k) || k.startsWith(\"http_\")) small[k] = v;\n }\n small.log_truncated = true;\n return JSON.stringify(small);\n}\n","// No imports: this module is shared with the edge entry (next-edge).\nexport const REQUEST_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;\nconst TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;\n\nexport function newRequestId(): string {\n return globalThis.crypto.randomUUID();\n}\n\nfunction first(header?: string | string[] | null): string | undefined {\n return Array.isArray(header) ? header[0] : (header ?? undefined);\n}\n\nexport function resolveRequestId(header?: string | string[] | null): string {\n const value = first(header);\n return value && REQUEST_ID_RE.test(value) ? value : newRequestId();\n}\n\nexport function parseTraceparent(header?: string | string[] | null): { trace_id: string; span_id: string } | undefined {\n const value = first(header)?.trim().toLowerCase();\n const m = value ? TRACEPARENT_RE.exec(value) : null;\n if (!m) return undefined;\n const [, version, traceId, spanId] = m as unknown as [string, string, string, string];\n if (version === \"ff\" || /^0+$/.test(traceId) || /^0+$/.test(spanId)) return undefined;\n return { trace_id: traceId, span_id: spanId };\n}\n","import { getContext } from \"./context\";\nimport { markLogged, serializeError } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport { stripUrl, type Fields } from \"./normalize\";\nimport { parseTraceparent, resolveRequestId } from \"./request-id\";\n\nexport { newRequestId, parseTraceparent, REQUEST_ID_RE, resolveRequestId } from \"./request-id\";\n\nexport type HeaderSource = Headers | Record<string, string | string[] | undefined>;\n\nfunction header(headers: HeaderSource, name: string): string | string[] | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n return (headers as Record<string, string | string[] | undefined>)[name];\n}\n\nexport function requestContextFields(headers: HeaderSource): { request_id: string; trace_id?: string; span_id?: string } {\n const out: { request_id: string; trace_id?: string; span_id?: string } = {\n request_id: resolveRequestId(header(headers, \"x-request-id\")),\n };\n const tp = parseTraceparent(header(headers, \"traceparent\"));\n if (tp) Object.assign(out, tp);\n return out;\n}\n\n// Headers to forward to services we own, so their logs share request_id / trace_id.\nexport function propagationHeaders(): Record<string, string> {\n const ctx = getContext();\n const out: Record<string, string> = {};\n if (typeof ctx.request_id === \"string\") out[\"x-request-id\"] = ctx.request_id;\n if (typeof ctx.trace_id === \"string\" && typeof ctx.span_id === \"string\") {\n out.traceparent = `00-${ctx.trace_id}-${ctx.span_id}-01`;\n }\n return out;\n}\n\nexport const DEFAULT_SKIP_PATHS: (string | RegExp)[] = [\n /^\\/(health|healthz|ready|readyz|livez)\\/?$/,\n \"/favicon.ico\",\n /^\\/_next\\/static\\//,\n];\n\nexport interface HttpLogOptions {\n skipPaths?: (string | RegExp)[];\n}\n\nexport interface HttpRequestInfo {\n method: string;\n route?: string;\n path: string;\n status: number;\n duration_ms?: number;\n err?: unknown;\n fields?: Fields;\n}\n\nexport function shouldSkip(method: string, path: string, skipPaths: (string | RegExp)[] = DEFAULT_SKIP_PATHS): boolean {\n if (method.toUpperCase() === \"OPTIONS\") return true;\n const p = stripUrl(path);\n return skipPaths.some((s) => (typeof s === \"string\" ? p === s : s.test(p)));\n}\n\n// Writes the one `http.request` summary line for an inbound request.\nexport function logHttpRequest(log: Logger, info: HttpRequestInfo, opts: HttpLogOptions = {}): void {\n if (shouldSkip(info.method, info.path, opts.skipPaths)) return;\n const level = info.status >= 500 ? \"error\" : info.status >= 400 ? \"warn\" : \"info\";\n const path = stripUrl(info.path);\n const errFields: Fields = {};\n if (info.err !== undefined && info.err !== null) {\n Object.assign(errFields, serializeError(info.err));\n if (info.status < 500) delete errFields.err_stack;\n markLogged(info.err);\n }\n // Route templates keep the message bounded; the raw path would make one message per URL.\n const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;\n log[level](message, {\n ...info.fields,\n event: \"http.request\",\n http_method: info.method,\n http_route: info.route,\n http_path: path,\n http_status: info.status,\n duration_ms: info.duration_ms === undefined ? undefined : Math.round(info.duration_ms),\n ...errFields,\n });\n}\n","import { globalSingleton } from \"./global\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\nexport function logStartup(log: Logger, fields: Fields = {}): void {\n log.info(\"app started\", {\n event: \"app.started\",\n runtime_version: `node ${process.versions.node}`,\n log_level: log.level,\n ...fields,\n });\n}\n\n/** Call at the start of your own shutdown handler (e.g. on SIGTERM). The library never handles signals. */\nexport function logShutdown(log: Logger, fields: Fields = {}): void {\n log.info(\"app stopping\", { event: \"app.stopping\", ...fields });\n}\n\nexport interface ProcessHandlerOptions {\n /** Exit with code 1 after an uncaught error. Next.js keeps serving, so its adapter passes false. */\n exitOnCrash?: boolean;\n /** Time other crash handlers (Sentry, APM) get to flush before exit. Default 1000 ms. */\n exitDelayMs?: number;\n exit?: (code: number) => void;\n}\n\nexport function crashHandlers(log: Logger, opts: ProcessHandlerOptions = {}) {\n const exitOnCrash = opts.exitOnCrash ?? true;\n const exitDelayMs = opts.exitDelayMs ?? 1000;\n const exit = opts.exit ?? ((code: number) => process.exit(code));\n let exiting = false;\n const crash = (source: string, err: unknown) => {\n if (!exitOnCrash) {\n log.error(\"unhandled error\", { event: \"app.unhandled_error\", crash_source: source, err });\n return;\n }\n log.fatal(\"app crashed\", { event: \"app.crashed\", crash_source: source, err });\n if (exiting) return;\n exiting = true;\n if (typeof process !== \"undefined\") process.exitCode = 1;\n // Deferred so listeners registered after ours still run; unref so a process with nothing\n // left to do exits on its own (with exitCode 1) without waiting.\n const timer = setTimeout(() => exit(1), exitDelayMs);\n (timer as { unref?: () => void }).unref?.();\n };\n return {\n onUncaught: (err: unknown) => crash(\"uncaughtException\", err),\n onRejection: (reason: unknown) => crash(\"unhandledRejection\", reason),\n };\n}\n\nconst state = globalSingleton(\"process\", () => ({ installed: false }));\n\n/** Logs uncaught exceptions and unhandled rejections. Does not touch SIGTERM/SIGINT. */\nexport function installProcessHandlers(log: Logger, opts: ProcessHandlerOptions = {}): void {\n if (state.installed || typeof process === \"undefined\" || typeof process.on !== \"function\") return;\n state.installed = true;\n const h = crashHandlers(log, opts);\n process.on(\"uncaughtException\", h.onUncaught);\n process.on(\"unhandledRejection\", h.onRejection);\n}\n"],"mappings":"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","names":["header"]}
1
+ {"version":3,"sources":["../src/next.ts","../src/context.ts","../src/global.ts","../src/redact.ts","../src/error.ts","../src/normalize.ts","../src/request-id.ts","../src/http.ts","../src/process.ts"],"sourcesContent":["import { runWithStore } from \"./context\";\nimport { isLogged, markLogged } from \"./error\";\nimport { logHttpRequest, requestContextFields, type HeaderSource, type HttpLogOptions } from \"./http\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\nimport { installProcessHandlers, logStartup } from \"./process\";\n\nexport interface WithLoggingOptions extends HttpLogOptions {\n /** Route template, e.g. \"/api/orders/[id]\". Left empty when not given. */\n route?: string;\n}\n\n/** Status for Next's control-flow errors (redirect(), notFound(), forbidden()...), else undefined. */\nexport function nextControlStatus(err: unknown): number | undefined {\n const digest = (err as { digest?: unknown } | null)?.digest;\n if (typeof digest !== \"string\") return undefined;\n if (digest.startsWith(\"NEXT_REDIRECT\")) {\n const status = Number(digest.split(\";\")[3]);\n return Number.isInteger(status) && status >= 300 ? status : 307;\n }\n if (digest === \"NEXT_NOT_FOUND\") return 404;\n if (digest.startsWith(\"NEXT_HTTP_ERROR_FALLBACK;\")) return Number(digest.split(\";\")[1]) || 404;\n return undefined;\n}\n\nfunction asError(value: unknown): unknown {\n if (typeof value === \"object\" && value !== null) return value;\n return Object.assign(new Error(String(value)), { name: \"NonErrorThrown\", thrown: value });\n}\n\nfunction trySetHeader(res: Response, name: string, value: string): void {\n try {\n res.headers.set(name, value);\n } catch {\n // Some responses (Response.redirect, proxied fetch responses) have immutable headers.\n }\n}\n\n/** Any App Router route handler: `()`, `(request)` or `(request, { params })`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type RouteHandler = (...args: any[]) => Response | Promise<Response>;\n\n/**\n * Wraps an App Router route handler: request context + one http.request line. The returned\n * function has exactly the handler's parameters, so Next's route type checks still pass.\n */\nexport function withLogging<H extends RouteHandler>(\n log: Logger,\n handler: H,\n opts: WithLoggingOptions = {},\n): (...args: Parameters<H>) => Promise<Response> {\n return async (...args) => {\n const request: unknown = args[0];\n // Next always passes the request; direct calls in unit tests may not.\n if (!(request instanceof Request)) return handler(...args);\n const ctx = requestContextFields(request.headers);\n const path = new URL(request.url).pathname;\n const start = performance.now();\n return runWithStore(ctx, async (store) => {\n const summary = (status: number, err: unknown) =>\n logHttpRequest(\n log,\n {\n method: request.method,\n route: opts.route,\n path,\n status,\n duration_ms: performance.now() - start,\n err,\n fields: store.fields,\n },\n opts,\n );\n try {\n const res = await handler(...args);\n summary(res.status, store.error);\n trySetHeader(res, \"x-request-id\", ctx.request_id);\n return res;\n } catch (caught) {\n const control = nextControlStatus(caught);\n // Non-objects cannot be marked as logged, so onRequestError would log them again.\n const err = control === undefined ? asError(caught) : caught;\n summary(control ?? 500, control === undefined ? err : store.error);\n throw err;\n }\n });\n };\n}\n\nexport interface NextRequestInfo {\n path?: string;\n method?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\nexport interface NextErrorContext {\n routerKind?: string;\n routePath?: string;\n routeType?: string;\n}\n\n/** `export const onRequestError = nextOnRequestError(log)` in instrumentation.ts. */\nexport function nextOnRequestError(log: Logger, opts: HttpLogOptions = {}) {\n return async (err: unknown, request: NextRequestInfo, context: NextErrorContext): Promise<void> => {\n if (isLogged(err) || nextControlStatus(err) !== undefined) return;\n const digest = (err as { digest?: unknown } | null)?.digest;\n logHttpRequest(\n log,\n {\n method: request?.method ?? \"GET\",\n route: context?.routePath,\n path: request?.path ?? \"\",\n status: 500,\n err,\n fields: {\n ...requestContextFields(request?.headers ?? {}),\n next_route_type: context?.routeType,\n next_router_kind: context?.routerKind,\n next_digest: typeof digest === \"string\" ? digest : undefined,\n },\n },\n opts,\n );\n };\n}\n\n/** Call from `register()` in instrumentation.ts. Does nothing on the edge runtime. */\nexport function registerNext(log: Logger, fields?: Fields): void {\n if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== \"nodejs\") return;\n // Next keeps serving after an unhandled rejection, so do not exit.\n installProcessHandlers(log, { exitOnCrash: false });\n logStartup(log, { port: process.env.PORT ? Number(process.env.PORT) : undefined, ...fields });\n}\n\nexport interface WithActionOptions {\n /** Request headers source. Defaults to `headers()` from next/headers. */\n headers?: () => HeaderSource | Promise<HeaderSource>;\n}\n\nasync function nextHeaders(): Promise<HeaderSource> {\n try {\n const mod = (await import(\"next/headers\")) as { headers: () => HeaderSource | Promise<HeaderSource> };\n return await mod.headers();\n } catch {\n // Called outside a request (tests, scripts) or Next is not installed.\n return {};\n }\n}\n\n/**\n * Wraps a server action: request context (request_id from x-request-id) plus one\n * `action.completed` / `action.failed` line. redirect()/notFound() count as completed.\n */\nexport function withAction<A extends unknown[], R>(\n log: Logger,\n name: string,\n action: (...args: A) => Promise<R>,\n opts: WithActionOptions = {},\n): (...args: A) => Promise<R> {\n return async (...args) => {\n const headers = await (opts.headers ?? nextHeaders)();\n const start = performance.now();\n return runWithStore({ ...requestContextFields(headers), action_name: name }, async (store) => {\n const fields = () => ({ ...store.fields, duration_ms: Math.round(performance.now() - start) });\n try {\n const result = await action(...args);\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", err: store.error });\n return result;\n } catch (caught) {\n const control = nextControlStatus(caught);\n if (control !== undefined) {\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", next_control_status: control });\n throw caught;\n }\n const err = asError(caught);\n log.error(\"server action failed\", { ...fields(), event: \"action.failed\", err });\n markLogged(err);\n throw err;\n }\n });\n };\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { globalSingleton } from \"./global\";\nimport type { Fields } from \"./normalize\";\n\nexport interface ContextStore {\n fields: Fields;\n error?: unknown;\n /** Route template captured while the router still knew it (Express error path). */\n route?: string;\n parent?: ContextStore;\n}\n\nconst als = globalSingleton(\"als\", () => new AsyncLocalStorage<ContextStore>());\n\nexport const STORE_KEY: symbol = Symbol.for(\"@tanngo11/log.store\");\n\nexport function runWithStore<T>(fields: Fields, fn: (store: ContextStore) => T): T {\n const parent = als.getStore();\n const store: ContextStore = { fields: { ...parent?.fields, ...fields }, parent };\n return als.run(store, () => fn(store));\n}\n\nexport function withContext<T>(fields: Fields, fn: () => T): T {\n return runWithStore(fields, () => fn());\n}\n\nexport function currentStore(): ContextStore | undefined {\n return als.getStore();\n}\n\nexport function getContext(): Fields {\n return als.getStore()?.fields ?? {};\n}\n\n// Applies to the current context and every enclosing one, so a field added inside a nested\n// withContext/runJob still reaches the request's http.request line.\nexport function addContext(fields: Fields): void {\n for (let s = als.getStore(); s; s = s.parent) Object.assign(s.fields, fields);\n}\n\n// Sets the error on the store and on enclosing stores that have none yet.\nexport function recordErrorIn(store: ContextStore, err: unknown): void {\n store.error = err;\n for (let s = store.parent; s; s = s.parent) if (s.error === undefined) s.error = err;\n}\n\n// Marks the error that the current request/job summary line should carry. Does not write a line.\nexport function recordError(err: unknown): void {\n const store = als.getStore();\n if (store) recordErrorIn(store, err);\n}\n\nexport function attachStore(target: object, store: ContextStore): void {\n (target as Record<symbol, ContextStore>)[STORE_KEY] = store;\n}\n\nexport function storeOf(target: unknown): ContextStore | undefined {\n if (typeof target !== \"object\" || target === null) return undefined;\n return (target as Record<symbol, ContextStore | undefined>)[STORE_KEY];\n}\n","// Singletons shared by every copy of this package loaded in the process (ESM and CJS builds,\n// or duplicated installs), so context and dedupe state never split.\nexport function globalSingleton<T>(name: string, make: () => T): T {\n const key = Symbol.for(`@tanngo11/log.${name}`);\n const g = globalThis as unknown as Record<symbol, T | undefined>;\n let value = g[key];\n if (value === undefined) {\n value = make();\n g[key] = value;\n }\n return value;\n}\n","// Redaction shared by the logger and the error serializer. The collector redacts again,\n// but it cannot see secrets inside JSON-encoded strings, so the app side must catch them.\n\nexport const DEFAULT_REDACT_KEYS = [\n \"password\", \"passwd\", \"pwd\", \"passphrase\", \"secret\", \"client_secret\",\n \"token\", \"access_token\", \"refresh_token\", \"id_token\", \"auth_token\",\n \"api_key\", \"apikey\", \"access_key\", \"secret_key\", \"private_key\",\n \"authorization\", \"cookie\", \"set_cookie\", \"session_id\", \"sessionid\",\n \"credential\", \"credentials\", \"dsn\", \"connection_string\", \"conn_string\", \"jwt\", \"bearer\",\n];\n\n/** Opt-in value patterns for `redactValues`. Not on by default: they can hit ordinary numbers. */\nexport const REDACT_PATTERNS = {\n /** Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. */\n phoneVN: /(?<![\\d+])(?:\\+?84|0)[35789]\\d{8}(?!\\d)/g,\n /** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */\n paymentCard: /(?<!\\d)(?:4\\d{3}|5[1-5]\\d{2}|2[2-7]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))(?:[ -]?\\d{4}){2}[ -]?\\d{1,4}(?!\\d)/g,\n /** IPv4 addresses. */\n ipv4: /(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])/g,\n} as const;\n\n// Keys that only describe a secret (count, type, timestamps, flags) stay readable.\nconst SAFE_SUFFIXES = [\"_count\", \"_type\", \"_length\", \"_expires_at\", \"_expires_in\", \"_ttl\", \"_at\", \"_ms\", \"_enabled\"];\n\nexport const REDACTED = \"[REDACTED]\";\n\nexport function toSnakeCase(key: string): string {\n return key\n .replace(/([a-z0-9])([A-Z])/g, \"$1_$2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1_$2\")\n .replace(/[^A-Za-z0-9_]+/g, \"_\")\n .toLowerCase();\n}\n\nconst URL_CREDENTIALS_RE = /([a-z][a-z0-9+.-]*:\\/\\/)[^\\s/@:\"']+:[^\\s/@\"']+@/gi;\nconst BEARER_RE = /\\bBearer\\s+[A-Za-z0-9._~+/=-]+/gi;\nconst JWT_RE = /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g;\nconst EMAIL_RE = /[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,})/g;\n\nexport type KeyPattern = string | RegExp;\n\nexport interface RedactorOptions {\n /** Extra sensitive keys: names (matched like the defaults) or patterns tested on the snake_case key. */\n keys?: readonly KeyPattern[];\n /** Extra value patterns; every match inside any string becomes [REDACTED]. */\n values?: readonly RegExp[];\n}\n\nexport interface Redactor {\n isSensitive(key: string): boolean;\n /** Masks secrets and emails that appear inside free text. */\n scrub(s: string): string;\n /** JSON.stringify that redacts sensitive keys at any depth and never throws. */\n json(value: unknown): string;\n}\n\nfunction globalize(re: RegExp): RegExp {\n return re.flags.includes(\"g\") ? new RegExp(re.source, re.flags) : new RegExp(re.source, `${re.flags}g`);\n}\n\nexport function createRedactor(opts: RedactorOptions = {}): Redactor {\n const words = [...DEFAULT_REDACT_KEYS];\n const keyPatterns: RegExp[] = [];\n for (const k of opts.keys ?? []) {\n if (typeof k === \"string\") words.push(toSnakeCase(k));\n else keyPatterns.push(new RegExp(k.source, k.flags.replace(\"g\", \"\")));\n }\n const valuePatterns = (opts.values ?? []).map(globalize);\n\n const isSensitive = (key: string): boolean => {\n const k = toSnakeCase(key);\n if (keyPatterns.some((re) => re.test(k))) return true;\n if (!words.includes(k) && SAFE_SUFFIXES.some((s) => k.endsWith(s))) return false;\n return words.some((w) => k === w || k.startsWith(`${w}_`) || k.endsWith(`_${w}`) || k.includes(`_${w}_`));\n };\n\n const scrub = (s: string): string => {\n let out = s\n .replace(URL_CREDENTIALS_RE, `$1${REDACTED}@`)\n .replace(BEARER_RE, `Bearer ${REDACTED}`)\n .replace(JWT_RE, \"[REDACTED_JWT]\")\n .replace(EMAIL_RE, \"***@$1\");\n for (const re of valuePatterns) out = out.replace(re, REDACTED);\n return out;\n };\n\n const json = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n try {\n const text = JSON.stringify(value, (key, val: unknown) => {\n if (key && val !== null && val !== undefined && isSensitive(key)) return REDACTED;\n if (typeof val === \"bigint\") return val.toString();\n if (val instanceof Error) return { name: val.name, message: val.message };\n return val;\n });\n return text ?? String(value);\n } catch {\n return String(value);\n }\n };\n\n return { isSensitive, scrub, json };\n}\n\nexport const defaultRedactor: Redactor = createRedactor();\n","import { globalSingleton } from \"./global\";\nimport { defaultRedactor } from \"./redact\";\n\nexport interface ErrFields {\n err_type?: string;\n err_message?: string;\n err_code?: string;\n err_stack?: string;\n err_cause?: string;\n}\n\nconst MAX_CAUSE_DEPTH = 5;\n\nexport function isErrorLike(v: unknown): v is Error {\n if (v instanceof Error) return true;\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as { message?: unknown }).message === \"string\" &&\n (\"stack\" in v || \"name\" in v)\n );\n}\n\nfunction errorType(e: Error): string {\n if (e.name && e.name !== \"Error\") return e.name;\n const ctor = (e as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== \"Object\" ? ctor : \"Error\";\n}\n\nexport function serializeError(err: unknown): ErrFields {\n if (!isErrorLike(err)) {\n if (typeof err === \"string\") return { err_type: \"string\", err_message: err };\n return { err_type: typeof err, err_message: defaultRedactor.json(err) };\n }\n const e = err as Error & { code?: unknown; cause?: unknown };\n const out: ErrFields = { err_type: errorType(e), err_message: String(e.message ?? \"\") };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n if (typeof e.stack === \"string\") out.err_stack = e.stack;\n\n const causes: string[] = [];\n let cause = e.cause;\n while (cause !== undefined && cause !== null && causes.length < MAX_CAUSE_DEPTH) {\n if (isErrorLike(cause)) {\n causes.push(`${errorType(cause)}: ${cause.message}`);\n cause = (cause as { cause?: unknown }).cause;\n } else {\n causes.push(defaultRedactor.json(cause));\n cause = undefined;\n }\n }\n if (causes.length > 0) out.err_cause = causes.join(\" <- \");\n return out;\n}\n\nconst logged = globalSingleton(\"logged\", () => new WeakSet<object>());\n\n// Marks an error as already written, so a later boundary (e.g. Next's onRequestError)\n// does not log it a second time.\nexport function markLogged(err: unknown): void {\n if (typeof err === \"object\" && err !== null) logged.add(err);\n}\n\nexport function isLogged(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && logged.has(err);\n}\n","import { isErrorLike, serializeError } from \"./error\";\nimport type { Level } from \"./levels\";\nimport { REDACTED, toSnakeCase, type Redactor } from \"./redact\";\n\nexport { DEFAULT_REDACT_KEYS, toSnakeCase } from \"./redact\";\n\n/** Final per-field hook: return a replacement, or undefined to drop the field. */\nexport type RedactHook = (key: string, value: Primitive) => unknown;\n\nexport type Fields = Record<string, unknown>;\nexport type Primitive = string | number | boolean | null;\nexport type LogRecord = Record<string, Primitive>;\n\nexport interface NormalizeOptions {\n snakeCase: boolean;\n redactor: Redactor;\n hook?: RedactHook;\n}\n\n// Fields the redact hook never sees, so every line keeps its level, message and event.\nconst HOOK_EXEMPT = new Set([\"level\", \"message\", \"event\"]);\n\n// Keys owned by Vector (overwritten or deleted on ingest) or by the logger itself.\nexport const RESERVED_KEYS = new Set([\n \"project\", \"service\", \"vps\", \"version\", \"container_name\", \"container_id\", \"image\", \"stream\",\n \"_timestamp\", \"label\", \"host\", \"source_type\", \"msg\", \"lvl\", \"severity\", \"level\", \"message\",\n]);\n\nexport const MAX_LINE_BYTES = 16000;\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 4000;\nconst MAX_STRING = 1000;\nconst MAX_DEPTH = 3;\nconst TRUNCATED = \"…[truncated]\";\n\n// Fields kept when a line is too large.\nconst KEEP_ON_SHRINK = new Set([\n \"level\", \"message\", \"event\", \"request_id\", \"trace_id\", \"span_id\", \"job_name\", \"job_id\", \"job_attempt\",\n \"user_id\", \"tenant_id\", \"err_type\", \"err_message\", \"err_code\",\n]);\n\nexport function stripUrl(url: string): string {\n const noQuery = url.split(/[?#]/, 1)[0] ?? \"\";\n return noQuery.replace(/^([a-z][a-z0-9+.-]*:\\/\\/)[^/@\\s]*@/i, \"$1\");\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max - TRUNCATED.length) + TRUNCATED : s;\n}\n\nfunction isPlainObject(v: unknown): v is Fields {\n if (typeof v !== \"object\" || v === null) return false;\n const proto = Object.getPrototypeOf(v);\n return proto === Object.prototype || proto === null;\n}\n\nfunction isUrlKey(key: string): boolean {\n return key === \"http_path\" || key === \"http_url\" || key.endsWith(\"_url\");\n}\n\nfunction flattenInto(out: LogRecord, prefix: string, obj: Fields, depth: number, opts: NormalizeOptions): void {\n for (const [rawKey, value] of Object.entries(obj)) {\n const key = opts.snakeCase ? toSnakeCase(rawKey) : rawKey;\n const full = prefix ? `${prefix}_${key}` : key;\n if (value === undefined || typeof value === \"function\" || typeof value === \"symbol\") continue;\n if (value !== null && (opts.redactor.isSensitive(rawKey) || opts.redactor.isSensitive(full))) {\n out[full] = REDACTED;\n continue;\n }\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") {\n out[full] = value;\n } else if (typeof value === \"number\") {\n out[full] = Number.isFinite(value) ? value : String(value);\n } else if (typeof value === \"bigint\") {\n out[full] = value.toString();\n } else if (value instanceof Date) {\n out[full] = Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n } else if (isErrorLike(value)) {\n out[full] = `${value.name}: ${value.message}`;\n } else if (isPlainObject(value) && depth < MAX_DEPTH - 1) {\n flattenInto(out, full, value, depth + 1, opts);\n } else {\n // Arrays, deep objects, class instances: stringified with sensitive keys redacted.\n out[full] = opts.redactor.json(value);\n }\n }\n}\n\nfunction finalize(key: string, value: Primitive, opts: NormalizeOptions): Primitive {\n if (typeof value !== \"string\") return value;\n const v = opts.redactor.scrub(isUrlKey(key) ? stripUrl(value) : value);\n return truncate(v, key === \"err_stack\" ? MAX_STACK : MAX_STRING);\n}\n\nexport function buildRecord(\n level: Level,\n message: unknown,\n sources: (Fields | undefined)[],\n opts: NormalizeOptions,\n): LogRecord {\n const merged: Fields = {};\n for (const source of sources) if (source) Object.assign(merged, source);\n\n let err: unknown;\n if (merged.err !== undefined && merged.err !== null) {\n err = merged.err;\n delete merged.err;\n } else if (isErrorLike(merged.error)) {\n err = merged.error;\n delete merged.error;\n }\n\n const flat: LogRecord = {};\n flattenInto(flat, \"\", merged, 0, opts);\n if (err !== undefined) Object.assign(flat, serializeError(err));\n\n const text = typeof message === \"string\" ? message : opts.redactor.json(message);\n const out: LogRecord = { level, message: truncate(opts.redactor.scrub(text), MAX_MESSAGE) };\n for (const [k, v] of Object.entries(flat)) {\n const key = RESERVED_KEYS.has(k) ? `app_${k}` : k;\n let value = finalize(key, v, opts);\n if (opts.hook && !HOOK_EXEMPT.has(key)) {\n const replaced = opts.hook(key, value);\n if (replaced === undefined) continue;\n value = typeof replaced === \"object\" && replaced !== null ? opts.redactor.json(replaced) : (replaced as Primitive);\n }\n out[key] = value;\n }\n return out;\n}\n\nfunction byteLength(s: string): number {\n // UTF-8 uses at most 3 bytes per UTF-16 code unit: skip encoding short lines.\n if (s.length * 3 <= MAX_LINE_BYTES) return s.length;\n return new TextEncoder().encode(s).length;\n}\n\nexport function serializeRecord(record: LogRecord): string {\n const line = JSON.stringify(record);\n if (byteLength(line) <= MAX_LINE_BYTES) return line;\n const small: LogRecord = {};\n for (const [k, v] of Object.entries(record)) {\n if (KEEP_ON_SHRINK.has(k) || k.startsWith(\"http_\")) small[k] = v;\n }\n small.log_truncated = true;\n return JSON.stringify(small);\n}\n","// No imports: this module is shared with the edge entry (next-edge).\nexport const REQUEST_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;\nconst TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;\n\nexport function newRequestId(): string {\n return globalThis.crypto.randomUUID();\n}\n\nfunction first(header?: string | string[] | null): string | undefined {\n return Array.isArray(header) ? header[0] : (header ?? undefined);\n}\n\nexport function resolveRequestId(header?: string | string[] | null): string {\n const value = first(header);\n return value && REQUEST_ID_RE.test(value) ? value : newRequestId();\n}\n\nexport function parseTraceparent(header?: string | string[] | null): { trace_id: string; span_id: string } | undefined {\n const value = first(header)?.trim().toLowerCase();\n const m = value ? TRACEPARENT_RE.exec(value) : null;\n if (!m) return undefined;\n const [, version, traceId, spanId] = m as unknown as [string, string, string, string];\n if (version === \"ff\" || /^0+$/.test(traceId) || /^0+$/.test(spanId)) return undefined;\n return { trace_id: traceId, span_id: spanId };\n}\n","import { getContext } from \"./context\";\nimport { markLogged, serializeError } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport { stripUrl, type Fields } from \"./normalize\";\nimport { parseTraceparent, resolveRequestId } from \"./request-id\";\n\nexport { newRequestId, parseTraceparent, REQUEST_ID_RE, resolveRequestId } from \"./request-id\";\n\nexport type HeaderSource = Headers | Record<string, string | string[] | undefined>;\n\nfunction header(headers: HeaderSource, name: string): string | string[] | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n return (headers as Record<string, string | string[] | undefined>)[name];\n}\n\nexport function requestContextFields(headers: HeaderSource): { request_id: string; trace_id?: string; span_id?: string } {\n const out: { request_id: string; trace_id?: string; span_id?: string } = {\n request_id: resolveRequestId(header(headers, \"x-request-id\")),\n };\n const tp = parseTraceparent(header(headers, \"traceparent\"));\n if (tp) Object.assign(out, tp);\n return out;\n}\n\n// Headers to forward to services we own, so their logs share request_id / trace_id.\nexport function propagationHeaders(): Record<string, string> {\n const ctx = getContext();\n const out: Record<string, string> = {};\n if (typeof ctx.request_id === \"string\") out[\"x-request-id\"] = ctx.request_id;\n if (typeof ctx.trace_id === \"string\" && typeof ctx.span_id === \"string\") {\n out.traceparent = `00-${ctx.trace_id}-${ctx.span_id}-01`;\n }\n return out;\n}\n\nexport const DEFAULT_SKIP_PATHS: (string | RegExp)[] = [\n /^\\/(health|healthz|ready|readyz|livez)\\/?$/,\n \"/favicon.ico\",\n /^\\/_next\\/static\\//,\n];\n\nexport interface HttpLogOptions {\n skipPaths?: (string | RegExp)[];\n}\n\nexport interface HttpRequestInfo {\n method: string;\n route?: string;\n path: string;\n status: number;\n duration_ms?: number;\n err?: unknown;\n fields?: Fields;\n}\n\nexport function shouldSkip(method: string, path: string, skipPaths: (string | RegExp)[] = DEFAULT_SKIP_PATHS): boolean {\n if (method.toUpperCase() === \"OPTIONS\") return true;\n const p = stripUrl(path);\n return skipPaths.some((s) => (typeof s === \"string\" ? p === s : s.test(p)));\n}\n\n// Writes the one `http.request` summary line for an inbound request.\nexport function logHttpRequest(log: Logger, info: HttpRequestInfo, opts: HttpLogOptions = {}): void {\n if (shouldSkip(info.method, info.path, opts.skipPaths)) return;\n const level = info.status >= 500 ? \"error\" : info.status >= 400 ? \"warn\" : \"info\";\n const path = stripUrl(info.path);\n const errFields: Fields = {};\n if (info.err !== undefined && info.err !== null) {\n Object.assign(errFields, serializeError(info.err));\n if (info.status < 500) delete errFields.err_stack;\n markLogged(info.err);\n }\n // Route templates keep the message bounded; the raw path would make one message per URL.\n const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;\n log[level](message, {\n ...info.fields,\n event: \"http.request\",\n http_method: info.method,\n http_route: info.route,\n http_path: path,\n http_status: info.status,\n duration_ms: info.duration_ms === undefined ? undefined : Math.round(info.duration_ms),\n ...errFields,\n });\n}\n","import { globalSingleton } from \"./global\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\nexport function logStartup(log: Logger, fields: Fields = {}): void {\n log.info(\"app started\", {\n event: \"app.started\",\n runtime_version: `node ${process.versions.node}`,\n log_level: log.level,\n ...fields,\n });\n}\n\n/** Call at the start of your own shutdown handler (e.g. on SIGTERM). The library never handles signals. */\nexport function logShutdown(log: Logger, fields: Fields = {}): void {\n log.info(\"app stopping\", { event: \"app.stopping\", ...fields });\n}\n\nexport interface ProcessHandlerOptions {\n /** Exit with code 1 after an uncaught error. Next.js keeps serving, so its adapter passes false. */\n exitOnCrash?: boolean;\n /** Time other crash handlers (Sentry, APM) get to flush before exit. Default 1000 ms. */\n exitDelayMs?: number;\n exit?: (code: number) => void;\n}\n\nexport function crashHandlers(log: Logger, opts: ProcessHandlerOptions = {}) {\n const exitOnCrash = opts.exitOnCrash ?? true;\n const exitDelayMs = opts.exitDelayMs ?? 1000;\n const exit = opts.exit ?? ((code: number) => process.exit(code));\n let exiting = false;\n const crash = (source: string, err: unknown) => {\n if (!exitOnCrash) {\n log.error(\"unhandled error\", { event: \"app.unhandled_error\", crash_source: source, err });\n return;\n }\n log.fatal(\"app crashed\", { event: \"app.crashed\", crash_source: source, err });\n if (exiting) return;\n exiting = true;\n if (typeof process !== \"undefined\") process.exitCode = 1;\n // Deferred so listeners registered after ours still run; unref so a process with nothing\n // left to do exits on its own (with exitCode 1) without waiting.\n const timer = setTimeout(() => exit(1), exitDelayMs);\n (timer as { unref?: () => void }).unref?.();\n };\n return {\n onUncaught: (err: unknown) => crash(\"uncaughtException\", err),\n onRejection: (reason: unknown) => crash(\"unhandledRejection\", reason),\n };\n}\n\nconst state = globalSingleton(\"process\", () => ({ installed: false }));\n\n/** Logs uncaught exceptions and unhandled rejections. Does not touch SIGTERM/SIGINT. */\nexport function installProcessHandlers(log: Logger, opts: ProcessHandlerOptions = {}): void {\n if (state.installed || typeof process === \"undefined\" || typeof process.on !== \"function\") return;\n state.installed = true;\n const h = crashHandlers(log, opts);\n process.on(\"uncaughtException\", h.onUncaught);\n process.on(\"unhandledRejection\", h.onRejection);\n}\n"],"mappings":"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","names":["header"]}
package/dist/next.d.cts CHANGED
@@ -7,8 +7,13 @@ interface WithLoggingOptions extends HttpLogOptions {
7
7
  }
8
8
  /** Status for Next's control-flow errors (redirect(), notFound(), forbidden()...), else undefined. */
9
9
  declare function nextControlStatus(err: unknown): number | undefined;
10
- /** Wraps an App Router route handler: request context + one http.request line. */
11
- declare function withLogging<R extends Request, C>(log: Logger, handler: (request: R, context: C) => Response | Promise<Response>, opts?: WithLoggingOptions): (request: R, context: C) => Promise<Response>;
10
+ /** Any App Router route handler: `()`, `(request)` or `(request, { params })`. */
11
+ type RouteHandler = (...args: any[]) => Response | Promise<Response>;
12
+ /**
13
+ * Wraps an App Router route handler: request context + one http.request line. The returned
14
+ * function has exactly the handler's parameters, so Next's route type checks still pass.
15
+ */
16
+ declare function withLogging<H extends RouteHandler>(log: Logger, handler: H, opts?: WithLoggingOptions): (...args: Parameters<H>) => Promise<Response>;
12
17
  interface NextRequestInfo {
13
18
  path?: string;
14
19
  method?: string;
@@ -33,4 +38,4 @@ interface WithActionOptions {
33
38
  */
34
39
  declare function withAction<A extends unknown[], R>(log: Logger, name: string, action: (...args: A) => Promise<R>, opts?: WithActionOptions): (...args: A) => Promise<R>;
35
40
 
36
- export { type NextErrorContext, type NextRequestInfo, type WithActionOptions, type WithLoggingOptions, nextControlStatus, nextOnRequestError, registerNext, withAction, withLogging };
41
+ export { type NextErrorContext, type NextRequestInfo, type RouteHandler, type WithActionOptions, type WithLoggingOptions, nextControlStatus, nextOnRequestError, registerNext, withAction, withLogging };
package/dist/next.d.ts CHANGED
@@ -7,8 +7,13 @@ interface WithLoggingOptions extends HttpLogOptions {
7
7
  }
8
8
  /** Status for Next's control-flow errors (redirect(), notFound(), forbidden()...), else undefined. */
9
9
  declare function nextControlStatus(err: unknown): number | undefined;
10
- /** Wraps an App Router route handler: request context + one http.request line. */
11
- declare function withLogging<R extends Request, C>(log: Logger, handler: (request: R, context: C) => Response | Promise<Response>, opts?: WithLoggingOptions): (request: R, context: C) => Promise<Response>;
10
+ /** Any App Router route handler: `()`, `(request)` or `(request, { params })`. */
11
+ type RouteHandler = (...args: any[]) => Response | Promise<Response>;
12
+ /**
13
+ * Wraps an App Router route handler: request context + one http.request line. The returned
14
+ * function has exactly the handler's parameters, so Next's route type checks still pass.
15
+ */
16
+ declare function withLogging<H extends RouteHandler>(log: Logger, handler: H, opts?: WithLoggingOptions): (...args: Parameters<H>) => Promise<Response>;
12
17
  interface NextRequestInfo {
13
18
  path?: string;
14
19
  method?: string;
@@ -33,4 +38,4 @@ interface WithActionOptions {
33
38
  */
34
39
  declare function withAction<A extends unknown[], R>(log: Logger, name: string, action: (...args: A) => Promise<R>, opts?: WithActionOptions): (...args: A) => Promise<R>;
35
40
 
36
- export { type NextErrorContext, type NextRequestInfo, type WithActionOptions, type WithLoggingOptions, nextControlStatus, nextOnRequestError, registerNext, withAction, withLogging };
41
+ export { type NextErrorContext, type NextRequestInfo, type RouteHandler, type WithActionOptions, type WithLoggingOptions, nextControlStatus, nextOnRequestError, registerNext, withAction, withLogging };
package/dist/next.js CHANGED
@@ -36,7 +36,9 @@ function trySetHeader(res, name, value) {
36
36
  }
37
37
  }
38
38
  function withLogging(log, handler, opts = {}) {
39
- return (request, context) => {
39
+ return async (...args) => {
40
+ const request = args[0];
41
+ if (!(request instanceof Request)) return handler(...args);
40
42
  const ctx = requestContextFields(request.headers);
41
43
  const path = new URL(request.url).pathname;
42
44
  const start = performance.now();
@@ -55,7 +57,7 @@ function withLogging(log, handler, opts = {}) {
55
57
  opts
56
58
  );
57
59
  try {
58
- const res = await handler(request, context);
60
+ const res = await handler(...args);
59
61
  summary(res.status, store.error);
60
62
  trySetHeader(res, "x-request-id", ctx.request_id);
61
63
  return res;
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts"],"sourcesContent":["import { runWithStore } from \"./context\";\nimport { isLogged, markLogged } from \"./error\";\nimport { logHttpRequest, requestContextFields, type HeaderSource, type HttpLogOptions } from \"./http\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\nimport { installProcessHandlers, logStartup } from \"./process\";\n\nexport interface WithLoggingOptions extends HttpLogOptions {\n /** Route template, e.g. \"/api/orders/[id]\". Left empty when not given. */\n route?: string;\n}\n\n/** Status for Next's control-flow errors (redirect(), notFound(), forbidden()...), else undefined. */\nexport function nextControlStatus(err: unknown): number | undefined {\n const digest = (err as { digest?: unknown } | null)?.digest;\n if (typeof digest !== \"string\") return undefined;\n if (digest.startsWith(\"NEXT_REDIRECT\")) {\n const status = Number(digest.split(\";\")[3]);\n return Number.isInteger(status) && status >= 300 ? status : 307;\n }\n if (digest === \"NEXT_NOT_FOUND\") return 404;\n if (digest.startsWith(\"NEXT_HTTP_ERROR_FALLBACK;\")) return Number(digest.split(\";\")[1]) || 404;\n return undefined;\n}\n\nfunction asError(value: unknown): unknown {\n if (typeof value === \"object\" && value !== null) return value;\n return Object.assign(new Error(String(value)), { name: \"NonErrorThrown\", thrown: value });\n}\n\nfunction trySetHeader(res: Response, name: string, value: string): void {\n try {\n res.headers.set(name, value);\n } catch {\n // Some responses (Response.redirect, proxied fetch responses) have immutable headers.\n }\n}\n\n/** Wraps an App Router route handler: request context + one http.request line. */\nexport function withLogging<R extends Request, C>(\n log: Logger,\n handler: (request: R, context: C) => Response | Promise<Response>,\n opts: WithLoggingOptions = {},\n): (request: R, context: C) => Promise<Response> {\n return (request, context) => {\n const ctx = requestContextFields(request.headers);\n const path = new URL(request.url).pathname;\n const start = performance.now();\n return runWithStore(ctx, async (store) => {\n const summary = (status: number, err: unknown) =>\n logHttpRequest(\n log,\n {\n method: request.method,\n route: opts.route,\n path,\n status,\n duration_ms: performance.now() - start,\n err,\n fields: store.fields,\n },\n opts,\n );\n try {\n const res = await handler(request, context);\n summary(res.status, store.error);\n trySetHeader(res, \"x-request-id\", ctx.request_id);\n return res;\n } catch (caught) {\n const control = nextControlStatus(caught);\n // Non-objects cannot be marked as logged, so onRequestError would log them again.\n const err = control === undefined ? asError(caught) : caught;\n summary(control ?? 500, control === undefined ? err : store.error);\n throw err;\n }\n });\n };\n}\n\nexport interface NextRequestInfo {\n path?: string;\n method?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\nexport interface NextErrorContext {\n routerKind?: string;\n routePath?: string;\n routeType?: string;\n}\n\n/** `export const onRequestError = nextOnRequestError(log)` in instrumentation.ts. */\nexport function nextOnRequestError(log: Logger, opts: HttpLogOptions = {}) {\n return async (err: unknown, request: NextRequestInfo, context: NextErrorContext): Promise<void> => {\n if (isLogged(err) || nextControlStatus(err) !== undefined) return;\n const digest = (err as { digest?: unknown } | null)?.digest;\n logHttpRequest(\n log,\n {\n method: request?.method ?? \"GET\",\n route: context?.routePath,\n path: request?.path ?? \"\",\n status: 500,\n err,\n fields: {\n ...requestContextFields(request?.headers ?? {}),\n next_route_type: context?.routeType,\n next_router_kind: context?.routerKind,\n next_digest: typeof digest === \"string\" ? digest : undefined,\n },\n },\n opts,\n );\n };\n}\n\n/** Call from `register()` in instrumentation.ts. Does nothing on the edge runtime. */\nexport function registerNext(log: Logger, fields?: Fields): void {\n if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== \"nodejs\") return;\n // Next keeps serving after an unhandled rejection, so do not exit.\n installProcessHandlers(log, { exitOnCrash: false });\n logStartup(log, { port: process.env.PORT ? Number(process.env.PORT) : undefined, ...fields });\n}\n\nexport interface WithActionOptions {\n /** Request headers source. Defaults to `headers()` from next/headers. */\n headers?: () => HeaderSource | Promise<HeaderSource>;\n}\n\nasync function nextHeaders(): Promise<HeaderSource> {\n try {\n const mod = (await import(\"next/headers\")) as { headers: () => HeaderSource | Promise<HeaderSource> };\n return await mod.headers();\n } catch {\n // Called outside a request (tests, scripts) or Next is not installed.\n return {};\n }\n}\n\n/**\n * Wraps a server action: request context (request_id from x-request-id) plus one\n * `action.completed` / `action.failed` line. redirect()/notFound() count as completed.\n */\nexport function withAction<A extends unknown[], R>(\n log: Logger,\n name: string,\n action: (...args: A) => Promise<R>,\n opts: WithActionOptions = {},\n): (...args: A) => Promise<R> {\n return async (...args) => {\n const headers = await (opts.headers ?? nextHeaders)();\n const start = performance.now();\n return runWithStore({ ...requestContextFields(headers), action_name: name }, async (store) => {\n const fields = () => ({ ...store.fields, duration_ms: Math.round(performance.now() - start) });\n try {\n const result = await action(...args);\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", err: store.error });\n return result;\n } catch (caught) {\n const control = nextControlStatus(caught);\n if (control !== undefined) {\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", next_control_status: control });\n throw caught;\n }\n const err = asError(caught);\n log.error(\"server action failed\", { ...fields(), event: \"action.failed\", err });\n markLogged(err);\n throw err;\n }\n });\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/next.ts"],"sourcesContent":["import { runWithStore } from \"./context\";\nimport { isLogged, markLogged } from \"./error\";\nimport { logHttpRequest, requestContextFields, type HeaderSource, type HttpLogOptions } from \"./http\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\nimport { installProcessHandlers, logStartup } from \"./process\";\n\nexport interface WithLoggingOptions extends HttpLogOptions {\n /** Route template, e.g. \"/api/orders/[id]\". Left empty when not given. */\n route?: string;\n}\n\n/** Status for Next's control-flow errors (redirect(), notFound(), forbidden()...), else undefined. */\nexport function nextControlStatus(err: unknown): number | undefined {\n const digest = (err as { digest?: unknown } | null)?.digest;\n if (typeof digest !== \"string\") return undefined;\n if (digest.startsWith(\"NEXT_REDIRECT\")) {\n const status = Number(digest.split(\";\")[3]);\n return Number.isInteger(status) && status >= 300 ? status : 307;\n }\n if (digest === \"NEXT_NOT_FOUND\") return 404;\n if (digest.startsWith(\"NEXT_HTTP_ERROR_FALLBACK;\")) return Number(digest.split(\";\")[1]) || 404;\n return undefined;\n}\n\nfunction asError(value: unknown): unknown {\n if (typeof value === \"object\" && value !== null) return value;\n return Object.assign(new Error(String(value)), { name: \"NonErrorThrown\", thrown: value });\n}\n\nfunction trySetHeader(res: Response, name: string, value: string): void {\n try {\n res.headers.set(name, value);\n } catch {\n // Some responses (Response.redirect, proxied fetch responses) have immutable headers.\n }\n}\n\n/** Any App Router route handler: `()`, `(request)` or `(request, { params })`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type RouteHandler = (...args: any[]) => Response | Promise<Response>;\n\n/**\n * Wraps an App Router route handler: request context + one http.request line. The returned\n * function has exactly the handler's parameters, so Next's route type checks still pass.\n */\nexport function withLogging<H extends RouteHandler>(\n log: Logger,\n handler: H,\n opts: WithLoggingOptions = {},\n): (...args: Parameters<H>) => Promise<Response> {\n return async (...args) => {\n const request: unknown = args[0];\n // Next always passes the request; direct calls in unit tests may not.\n if (!(request instanceof Request)) return handler(...args);\n const ctx = requestContextFields(request.headers);\n const path = new URL(request.url).pathname;\n const start = performance.now();\n return runWithStore(ctx, async (store) => {\n const summary = (status: number, err: unknown) =>\n logHttpRequest(\n log,\n {\n method: request.method,\n route: opts.route,\n path,\n status,\n duration_ms: performance.now() - start,\n err,\n fields: store.fields,\n },\n opts,\n );\n try {\n const res = await handler(...args);\n summary(res.status, store.error);\n trySetHeader(res, \"x-request-id\", ctx.request_id);\n return res;\n } catch (caught) {\n const control = nextControlStatus(caught);\n // Non-objects cannot be marked as logged, so onRequestError would log them again.\n const err = control === undefined ? asError(caught) : caught;\n summary(control ?? 500, control === undefined ? err : store.error);\n throw err;\n }\n });\n };\n}\n\nexport interface NextRequestInfo {\n path?: string;\n method?: string;\n headers?: Record<string, string | string[] | undefined>;\n}\n\nexport interface NextErrorContext {\n routerKind?: string;\n routePath?: string;\n routeType?: string;\n}\n\n/** `export const onRequestError = nextOnRequestError(log)` in instrumentation.ts. */\nexport function nextOnRequestError(log: Logger, opts: HttpLogOptions = {}) {\n return async (err: unknown, request: NextRequestInfo, context: NextErrorContext): Promise<void> => {\n if (isLogged(err) || nextControlStatus(err) !== undefined) return;\n const digest = (err as { digest?: unknown } | null)?.digest;\n logHttpRequest(\n log,\n {\n method: request?.method ?? \"GET\",\n route: context?.routePath,\n path: request?.path ?? \"\",\n status: 500,\n err,\n fields: {\n ...requestContextFields(request?.headers ?? {}),\n next_route_type: context?.routeType,\n next_router_kind: context?.routerKind,\n next_digest: typeof digest === \"string\" ? digest : undefined,\n },\n },\n opts,\n );\n };\n}\n\n/** Call from `register()` in instrumentation.ts. Does nothing on the edge runtime. */\nexport function registerNext(log: Logger, fields?: Fields): void {\n if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== \"nodejs\") return;\n // Next keeps serving after an unhandled rejection, so do not exit.\n installProcessHandlers(log, { exitOnCrash: false });\n logStartup(log, { port: process.env.PORT ? Number(process.env.PORT) : undefined, ...fields });\n}\n\nexport interface WithActionOptions {\n /** Request headers source. Defaults to `headers()` from next/headers. */\n headers?: () => HeaderSource | Promise<HeaderSource>;\n}\n\nasync function nextHeaders(): Promise<HeaderSource> {\n try {\n const mod = (await import(\"next/headers\")) as { headers: () => HeaderSource | Promise<HeaderSource> };\n return await mod.headers();\n } catch {\n // Called outside a request (tests, scripts) or Next is not installed.\n return {};\n }\n}\n\n/**\n * Wraps a server action: request context (request_id from x-request-id) plus one\n * `action.completed` / `action.failed` line. redirect()/notFound() count as completed.\n */\nexport function withAction<A extends unknown[], R>(\n log: Logger,\n name: string,\n action: (...args: A) => Promise<R>,\n opts: WithActionOptions = {},\n): (...args: A) => Promise<R> {\n return async (...args) => {\n const headers = await (opts.headers ?? nextHeaders)();\n const start = performance.now();\n return runWithStore({ ...requestContextFields(headers), action_name: name }, async (store) => {\n const fields = () => ({ ...store.fields, duration_ms: Math.round(performance.now() - start) });\n try {\n const result = await action(...args);\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", err: store.error });\n return result;\n } catch (caught) {\n const control = nextControlStatus(caught);\n if (control !== undefined) {\n log.info(\"server action completed\", { ...fields(), event: \"action.completed\", next_control_status: control });\n throw caught;\n }\n const err = asError(caught);\n log.error(\"server action failed\", { ...fields(), event: \"action.failed\", err });\n markLogged(err);\n throw err;\n }\n });\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/strapi.ts
21
+ var strapi_exports = {};
22
+ __export(strapi_exports, {
23
+ strapiErrorCapture: () => strapiErrorCapture,
24
+ strapiLoggerConfig: () => strapiLoggerConfig,
25
+ strapiRequestLogger: () => strapiRequestLogger
26
+ });
27
+ module.exports = __toCommonJS(strapi_exports);
28
+ var import_node_stream = require("stream");
29
+
30
+ // src/context.ts
31
+ var import_node_async_hooks = require("async_hooks");
32
+
33
+ // src/global.ts
34
+ function globalSingleton(name, make) {
35
+ const key = /* @__PURE__ */ Symbol.for(`@tanngo11/log.${name}`);
36
+ const g = globalThis;
37
+ let value = g[key];
38
+ if (value === void 0) {
39
+ value = make();
40
+ g[key] = value;
41
+ }
42
+ return value;
43
+ }
44
+
45
+ // src/context.ts
46
+ var als = globalSingleton("als", () => new import_node_async_hooks.AsyncLocalStorage());
47
+ function runWithStore(fields, fn) {
48
+ const parent = als.getStore();
49
+ const store = { fields: { ...parent?.fields, ...fields }, parent };
50
+ return als.run(store, () => fn(store));
51
+ }
52
+ function currentStore() {
53
+ return als.getStore();
54
+ }
55
+ function recordErrorIn(store, err) {
56
+ store.error = err;
57
+ for (let s = store.parent; s; s = s.parent) if (s.error === void 0) s.error = err;
58
+ }
59
+
60
+ // src/redact.ts
61
+ var DEFAULT_REDACT_KEYS = [
62
+ "password",
63
+ "passwd",
64
+ "pwd",
65
+ "passphrase",
66
+ "secret",
67
+ "client_secret",
68
+ "token",
69
+ "access_token",
70
+ "refresh_token",
71
+ "id_token",
72
+ "auth_token",
73
+ "api_key",
74
+ "apikey",
75
+ "access_key",
76
+ "secret_key",
77
+ "private_key",
78
+ "authorization",
79
+ "cookie",
80
+ "set_cookie",
81
+ "session_id",
82
+ "sessionid",
83
+ "credential",
84
+ "credentials",
85
+ "dsn",
86
+ "connection_string",
87
+ "conn_string",
88
+ "jwt",
89
+ "bearer"
90
+ ];
91
+ var SAFE_SUFFIXES = ["_count", "_type", "_length", "_expires_at", "_expires_in", "_ttl", "_at", "_ms", "_enabled"];
92
+ var REDACTED = "[REDACTED]";
93
+ function toSnakeCase(key) {
94
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/[^A-Za-z0-9_]+/g, "_").toLowerCase();
95
+ }
96
+ var URL_CREDENTIALS_RE = /([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"']+:[^\s/@"']+@/gi;
97
+ var BEARER_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
98
+ var JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
99
+ var EMAIL_RE = /[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,})/g;
100
+ function globalize(re) {
101
+ return re.flags.includes("g") ? new RegExp(re.source, re.flags) : new RegExp(re.source, `${re.flags}g`);
102
+ }
103
+ function createRedactor(opts = {}) {
104
+ const words = [...DEFAULT_REDACT_KEYS];
105
+ const keyPatterns = [];
106
+ for (const k of opts.keys ?? []) {
107
+ if (typeof k === "string") words.push(toSnakeCase(k));
108
+ else keyPatterns.push(new RegExp(k.source, k.flags.replace("g", "")));
109
+ }
110
+ const valuePatterns = (opts.values ?? []).map(globalize);
111
+ const isSensitive = (key) => {
112
+ const k = toSnakeCase(key);
113
+ if (keyPatterns.some((re) => re.test(k))) return true;
114
+ if (!words.includes(k) && SAFE_SUFFIXES.some((s) => k.endsWith(s))) return false;
115
+ return words.some((w) => k === w || k.startsWith(`${w}_`) || k.endsWith(`_${w}`) || k.includes(`_${w}_`));
116
+ };
117
+ const scrub = (s) => {
118
+ let out = s.replace(URL_CREDENTIALS_RE, `$1${REDACTED}@`).replace(BEARER_RE, `Bearer ${REDACTED}`).replace(JWT_RE, "[REDACTED_JWT]").replace(EMAIL_RE, "***@$1");
119
+ for (const re of valuePatterns) out = out.replace(re, REDACTED);
120
+ return out;
121
+ };
122
+ const json = (value) => {
123
+ if (typeof value === "string") return value;
124
+ try {
125
+ const text = JSON.stringify(value, (key, val) => {
126
+ if (key && val !== null && val !== void 0 && isSensitive(key)) return REDACTED;
127
+ if (typeof val === "bigint") return val.toString();
128
+ if (val instanceof Error) return { name: val.name, message: val.message };
129
+ return val;
130
+ });
131
+ return text ?? String(value);
132
+ } catch {
133
+ return String(value);
134
+ }
135
+ };
136
+ return { isSensitive, scrub, json };
137
+ }
138
+ var defaultRedactor = createRedactor();
139
+
140
+ // src/error.ts
141
+ var MAX_CAUSE_DEPTH = 5;
142
+ function isErrorLike(v) {
143
+ if (v instanceof Error) return true;
144
+ return typeof v === "object" && v !== null && typeof v.message === "string" && ("stack" in v || "name" in v);
145
+ }
146
+ function errorType(e) {
147
+ if (e.name && e.name !== "Error") return e.name;
148
+ const ctor = e.constructor?.name;
149
+ return ctor && ctor !== "Object" ? ctor : "Error";
150
+ }
151
+ function serializeError(err) {
152
+ if (!isErrorLike(err)) {
153
+ if (typeof err === "string") return { err_type: "string", err_message: err };
154
+ return { err_type: typeof err, err_message: defaultRedactor.json(err) };
155
+ }
156
+ const e = err;
157
+ const out = { err_type: errorType(e), err_message: String(e.message ?? "") };
158
+ if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
159
+ if (typeof e.stack === "string") out.err_stack = e.stack;
160
+ const causes = [];
161
+ let cause = e.cause;
162
+ while (cause !== void 0 && cause !== null && causes.length < MAX_CAUSE_DEPTH) {
163
+ if (isErrorLike(cause)) {
164
+ causes.push(`${errorType(cause)}: ${cause.message}`);
165
+ cause = cause.cause;
166
+ } else {
167
+ causes.push(defaultRedactor.json(cause));
168
+ cause = void 0;
169
+ }
170
+ }
171
+ if (causes.length > 0) out.err_cause = causes.join(" <- ");
172
+ return out;
173
+ }
174
+ var logged = globalSingleton("logged", () => /* @__PURE__ */ new WeakSet());
175
+ function markLogged(err) {
176
+ if (typeof err === "object" && err !== null) logged.add(err);
177
+ }
178
+ function isLogged(err) {
179
+ return typeof err === "object" && err !== null && logged.has(err);
180
+ }
181
+
182
+ // src/normalize.ts
183
+ function stripUrl(url) {
184
+ const noQuery = url.split(/[?#]/, 1)[0] ?? "";
185
+ return noQuery.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@\s]*@/i, "$1");
186
+ }
187
+
188
+ // src/request-id.ts
189
+ var REQUEST_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
190
+ var TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
191
+ function newRequestId() {
192
+ return globalThis.crypto.randomUUID();
193
+ }
194
+ function first(header) {
195
+ return Array.isArray(header) ? header[0] : header ?? void 0;
196
+ }
197
+ function parseTraceparent(header) {
198
+ const value = first(header)?.trim().toLowerCase();
199
+ const m = value ? TRACEPARENT_RE.exec(value) : null;
200
+ if (!m) return void 0;
201
+ const [, version, traceId, spanId] = m;
202
+ if (version === "ff" || /^0+$/.test(traceId) || /^0+$/.test(spanId)) return void 0;
203
+ return { trace_id: traceId, span_id: spanId };
204
+ }
205
+
206
+ // src/http.ts
207
+ var DEFAULT_SKIP_PATHS = [
208
+ /^\/(health|healthz|ready|readyz|livez)\/?$/,
209
+ "/favicon.ico",
210
+ /^\/_next\/static\//
211
+ ];
212
+ function shouldSkip(method, path, skipPaths = DEFAULT_SKIP_PATHS) {
213
+ if (method.toUpperCase() === "OPTIONS") return true;
214
+ const p = stripUrl(path);
215
+ return skipPaths.some((s) => typeof s === "string" ? p === s : s.test(p));
216
+ }
217
+ function logHttpRequest(log, info, opts = {}) {
218
+ if (shouldSkip(info.method, info.path, opts.skipPaths)) return;
219
+ const level = info.status >= 500 ? "error" : info.status >= 400 ? "warn" : "info";
220
+ const path = stripUrl(info.path);
221
+ const errFields = {};
222
+ if (info.err !== void 0 && info.err !== null) {
223
+ Object.assign(errFields, serializeError(info.err));
224
+ if (info.status < 500) delete errFields.err_stack;
225
+ markLogged(info.err);
226
+ }
227
+ const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;
228
+ log[level](message, {
229
+ ...info.fields,
230
+ event: "http.request",
231
+ http_method: info.method,
232
+ http_route: info.route,
233
+ http_path: path,
234
+ http_status: info.status,
235
+ duration_ms: info.duration_ms === void 0 ? void 0 : Math.round(info.duration_ms),
236
+ ...errFields
237
+ });
238
+ }
239
+
240
+ // src/strapi.ts
241
+ function routeOf(ctx) {
242
+ if (typeof ctx._matchedRoute === "string") return ctx._matchedRoute;
243
+ const path = ctx.state.route?.path;
244
+ return typeof path === "string" ? path : void 0;
245
+ }
246
+ function strapiRequestLogger(opts) {
247
+ const stateKey = opts.stateKey ?? "requestId";
248
+ return async (ctx, next) => {
249
+ const incoming = ctx.get("x-request-id");
250
+ const caller = incoming && REQUEST_ID_RE.test(incoming) ? incoming : void 0;
251
+ const existing = ctx.state[stateKey];
252
+ const requestId = typeof existing === "string" && existing ? existing : opts.trustIncomingRequestId && caller ? caller : newRequestId();
253
+ if (ctx.state[stateKey] === void 0) ctx.state[stateKey] = requestId;
254
+ ctx.set("X-Request-ID", requestId);
255
+ const fields = { request_id: requestId };
256
+ if (caller && caller !== requestId) fields.upstream_request_id = caller;
257
+ const trace = parseTraceparent(ctx.get("traceparent"));
258
+ if (trace) Object.assign(fields, trace);
259
+ const start = performance.now();
260
+ await runWithStore(fields, async (store) => {
261
+ let failed = false;
262
+ try {
263
+ await next();
264
+ } catch (err) {
265
+ failed = true;
266
+ recordErrorIn(store, err);
267
+ throw err;
268
+ } finally {
269
+ logHttpRequest(
270
+ opts.log,
271
+ {
272
+ method: ctx.method,
273
+ route: routeOf(ctx),
274
+ path: ctx.url,
275
+ status: failed ? 500 : ctx.status,
276
+ duration_ms: performance.now() - start,
277
+ err: store.error,
278
+ fields: store.fields
279
+ },
280
+ opts
281
+ );
282
+ }
283
+ });
284
+ };
285
+ }
286
+ function strapiErrorCapture() {
287
+ return async (_ctx, next) => {
288
+ try {
289
+ await next();
290
+ } catch (err) {
291
+ const store = currentStore();
292
+ if (store) recordErrorIn(store, err);
293
+ markLogged(err);
294
+ throw err;
295
+ }
296
+ };
297
+ }
298
+ var LEVELS = {
299
+ error: "error",
300
+ warn: "warn",
301
+ info: "info",
302
+ http: "debug",
303
+ verbose: "debug",
304
+ debug: "debug",
305
+ silly: "trace"
306
+ };
307
+ var META_SKIP = /* @__PURE__ */ new Set(["level", "message", "stack"]);
308
+ function forward(log, info) {
309
+ if (isErrorLike(info) && isLogged(info)) return;
310
+ const level = LEVELS[String(info.level)] ?? "info";
311
+ const fields = {};
312
+ for (const [key, value] of Object.entries(info)) if (!META_SKIP.has(key)) fields[key] = value;
313
+ let message = info.message;
314
+ if (isErrorLike(info)) {
315
+ fields.err = info;
316
+ } else if (isErrorLike(message)) {
317
+ if (isLogged(message)) return;
318
+ fields.err = message;
319
+ message = message.message;
320
+ } else if (typeof info.stack === "string") {
321
+ fields.err_stack = info.stack;
322
+ }
323
+ if ((level === "warn" || level === "error") && fields.event === void 0) fields.event = "strapi.log";
324
+ log[level](typeof message === "string" ? message : String(message ?? ""), fields);
325
+ }
326
+ function strapiLoggerConfig(log) {
327
+ const transport = new import_node_stream.Writable({
328
+ objectMode: true,
329
+ write(info, _encoding, callback) {
330
+ try {
331
+ forward(log, info);
332
+ } finally {
333
+ callback();
334
+ }
335
+ }
336
+ });
337
+ Object.assign(transport, {
338
+ log(info, callback) {
339
+ forward(log, info);
340
+ callback();
341
+ }
342
+ });
343
+ return {
344
+ level: "silly",
345
+ // Identity format: replaces Strapi's prettyPrint so the transport receives raw info objects.
346
+ format: { transform: (info) => info },
347
+ transports: [transport]
348
+ };
349
+ }
350
+ // Annotate the CommonJS export names for ESM import in node:
351
+ 0 && (module.exports = {
352
+ strapiErrorCapture,
353
+ strapiLoggerConfig,
354
+ strapiRequestLogger
355
+ });
356
+ //# sourceMappingURL=strapi.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/strapi.ts","../src/context.ts","../src/global.ts","../src/redact.ts","../src/error.ts","../src/normalize.ts","../src/request-id.ts","../src/http.ts"],"sourcesContent":["import { Writable } from \"node:stream\";\nimport { currentStore, recordErrorIn, runWithStore } from \"./context\";\nimport { isErrorLike, isLogged, markLogged } from \"./error\";\nimport { logHttpRequest, newRequestId, parseTraceparent, REQUEST_ID_RE, type HttpLogOptions } from \"./http\";\nimport type { Level } from \"./levels\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\n/** The parts of a Koa/Strapi context the middlewares use. */\nexport interface KoaContextLike {\n method: string;\n url: string;\n status: number;\n state: Record<string, unknown>;\n get(name: string): string;\n set(name: string, value: string): void;\n /** Full matched route template, set by @koa/router (includes router prefixes such as /api). */\n _matchedRoute?: unknown;\n}\n\nexport type KoaMiddleware = (ctx: KoaContextLike, next: () => Promise<unknown>) => Promise<void>;\n\nexport interface StrapiRequestLoggerOptions extends HttpLogOptions {\n log: Logger;\n /**\n * Use a valid incoming x-request-id as request_id. Off by default: the server issues its own\n * id (Strapi apps often use it for audit rows) and records the caller's id as\n * upstream_request_id, which still links the logs of both services.\n */\n trustIncomingRequestId?: boolean;\n /** ctx.state key shared with the app's own correlation middleware. Default \"requestId\". */\n stateKey?: string;\n}\n\nfunction routeOf(ctx: KoaContextLike): string | undefined {\n if (typeof ctx._matchedRoute === \"string\") return ctx._matchedRoute;\n const path = (ctx.state.route as { path?: unknown } | undefined)?.path;\n return typeof path === \"string\" ? path : undefined;\n}\n\n/**\n * Replaces 'strapi::logger' (put it first in config/middlewares): opens the request context and\n * writes one http.request line per request.\n */\nexport function strapiRequestLogger(opts: StrapiRequestLoggerOptions): KoaMiddleware {\n const stateKey = opts.stateKey ?? \"requestId\";\n return async (ctx, next) => {\n const incoming = ctx.get(\"x-request-id\");\n const caller = incoming && REQUEST_ID_RE.test(incoming) ? incoming : undefined;\n const existing = ctx.state[stateKey];\n const requestId =\n typeof existing === \"string\" && existing ? existing : opts.trustIncomingRequestId && caller ? caller : newRequestId();\n // A UUID v4 here is kept by correlation middlewares that only replace invalid ids.\n if (ctx.state[stateKey] === undefined) ctx.state[stateKey] = requestId;\n ctx.set(\"X-Request-ID\", requestId);\n\n const fields: Fields = { request_id: requestId };\n if (caller && caller !== requestId) fields.upstream_request_id = caller;\n const trace = parseTraceparent(ctx.get(\"traceparent\"));\n if (trace) Object.assign(fields, trace);\n\n const start = performance.now();\n await runWithStore(fields, async (store) => {\n let failed = false;\n try {\n await next();\n } catch (err) {\n // Only reached when no error middleware handled it; Koa answers 500.\n failed = true;\n recordErrorIn(store, err);\n throw err;\n } finally {\n logHttpRequest(\n opts.log,\n {\n method: ctx.method,\n route: routeOf(ctx),\n path: ctx.url,\n status: failed ? 500 : ctx.status,\n duration_ms: performance.now() - start,\n err: store.error,\n fields: store.fields,\n },\n opts,\n );\n }\n });\n };\n}\n\n/**\n * Put right after 'strapi::errors'. Records thrown errors on the request's http.request line and\n * marks them, so the copy strapi::errors logs through strapi.log.error is not written twice.\n */\nexport function strapiErrorCapture(): KoaMiddleware {\n return async (_ctx, next) => {\n try {\n await next();\n } catch (err) {\n const store = currentStore();\n if (store) recordErrorIn(store, err);\n markLogged(err);\n throw err;\n }\n };\n}\n\nconst LEVELS: Record<string, Level> = {\n error: \"error\",\n warn: \"warn\",\n info: \"info\",\n http: \"debug\",\n verbose: \"debug\",\n debug: \"debug\",\n silly: \"trace\",\n};\n\nconst META_SKIP = new Set([\"level\", \"message\", \"stack\"]);\n\nfunction forward(log: Logger, info: Record<string, unknown>): void {\n // Already carried by the request's http.request line (see strapiErrorCapture).\n if (isErrorLike(info) && isLogged(info)) return;\n const level = LEVELS[String(info.level)] ?? \"info\";\n const fields: Fields = {};\n for (const [key, value] of Object.entries(info)) if (!META_SKIP.has(key)) fields[key] = value;\n\n let message: unknown = info.message;\n if (isErrorLike(info)) {\n fields.err = info;\n } else if (isErrorLike(message)) {\n if (isLogged(message)) return;\n fields.err = message;\n message = message.message;\n } else if (typeof info.stack === \"string\") {\n fields.err_stack = info.stack;\n }\n if ((level === \"warn\" || level === \"error\") && fields.event === undefined) fields.event = \"strapi.log\";\n log[level](typeof message === \"string\" ? message : String(message ?? \"\"), fields);\n}\n\n/**\n * Winston configuration for Strapi's config/logger.ts: every strapi.log.* call (app code and\n * framework) becomes a contract JSON line with the request context. Level filtering is left to\n * the logger (LOG_LEVEL).\n */\nexport function strapiLoggerConfig(log: Logger) {\n const transport = new Writable({\n objectMode: true,\n write(info: Record<string, unknown>, _encoding, callback) {\n try {\n forward(log, info);\n } finally {\n callback();\n }\n },\n });\n // winston requires a `log(info, callback)` method on stream transports; writes go through write().\n Object.assign(transport, {\n log(info: Record<string, unknown>, callback: () => void) {\n forward(log, info);\n callback();\n },\n });\n return {\n level: \"silly\",\n // Identity format: replaces Strapi's prettyPrint so the transport receives raw info objects.\n format: { transform: <T>(info: T): T => info },\n transports: [transport],\n };\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { globalSingleton } from \"./global\";\nimport type { Fields } from \"./normalize\";\n\nexport interface ContextStore {\n fields: Fields;\n error?: unknown;\n /** Route template captured while the router still knew it (Express error path). */\n route?: string;\n parent?: ContextStore;\n}\n\nconst als = globalSingleton(\"als\", () => new AsyncLocalStorage<ContextStore>());\n\nexport const STORE_KEY: symbol = Symbol.for(\"@tanngo11/log.store\");\n\nexport function runWithStore<T>(fields: Fields, fn: (store: ContextStore) => T): T {\n const parent = als.getStore();\n const store: ContextStore = { fields: { ...parent?.fields, ...fields }, parent };\n return als.run(store, () => fn(store));\n}\n\nexport function withContext<T>(fields: Fields, fn: () => T): T {\n return runWithStore(fields, () => fn());\n}\n\nexport function currentStore(): ContextStore | undefined {\n return als.getStore();\n}\n\nexport function getContext(): Fields {\n return als.getStore()?.fields ?? {};\n}\n\n// Applies to the current context and every enclosing one, so a field added inside a nested\n// withContext/runJob still reaches the request's http.request line.\nexport function addContext(fields: Fields): void {\n for (let s = als.getStore(); s; s = s.parent) Object.assign(s.fields, fields);\n}\n\n// Sets the error on the store and on enclosing stores that have none yet.\nexport function recordErrorIn(store: ContextStore, err: unknown): void {\n store.error = err;\n for (let s = store.parent; s; s = s.parent) if (s.error === undefined) s.error = err;\n}\n\n// Marks the error that the current request/job summary line should carry. Does not write a line.\nexport function recordError(err: unknown): void {\n const store = als.getStore();\n if (store) recordErrorIn(store, err);\n}\n\nexport function attachStore(target: object, store: ContextStore): void {\n (target as Record<symbol, ContextStore>)[STORE_KEY] = store;\n}\n\nexport function storeOf(target: unknown): ContextStore | undefined {\n if (typeof target !== \"object\" || target === null) return undefined;\n return (target as Record<symbol, ContextStore | undefined>)[STORE_KEY];\n}\n","// Singletons shared by every copy of this package loaded in the process (ESM and CJS builds,\n// or duplicated installs), so context and dedupe state never split.\nexport function globalSingleton<T>(name: string, make: () => T): T {\n const key = Symbol.for(`@tanngo11/log.${name}`);\n const g = globalThis as unknown as Record<symbol, T | undefined>;\n let value = g[key];\n if (value === undefined) {\n value = make();\n g[key] = value;\n }\n return value;\n}\n","// Redaction shared by the logger and the error serializer. The collector redacts again,\n// but it cannot see secrets inside JSON-encoded strings, so the app side must catch them.\n\nexport const DEFAULT_REDACT_KEYS = [\n \"password\", \"passwd\", \"pwd\", \"passphrase\", \"secret\", \"client_secret\",\n \"token\", \"access_token\", \"refresh_token\", \"id_token\", \"auth_token\",\n \"api_key\", \"apikey\", \"access_key\", \"secret_key\", \"private_key\",\n \"authorization\", \"cookie\", \"set_cookie\", \"session_id\", \"sessionid\",\n \"credential\", \"credentials\", \"dsn\", \"connection_string\", \"conn_string\", \"jwt\", \"bearer\",\n];\n\n/** Opt-in value patterns for `redactValues`. Not on by default: they can hit ordinary numbers. */\nexport const REDACT_PATTERNS = {\n /** Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. */\n phoneVN: /(?<![\\d+])(?:\\+?84|0)[35789]\\d{8}(?!\\d)/g,\n /** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */\n paymentCard: /(?<!\\d)(?:4\\d{3}|5[1-5]\\d{2}|2[2-7]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))(?:[ -]?\\d{4}){2}[ -]?\\d{1,4}(?!\\d)/g,\n /** IPv4 addresses. */\n ipv4: /(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])/g,\n} as const;\n\n// Keys that only describe a secret (count, type, timestamps, flags) stay readable.\nconst SAFE_SUFFIXES = [\"_count\", \"_type\", \"_length\", \"_expires_at\", \"_expires_in\", \"_ttl\", \"_at\", \"_ms\", \"_enabled\"];\n\nexport const REDACTED = \"[REDACTED]\";\n\nexport function toSnakeCase(key: string): string {\n return key\n .replace(/([a-z0-9])([A-Z])/g, \"$1_$2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1_$2\")\n .replace(/[^A-Za-z0-9_]+/g, \"_\")\n .toLowerCase();\n}\n\nconst URL_CREDENTIALS_RE = /([a-z][a-z0-9+.-]*:\\/\\/)[^\\s/@:\"']+:[^\\s/@\"']+@/gi;\nconst BEARER_RE = /\\bBearer\\s+[A-Za-z0-9._~+/=-]+/gi;\nconst JWT_RE = /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g;\nconst EMAIL_RE = /[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,})/g;\n\nexport type KeyPattern = string | RegExp;\n\nexport interface RedactorOptions {\n /** Extra sensitive keys: names (matched like the defaults) or patterns tested on the snake_case key. */\n keys?: readonly KeyPattern[];\n /** Extra value patterns; every match inside any string becomes [REDACTED]. */\n values?: readonly RegExp[];\n}\n\nexport interface Redactor {\n isSensitive(key: string): boolean;\n /** Masks secrets and emails that appear inside free text. */\n scrub(s: string): string;\n /** JSON.stringify that redacts sensitive keys at any depth and never throws. */\n json(value: unknown): string;\n}\n\nfunction globalize(re: RegExp): RegExp {\n return re.flags.includes(\"g\") ? new RegExp(re.source, re.flags) : new RegExp(re.source, `${re.flags}g`);\n}\n\nexport function createRedactor(opts: RedactorOptions = {}): Redactor {\n const words = [...DEFAULT_REDACT_KEYS];\n const keyPatterns: RegExp[] = [];\n for (const k of opts.keys ?? []) {\n if (typeof k === \"string\") words.push(toSnakeCase(k));\n else keyPatterns.push(new RegExp(k.source, k.flags.replace(\"g\", \"\")));\n }\n const valuePatterns = (opts.values ?? []).map(globalize);\n\n const isSensitive = (key: string): boolean => {\n const k = toSnakeCase(key);\n if (keyPatterns.some((re) => re.test(k))) return true;\n if (!words.includes(k) && SAFE_SUFFIXES.some((s) => k.endsWith(s))) return false;\n return words.some((w) => k === w || k.startsWith(`${w}_`) || k.endsWith(`_${w}`) || k.includes(`_${w}_`));\n };\n\n const scrub = (s: string): string => {\n let out = s\n .replace(URL_CREDENTIALS_RE, `$1${REDACTED}@`)\n .replace(BEARER_RE, `Bearer ${REDACTED}`)\n .replace(JWT_RE, \"[REDACTED_JWT]\")\n .replace(EMAIL_RE, \"***@$1\");\n for (const re of valuePatterns) out = out.replace(re, REDACTED);\n return out;\n };\n\n const json = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n try {\n const text = JSON.stringify(value, (key, val: unknown) => {\n if (key && val !== null && val !== undefined && isSensitive(key)) return REDACTED;\n if (typeof val === \"bigint\") return val.toString();\n if (val instanceof Error) return { name: val.name, message: val.message };\n return val;\n });\n return text ?? String(value);\n } catch {\n return String(value);\n }\n };\n\n return { isSensitive, scrub, json };\n}\n\nexport const defaultRedactor: Redactor = createRedactor();\n","import { globalSingleton } from \"./global\";\nimport { defaultRedactor } from \"./redact\";\n\nexport interface ErrFields {\n err_type?: string;\n err_message?: string;\n err_code?: string;\n err_stack?: string;\n err_cause?: string;\n}\n\nconst MAX_CAUSE_DEPTH = 5;\n\nexport function isErrorLike(v: unknown): v is Error {\n if (v instanceof Error) return true;\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as { message?: unknown }).message === \"string\" &&\n (\"stack\" in v || \"name\" in v)\n );\n}\n\nfunction errorType(e: Error): string {\n if (e.name && e.name !== \"Error\") return e.name;\n const ctor = (e as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== \"Object\" ? ctor : \"Error\";\n}\n\nexport function serializeError(err: unknown): ErrFields {\n if (!isErrorLike(err)) {\n if (typeof err === \"string\") return { err_type: \"string\", err_message: err };\n return { err_type: typeof err, err_message: defaultRedactor.json(err) };\n }\n const e = err as Error & { code?: unknown; cause?: unknown };\n const out: ErrFields = { err_type: errorType(e), err_message: String(e.message ?? \"\") };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n if (typeof e.stack === \"string\") out.err_stack = e.stack;\n\n const causes: string[] = [];\n let cause = e.cause;\n while (cause !== undefined && cause !== null && causes.length < MAX_CAUSE_DEPTH) {\n if (isErrorLike(cause)) {\n causes.push(`${errorType(cause)}: ${cause.message}`);\n cause = (cause as { cause?: unknown }).cause;\n } else {\n causes.push(defaultRedactor.json(cause));\n cause = undefined;\n }\n }\n if (causes.length > 0) out.err_cause = causes.join(\" <- \");\n return out;\n}\n\nconst logged = globalSingleton(\"logged\", () => new WeakSet<object>());\n\n// Marks an error as already written, so a later boundary (e.g. Next's onRequestError)\n// does not log it a second time.\nexport function markLogged(err: unknown): void {\n if (typeof err === \"object\" && err !== null) logged.add(err);\n}\n\nexport function isLogged(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && logged.has(err);\n}\n","import { isErrorLike, serializeError } from \"./error\";\nimport type { Level } from \"./levels\";\nimport { REDACTED, toSnakeCase, type Redactor } from \"./redact\";\n\nexport { DEFAULT_REDACT_KEYS, toSnakeCase } from \"./redact\";\n\n/** Final per-field hook: return a replacement, or undefined to drop the field. */\nexport type RedactHook = (key: string, value: Primitive) => unknown;\n\nexport type Fields = Record<string, unknown>;\nexport type Primitive = string | number | boolean | null;\nexport type LogRecord = Record<string, Primitive>;\n\nexport interface NormalizeOptions {\n snakeCase: boolean;\n redactor: Redactor;\n hook?: RedactHook;\n}\n\n// Fields the redact hook never sees, so every line keeps its level, message and event.\nconst HOOK_EXEMPT = new Set([\"level\", \"message\", \"event\"]);\n\n// Keys owned by Vector (overwritten or deleted on ingest) or by the logger itself.\nexport const RESERVED_KEYS = new Set([\n \"project\", \"service\", \"vps\", \"version\", \"container_name\", \"container_id\", \"image\", \"stream\",\n \"_timestamp\", \"label\", \"host\", \"source_type\", \"msg\", \"lvl\", \"severity\", \"level\", \"message\",\n]);\n\nexport const MAX_LINE_BYTES = 16000;\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 4000;\nconst MAX_STRING = 1000;\nconst MAX_DEPTH = 3;\nconst TRUNCATED = \"…[truncated]\";\n\n// Fields kept when a line is too large.\nconst KEEP_ON_SHRINK = new Set([\n \"level\", \"message\", \"event\", \"request_id\", \"trace_id\", \"span_id\", \"job_name\", \"job_id\", \"job_attempt\",\n \"user_id\", \"tenant_id\", \"err_type\", \"err_message\", \"err_code\",\n]);\n\nexport function stripUrl(url: string): string {\n const noQuery = url.split(/[?#]/, 1)[0] ?? \"\";\n return noQuery.replace(/^([a-z][a-z0-9+.-]*:\\/\\/)[^/@\\s]*@/i, \"$1\");\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max - TRUNCATED.length) + TRUNCATED : s;\n}\n\nfunction isPlainObject(v: unknown): v is Fields {\n if (typeof v !== \"object\" || v === null) return false;\n const proto = Object.getPrototypeOf(v);\n return proto === Object.prototype || proto === null;\n}\n\nfunction isUrlKey(key: string): boolean {\n return key === \"http_path\" || key === \"http_url\" || key.endsWith(\"_url\");\n}\n\nfunction flattenInto(out: LogRecord, prefix: string, obj: Fields, depth: number, opts: NormalizeOptions): void {\n for (const [rawKey, value] of Object.entries(obj)) {\n const key = opts.snakeCase ? toSnakeCase(rawKey) : rawKey;\n const full = prefix ? `${prefix}_${key}` : key;\n if (value === undefined || typeof value === \"function\" || typeof value === \"symbol\") continue;\n if (value !== null && (opts.redactor.isSensitive(rawKey) || opts.redactor.isSensitive(full))) {\n out[full] = REDACTED;\n continue;\n }\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") {\n out[full] = value;\n } else if (typeof value === \"number\") {\n out[full] = Number.isFinite(value) ? value : String(value);\n } else if (typeof value === \"bigint\") {\n out[full] = value.toString();\n } else if (value instanceof Date) {\n out[full] = Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n } else if (isErrorLike(value)) {\n out[full] = `${value.name}: ${value.message}`;\n } else if (isPlainObject(value) && depth < MAX_DEPTH - 1) {\n flattenInto(out, full, value, depth + 1, opts);\n } else {\n // Arrays, deep objects, class instances: stringified with sensitive keys redacted.\n out[full] = opts.redactor.json(value);\n }\n }\n}\n\nfunction finalize(key: string, value: Primitive, opts: NormalizeOptions): Primitive {\n if (typeof value !== \"string\") return value;\n const v = opts.redactor.scrub(isUrlKey(key) ? stripUrl(value) : value);\n return truncate(v, key === \"err_stack\" ? MAX_STACK : MAX_STRING);\n}\n\nexport function buildRecord(\n level: Level,\n message: unknown,\n sources: (Fields | undefined)[],\n opts: NormalizeOptions,\n): LogRecord {\n const merged: Fields = {};\n for (const source of sources) if (source) Object.assign(merged, source);\n\n let err: unknown;\n if (merged.err !== undefined && merged.err !== null) {\n err = merged.err;\n delete merged.err;\n } else if (isErrorLike(merged.error)) {\n err = merged.error;\n delete merged.error;\n }\n\n const flat: LogRecord = {};\n flattenInto(flat, \"\", merged, 0, opts);\n if (err !== undefined) Object.assign(flat, serializeError(err));\n\n const text = typeof message === \"string\" ? message : opts.redactor.json(message);\n const out: LogRecord = { level, message: truncate(opts.redactor.scrub(text), MAX_MESSAGE) };\n for (const [k, v] of Object.entries(flat)) {\n const key = RESERVED_KEYS.has(k) ? `app_${k}` : k;\n let value = finalize(key, v, opts);\n if (opts.hook && !HOOK_EXEMPT.has(key)) {\n const replaced = opts.hook(key, value);\n if (replaced === undefined) continue;\n value = typeof replaced === \"object\" && replaced !== null ? opts.redactor.json(replaced) : (replaced as Primitive);\n }\n out[key] = value;\n }\n return out;\n}\n\nfunction byteLength(s: string): number {\n // UTF-8 uses at most 3 bytes per UTF-16 code unit: skip encoding short lines.\n if (s.length * 3 <= MAX_LINE_BYTES) return s.length;\n return new TextEncoder().encode(s).length;\n}\n\nexport function serializeRecord(record: LogRecord): string {\n const line = JSON.stringify(record);\n if (byteLength(line) <= MAX_LINE_BYTES) return line;\n const small: LogRecord = {};\n for (const [k, v] of Object.entries(record)) {\n if (KEEP_ON_SHRINK.has(k) || k.startsWith(\"http_\")) small[k] = v;\n }\n small.log_truncated = true;\n return JSON.stringify(small);\n}\n","// No imports: this module is shared with the edge entry (next-edge).\nexport const REQUEST_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;\nconst TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;\n\nexport function newRequestId(): string {\n return globalThis.crypto.randomUUID();\n}\n\nfunction first(header?: string | string[] | null): string | undefined {\n return Array.isArray(header) ? header[0] : (header ?? undefined);\n}\n\nexport function resolveRequestId(header?: string | string[] | null): string {\n const value = first(header);\n return value && REQUEST_ID_RE.test(value) ? value : newRequestId();\n}\n\nexport function parseTraceparent(header?: string | string[] | null): { trace_id: string; span_id: string } | undefined {\n const value = first(header)?.trim().toLowerCase();\n const m = value ? TRACEPARENT_RE.exec(value) : null;\n if (!m) return undefined;\n const [, version, traceId, spanId] = m as unknown as [string, string, string, string];\n if (version === \"ff\" || /^0+$/.test(traceId) || /^0+$/.test(spanId)) return undefined;\n return { trace_id: traceId, span_id: spanId };\n}\n","import { getContext } from \"./context\";\nimport { markLogged, serializeError } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport { stripUrl, type Fields } from \"./normalize\";\nimport { parseTraceparent, resolveRequestId } from \"./request-id\";\n\nexport { newRequestId, parseTraceparent, REQUEST_ID_RE, resolveRequestId } from \"./request-id\";\n\nexport type HeaderSource = Headers | Record<string, string | string[] | undefined>;\n\nfunction header(headers: HeaderSource, name: string): string | string[] | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n return (headers as Record<string, string | string[] | undefined>)[name];\n}\n\nexport function requestContextFields(headers: HeaderSource): { request_id: string; trace_id?: string; span_id?: string } {\n const out: { request_id: string; trace_id?: string; span_id?: string } = {\n request_id: resolveRequestId(header(headers, \"x-request-id\")),\n };\n const tp = parseTraceparent(header(headers, \"traceparent\"));\n if (tp) Object.assign(out, tp);\n return out;\n}\n\n// Headers to forward to services we own, so their logs share request_id / trace_id.\nexport function propagationHeaders(): Record<string, string> {\n const ctx = getContext();\n const out: Record<string, string> = {};\n if (typeof ctx.request_id === \"string\") out[\"x-request-id\"] = ctx.request_id;\n if (typeof ctx.trace_id === \"string\" && typeof ctx.span_id === \"string\") {\n out.traceparent = `00-${ctx.trace_id}-${ctx.span_id}-01`;\n }\n return out;\n}\n\nexport const DEFAULT_SKIP_PATHS: (string | RegExp)[] = [\n /^\\/(health|healthz|ready|readyz|livez)\\/?$/,\n \"/favicon.ico\",\n /^\\/_next\\/static\\//,\n];\n\nexport interface HttpLogOptions {\n skipPaths?: (string | RegExp)[];\n}\n\nexport interface HttpRequestInfo {\n method: string;\n route?: string;\n path: string;\n status: number;\n duration_ms?: number;\n err?: unknown;\n fields?: Fields;\n}\n\nexport function shouldSkip(method: string, path: string, skipPaths: (string | RegExp)[] = DEFAULT_SKIP_PATHS): boolean {\n if (method.toUpperCase() === \"OPTIONS\") return true;\n const p = stripUrl(path);\n return skipPaths.some((s) => (typeof s === \"string\" ? p === s : s.test(p)));\n}\n\n// Writes the one `http.request` summary line for an inbound request.\nexport function logHttpRequest(log: Logger, info: HttpRequestInfo, opts: HttpLogOptions = {}): void {\n if (shouldSkip(info.method, info.path, opts.skipPaths)) return;\n const level = info.status >= 500 ? \"error\" : info.status >= 400 ? \"warn\" : \"info\";\n const path = stripUrl(info.path);\n const errFields: Fields = {};\n if (info.err !== undefined && info.err !== null) {\n Object.assign(errFields, serializeError(info.err));\n if (info.status < 500) delete errFields.err_stack;\n markLogged(info.err);\n }\n // Route templates keep the message bounded; the raw path would make one message per URL.\n const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;\n log[level](message, {\n ...info.fields,\n event: \"http.request\",\n http_method: info.method,\n http_route: info.route,\n http_path: path,\n http_status: info.status,\n duration_ms: info.duration_ms === undefined ? undefined : Math.round(info.duration_ms),\n ...errFields,\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,51 @@
1
+ import { Writable } from 'node:stream';
2
+ import { H as HttpLogOptions } from './http-BXAiU-rN.cjs';
3
+ import { L as Logger } from './logger-CFMTzjsm.cjs';
4
+
5
+ /** The parts of a Koa/Strapi context the middlewares use. */
6
+ interface KoaContextLike {
7
+ method: string;
8
+ url: string;
9
+ status: number;
10
+ state: Record<string, unknown>;
11
+ get(name: string): string;
12
+ set(name: string, value: string): void;
13
+ /** Full matched route template, set by @koa/router (includes router prefixes such as /api). */
14
+ _matchedRoute?: unknown;
15
+ }
16
+ type KoaMiddleware = (ctx: KoaContextLike, next: () => Promise<unknown>) => Promise<void>;
17
+ interface StrapiRequestLoggerOptions extends HttpLogOptions {
18
+ log: Logger;
19
+ /**
20
+ * Use a valid incoming x-request-id as request_id. Off by default: the server issues its own
21
+ * id (Strapi apps often use it for audit rows) and records the caller's id as
22
+ * upstream_request_id, which still links the logs of both services.
23
+ */
24
+ trustIncomingRequestId?: boolean;
25
+ /** ctx.state key shared with the app's own correlation middleware. Default "requestId". */
26
+ stateKey?: string;
27
+ }
28
+ /**
29
+ * Replaces 'strapi::logger' (put it first in config/middlewares): opens the request context and
30
+ * writes one http.request line per request.
31
+ */
32
+ declare function strapiRequestLogger(opts: StrapiRequestLoggerOptions): KoaMiddleware;
33
+ /**
34
+ * Put right after 'strapi::errors'. Records thrown errors on the request's http.request line and
35
+ * marks them, so the copy strapi::errors logs through strapi.log.error is not written twice.
36
+ */
37
+ declare function strapiErrorCapture(): KoaMiddleware;
38
+ /**
39
+ * Winston configuration for Strapi's config/logger.ts: every strapi.log.* call (app code and
40
+ * framework) becomes a contract JSON line with the request context. Level filtering is left to
41
+ * the logger (LOG_LEVEL).
42
+ */
43
+ declare function strapiLoggerConfig(log: Logger): {
44
+ level: string;
45
+ format: {
46
+ transform: <T>(info: T) => T;
47
+ };
48
+ transports: Writable[];
49
+ };
50
+
51
+ export { type KoaContextLike, type KoaMiddleware, type StrapiRequestLoggerOptions, strapiErrorCapture, strapiLoggerConfig, strapiRequestLogger };
@@ -0,0 +1,51 @@
1
+ import { Writable } from 'node:stream';
2
+ import { H as HttpLogOptions } from './http-BI6ARZlD.js';
3
+ import { L as Logger } from './logger-CFMTzjsm.js';
4
+
5
+ /** The parts of a Koa/Strapi context the middlewares use. */
6
+ interface KoaContextLike {
7
+ method: string;
8
+ url: string;
9
+ status: number;
10
+ state: Record<string, unknown>;
11
+ get(name: string): string;
12
+ set(name: string, value: string): void;
13
+ /** Full matched route template, set by @koa/router (includes router prefixes such as /api). */
14
+ _matchedRoute?: unknown;
15
+ }
16
+ type KoaMiddleware = (ctx: KoaContextLike, next: () => Promise<unknown>) => Promise<void>;
17
+ interface StrapiRequestLoggerOptions extends HttpLogOptions {
18
+ log: Logger;
19
+ /**
20
+ * Use a valid incoming x-request-id as request_id. Off by default: the server issues its own
21
+ * id (Strapi apps often use it for audit rows) and records the caller's id as
22
+ * upstream_request_id, which still links the logs of both services.
23
+ */
24
+ trustIncomingRequestId?: boolean;
25
+ /** ctx.state key shared with the app's own correlation middleware. Default "requestId". */
26
+ stateKey?: string;
27
+ }
28
+ /**
29
+ * Replaces 'strapi::logger' (put it first in config/middlewares): opens the request context and
30
+ * writes one http.request line per request.
31
+ */
32
+ declare function strapiRequestLogger(opts: StrapiRequestLoggerOptions): KoaMiddleware;
33
+ /**
34
+ * Put right after 'strapi::errors'. Records thrown errors on the request's http.request line and
35
+ * marks them, so the copy strapi::errors logs through strapi.log.error is not written twice.
36
+ */
37
+ declare function strapiErrorCapture(): KoaMiddleware;
38
+ /**
39
+ * Winston configuration for Strapi's config/logger.ts: every strapi.log.* call (app code and
40
+ * framework) becomes a contract JSON line with the request context. Level filtering is left to
41
+ * the logger (LOG_LEVEL).
42
+ */
43
+ declare function strapiLoggerConfig(log: Logger): {
44
+ level: string;
45
+ format: {
46
+ transform: <T>(info: T) => T;
47
+ };
48
+ transports: Writable[];
49
+ };
50
+
51
+ export { type KoaContextLike, type KoaMiddleware, type StrapiRequestLoggerOptions, strapiErrorCapture, strapiLoggerConfig, strapiRequestLogger };
package/dist/strapi.js ADDED
@@ -0,0 +1,134 @@
1
+ import {
2
+ logHttpRequest
3
+ } from "./chunk-KD5H5G6Y.js";
4
+ import {
5
+ REQUEST_ID_RE,
6
+ newRequestId,
7
+ parseTraceparent
8
+ } from "./chunk-KLBL7QEN.js";
9
+ import {
10
+ currentStore,
11
+ isErrorLike,
12
+ isLogged,
13
+ markLogged,
14
+ recordErrorIn,
15
+ runWithStore
16
+ } from "./chunk-QMC6BWGK.js";
17
+
18
+ // src/strapi.ts
19
+ import { Writable } from "stream";
20
+ function routeOf(ctx) {
21
+ if (typeof ctx._matchedRoute === "string") return ctx._matchedRoute;
22
+ const path = ctx.state.route?.path;
23
+ return typeof path === "string" ? path : void 0;
24
+ }
25
+ function strapiRequestLogger(opts) {
26
+ const stateKey = opts.stateKey ?? "requestId";
27
+ return async (ctx, next) => {
28
+ const incoming = ctx.get("x-request-id");
29
+ const caller = incoming && REQUEST_ID_RE.test(incoming) ? incoming : void 0;
30
+ const existing = ctx.state[stateKey];
31
+ const requestId = typeof existing === "string" && existing ? existing : opts.trustIncomingRequestId && caller ? caller : newRequestId();
32
+ if (ctx.state[stateKey] === void 0) ctx.state[stateKey] = requestId;
33
+ ctx.set("X-Request-ID", requestId);
34
+ const fields = { request_id: requestId };
35
+ if (caller && caller !== requestId) fields.upstream_request_id = caller;
36
+ const trace = parseTraceparent(ctx.get("traceparent"));
37
+ if (trace) Object.assign(fields, trace);
38
+ const start = performance.now();
39
+ await runWithStore(fields, async (store) => {
40
+ let failed = false;
41
+ try {
42
+ await next();
43
+ } catch (err) {
44
+ failed = true;
45
+ recordErrorIn(store, err);
46
+ throw err;
47
+ } finally {
48
+ logHttpRequest(
49
+ opts.log,
50
+ {
51
+ method: ctx.method,
52
+ route: routeOf(ctx),
53
+ path: ctx.url,
54
+ status: failed ? 500 : ctx.status,
55
+ duration_ms: performance.now() - start,
56
+ err: store.error,
57
+ fields: store.fields
58
+ },
59
+ opts
60
+ );
61
+ }
62
+ });
63
+ };
64
+ }
65
+ function strapiErrorCapture() {
66
+ return async (_ctx, next) => {
67
+ try {
68
+ await next();
69
+ } catch (err) {
70
+ const store = currentStore();
71
+ if (store) recordErrorIn(store, err);
72
+ markLogged(err);
73
+ throw err;
74
+ }
75
+ };
76
+ }
77
+ var LEVELS = {
78
+ error: "error",
79
+ warn: "warn",
80
+ info: "info",
81
+ http: "debug",
82
+ verbose: "debug",
83
+ debug: "debug",
84
+ silly: "trace"
85
+ };
86
+ var META_SKIP = /* @__PURE__ */ new Set(["level", "message", "stack"]);
87
+ function forward(log, info) {
88
+ if (isErrorLike(info) && isLogged(info)) return;
89
+ const level = LEVELS[String(info.level)] ?? "info";
90
+ const fields = {};
91
+ for (const [key, value] of Object.entries(info)) if (!META_SKIP.has(key)) fields[key] = value;
92
+ let message = info.message;
93
+ if (isErrorLike(info)) {
94
+ fields.err = info;
95
+ } else if (isErrorLike(message)) {
96
+ if (isLogged(message)) return;
97
+ fields.err = message;
98
+ message = message.message;
99
+ } else if (typeof info.stack === "string") {
100
+ fields.err_stack = info.stack;
101
+ }
102
+ if ((level === "warn" || level === "error") && fields.event === void 0) fields.event = "strapi.log";
103
+ log[level](typeof message === "string" ? message : String(message ?? ""), fields);
104
+ }
105
+ function strapiLoggerConfig(log) {
106
+ const transport = new Writable({
107
+ objectMode: true,
108
+ write(info, _encoding, callback) {
109
+ try {
110
+ forward(log, info);
111
+ } finally {
112
+ callback();
113
+ }
114
+ }
115
+ });
116
+ Object.assign(transport, {
117
+ log(info, callback) {
118
+ forward(log, info);
119
+ callback();
120
+ }
121
+ });
122
+ return {
123
+ level: "silly",
124
+ // Identity format: replaces Strapi's prettyPrint so the transport receives raw info objects.
125
+ format: { transform: (info) => info },
126
+ transports: [transport]
127
+ };
128
+ }
129
+ export {
130
+ strapiErrorCapture,
131
+ strapiLoggerConfig,
132
+ strapiRequestLogger
133
+ };
134
+ //# sourceMappingURL=strapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/strapi.ts"],"sourcesContent":["import { Writable } from \"node:stream\";\nimport { currentStore, recordErrorIn, runWithStore } from \"./context\";\nimport { isErrorLike, isLogged, markLogged } from \"./error\";\nimport { logHttpRequest, newRequestId, parseTraceparent, REQUEST_ID_RE, type HttpLogOptions } from \"./http\";\nimport type { Level } from \"./levels\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\n/** The parts of a Koa/Strapi context the middlewares use. */\nexport interface KoaContextLike {\n method: string;\n url: string;\n status: number;\n state: Record<string, unknown>;\n get(name: string): string;\n set(name: string, value: string): void;\n /** Full matched route template, set by @koa/router (includes router prefixes such as /api). */\n _matchedRoute?: unknown;\n}\n\nexport type KoaMiddleware = (ctx: KoaContextLike, next: () => Promise<unknown>) => Promise<void>;\n\nexport interface StrapiRequestLoggerOptions extends HttpLogOptions {\n log: Logger;\n /**\n * Use a valid incoming x-request-id as request_id. Off by default: the server issues its own\n * id (Strapi apps often use it for audit rows) and records the caller's id as\n * upstream_request_id, which still links the logs of both services.\n */\n trustIncomingRequestId?: boolean;\n /** ctx.state key shared with the app's own correlation middleware. Default \"requestId\". */\n stateKey?: string;\n}\n\nfunction routeOf(ctx: KoaContextLike): string | undefined {\n if (typeof ctx._matchedRoute === \"string\") return ctx._matchedRoute;\n const path = (ctx.state.route as { path?: unknown } | undefined)?.path;\n return typeof path === \"string\" ? path : undefined;\n}\n\n/**\n * Replaces 'strapi::logger' (put it first in config/middlewares): opens the request context and\n * writes one http.request line per request.\n */\nexport function strapiRequestLogger(opts: StrapiRequestLoggerOptions): KoaMiddleware {\n const stateKey = opts.stateKey ?? \"requestId\";\n return async (ctx, next) => {\n const incoming = ctx.get(\"x-request-id\");\n const caller = incoming && REQUEST_ID_RE.test(incoming) ? incoming : undefined;\n const existing = ctx.state[stateKey];\n const requestId =\n typeof existing === \"string\" && existing ? existing : opts.trustIncomingRequestId && caller ? caller : newRequestId();\n // A UUID v4 here is kept by correlation middlewares that only replace invalid ids.\n if (ctx.state[stateKey] === undefined) ctx.state[stateKey] = requestId;\n ctx.set(\"X-Request-ID\", requestId);\n\n const fields: Fields = { request_id: requestId };\n if (caller && caller !== requestId) fields.upstream_request_id = caller;\n const trace = parseTraceparent(ctx.get(\"traceparent\"));\n if (trace) Object.assign(fields, trace);\n\n const start = performance.now();\n await runWithStore(fields, async (store) => {\n let failed = false;\n try {\n await next();\n } catch (err) {\n // Only reached when no error middleware handled it; Koa answers 500.\n failed = true;\n recordErrorIn(store, err);\n throw err;\n } finally {\n logHttpRequest(\n opts.log,\n {\n method: ctx.method,\n route: routeOf(ctx),\n path: ctx.url,\n status: failed ? 500 : ctx.status,\n duration_ms: performance.now() - start,\n err: store.error,\n fields: store.fields,\n },\n opts,\n );\n }\n });\n };\n}\n\n/**\n * Put right after 'strapi::errors'. Records thrown errors on the request's http.request line and\n * marks them, so the copy strapi::errors logs through strapi.log.error is not written twice.\n */\nexport function strapiErrorCapture(): KoaMiddleware {\n return async (_ctx, next) => {\n try {\n await next();\n } catch (err) {\n const store = currentStore();\n if (store) recordErrorIn(store, err);\n markLogged(err);\n throw err;\n }\n };\n}\n\nconst LEVELS: Record<string, Level> = {\n error: \"error\",\n warn: \"warn\",\n info: \"info\",\n http: \"debug\",\n verbose: \"debug\",\n debug: \"debug\",\n silly: \"trace\",\n};\n\nconst META_SKIP = new Set([\"level\", \"message\", \"stack\"]);\n\nfunction forward(log: Logger, info: Record<string, unknown>): void {\n // Already carried by the request's http.request line (see strapiErrorCapture).\n if (isErrorLike(info) && isLogged(info)) return;\n const level = LEVELS[String(info.level)] ?? \"info\";\n const fields: Fields = {};\n for (const [key, value] of Object.entries(info)) if (!META_SKIP.has(key)) fields[key] = value;\n\n let message: unknown = info.message;\n if (isErrorLike(info)) {\n fields.err = info;\n } else if (isErrorLike(message)) {\n if (isLogged(message)) return;\n fields.err = message;\n message = message.message;\n } else if (typeof info.stack === \"string\") {\n fields.err_stack = info.stack;\n }\n if ((level === \"warn\" || level === \"error\") && fields.event === undefined) fields.event = \"strapi.log\";\n log[level](typeof message === \"string\" ? message : String(message ?? \"\"), fields);\n}\n\n/**\n * Winston configuration for Strapi's config/logger.ts: every strapi.log.* call (app code and\n * framework) becomes a contract JSON line with the request context. Level filtering is left to\n * the logger (LOG_LEVEL).\n */\nexport function strapiLoggerConfig(log: Logger) {\n const transport = new Writable({\n objectMode: true,\n write(info: Record<string, unknown>, _encoding, callback) {\n try {\n forward(log, info);\n } finally {\n callback();\n }\n },\n });\n // winston requires a `log(info, callback)` method on stream transports; writes go through write().\n Object.assign(transport, {\n log(info: Record<string, unknown>, callback: () => void) {\n forward(log, info);\n callback();\n },\n });\n return {\n level: \"silly\",\n // Identity format: replaces Strapi's prettyPrint so the transport receives raw info objects.\n format: { transform: <T>(info: T): T => info },\n transports: [transport],\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AAkCzB,SAAS,QAAQ,KAAyC;AACxD,MAAI,OAAO,IAAI,kBAAkB,SAAU,QAAO,IAAI;AACtD,QAAM,OAAQ,IAAI,MAAM,OAA0C;AAClE,SAAO,OAAO,SAAS,WAAW,OAAO;AAC3C;AAMO,SAAS,oBAAoB,MAAiD;AACnF,QAAM,WAAW,KAAK,YAAY;AAClC,SAAO,OAAO,KAAK,SAAS;AAC1B,UAAM,WAAW,IAAI,IAAI,cAAc;AACvC,UAAM,SAAS,YAAY,cAAc,KAAK,QAAQ,IAAI,WAAW;AACrE,UAAM,WAAW,IAAI,MAAM,QAAQ;AACnC,UAAM,YACJ,OAAO,aAAa,YAAY,WAAW,WAAW,KAAK,0BAA0B,SAAS,SAAS,aAAa;AAEtH,QAAI,IAAI,MAAM,QAAQ,MAAM,OAAW,KAAI,MAAM,QAAQ,IAAI;AAC7D,QAAI,IAAI,gBAAgB,SAAS;AAEjC,UAAM,SAAiB,EAAE,YAAY,UAAU;AAC/C,QAAI,UAAU,WAAW,UAAW,QAAO,sBAAsB;AACjE,UAAM,QAAQ,iBAAiB,IAAI,IAAI,aAAa,CAAC;AACrD,QAAI,MAAO,QAAO,OAAO,QAAQ,KAAK;AAEtC,UAAM,QAAQ,YAAY,IAAI;AAC9B,UAAM,aAAa,QAAQ,OAAO,UAAU;AAC1C,UAAI,SAAS;AACb,UAAI;AACF,cAAM,KAAK;AAAA,MACb,SAAS,KAAK;AAEZ,iBAAS;AACT,sBAAc,OAAO,GAAG;AACxB,cAAM;AAAA,MACR,UAAE;AACA;AAAA,UACE,KAAK;AAAA,UACL;AAAA,YACE,QAAQ,IAAI;AAAA,YACZ,OAAO,QAAQ,GAAG;AAAA,YAClB,MAAM,IAAI;AAAA,YACV,QAAQ,SAAS,MAAM,IAAI;AAAA,YAC3B,aAAa,YAAY,IAAI,IAAI;AAAA,YACjC,KAAK,MAAM;AAAA,YACX,QAAQ,MAAM;AAAA,UAChB;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAMO,SAAS,qBAAoC;AAClD,SAAO,OAAO,MAAM,SAAS;AAC3B,QAAI;AACF,YAAM,KAAK;AAAA,IACb,SAAS,KAAK;AACZ,YAAM,QAAQ,aAAa;AAC3B,UAAI,MAAO,eAAc,OAAO,GAAG;AACnC,iBAAW,GAAG;AACd,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEA,IAAM,SAAgC;AAAA,EACpC,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,OAAO;AACT;AAEA,IAAM,YAAY,oBAAI,IAAI,CAAC,SAAS,WAAW,OAAO,CAAC;AAEvD,SAAS,QAAQ,KAAa,MAAqC;AAEjE,MAAI,YAAY,IAAI,KAAK,SAAS,IAAI,EAAG;AACzC,QAAM,QAAQ,OAAO,OAAO,KAAK,KAAK,CAAC,KAAK;AAC5C,QAAM,SAAiB,CAAC;AACxB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,EAAG,KAAI,CAAC,UAAU,IAAI,GAAG,EAAG,QAAO,GAAG,IAAI;AAExF,MAAI,UAAmB,KAAK;AAC5B,MAAI,YAAY,IAAI,GAAG;AACrB,WAAO,MAAM;AAAA,EACf,WAAW,YAAY,OAAO,GAAG;AAC/B,QAAI,SAAS,OAAO,EAAG;AACvB,WAAO,MAAM;AACb,cAAU,QAAQ;AAAA,EACpB,WAAW,OAAO,KAAK,UAAU,UAAU;AACzC,WAAO,YAAY,KAAK;AAAA,EAC1B;AACA,OAAK,UAAU,UAAU,UAAU,YAAY,OAAO,UAAU,OAAW,QAAO,QAAQ;AAC1F,MAAI,KAAK,EAAE,OAAO,YAAY,WAAW,UAAU,OAAO,WAAW,EAAE,GAAG,MAAM;AAClF;AAOO,SAAS,mBAAmB,KAAa;AAC9C,QAAM,YAAY,IAAI,SAAS;AAAA,IAC7B,YAAY;AAAA,IACZ,MAAM,MAA+B,WAAW,UAAU;AACxD,UAAI;AACF,gBAAQ,KAAK,IAAI;AAAA,MACnB,UAAE;AACA,iBAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,OAAO,WAAW;AAAA,IACvB,IAAI,MAA+B,UAAsB;AACvD,cAAQ,KAAK,IAAI;AACjB,eAAS;AAAA,IACX;AAAA,EACF,CAAC;AACD,SAAO;AAAA,IACL,OAAO;AAAA;AAAA,IAEP,QAAQ,EAAE,WAAW,CAAI,SAAe,KAAK;AAAA,IAC7C,YAAY,CAAC,SAAS;AAAA,EACxB;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanngo11/log",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Structured JSON logging for Node services, implementing log contract v1",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -102,6 +102,16 @@
102
102
  "types": "./dist/prisma.d.cts",
103
103
  "default": "./dist/prisma.cjs"
104
104
  }
105
+ },
106
+ "./strapi": {
107
+ "import": {
108
+ "types": "./dist/strapi.d.ts",
109
+ "default": "./dist/strapi.js"
110
+ },
111
+ "require": {
112
+ "types": "./dist/strapi.d.cts",
113
+ "default": "./dist/strapi.cjs"
114
+ }
105
115
  }
106
116
  },
107
117
  "scripts": {
@@ -119,20 +129,25 @@
119
129
  }
120
130
  },
121
131
  "devDependencies": {
132
+ "@koa/router": "^12.0.2",
122
133
  "@nestjs/common": "^10.4.22",
123
134
  "@nestjs/core": "^10.4.22",
124
135
  "@nestjs/platform-express": "^10.4.22",
125
136
  "@nestjs/platform-fastify": "^10.4.22",
126
137
  "@prisma/client": "^6.19.3",
127
138
  "@types/express": "^4.17.25",
139
+ "@types/koa": "^2.15.2",
140
+ "@types/koa__router": "^12.0.5",
128
141
  "@types/node": "^22.20.4",
129
142
  "express": "^4.22.3",
130
143
  "fastify": "^5.12.5",
144
+ "koa": "^2.16.4",
131
145
  "prisma": "^6.19.3",
132
146
  "reflect-metadata": "^0.2.2",
133
147
  "rxjs": "^7.8.2",
134
148
  "tsup": "^8.5.1",
135
149
  "typescript": "^5.9.3",
136
- "vitest": "^3.2.7"
150
+ "vitest": "^3.2.7",
151
+ "winston": "^3.10.0"
137
152
  }
138
153
  }