@tanngo11/log 0.5.1 → 0.6.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRACT.md +1 -1
  3. package/README.md +18 -0
  4. package/dist/bullmq.cjs.map +1 -1
  5. package/dist/bullmq.d.cts +1 -1
  6. package/dist/bullmq.d.ts +1 -1
  7. package/dist/bullmq.js +2 -2
  8. package/dist/{chunk-UXKPPF4G.js → chunk-2WNGXGP7.js} +2 -2
  9. package/dist/{chunk-754Y5TQX.js → chunk-AF4YR4Y4.js} +14 -5
  10. package/dist/chunk-AF4YR4Y4.js.map +1 -0
  11. package/dist/{chunk-7DW57ECW.js → chunk-RVETZAK5.js} +3 -3
  12. package/dist/{chunk-QMC6BWGK.js → chunk-UDS6S22P.js} +28 -4
  13. package/dist/chunk-UDS6S22P.js.map +1 -0
  14. package/dist/{chunk-42ZPVKHN.js → chunk-UOIJCSP5.js} +3 -3
  15. package/dist/{chunk-35VPKN2C.js → chunk-URPHVC4Y.js} +2 -2
  16. package/dist/express.cjs +26 -4
  17. package/dist/express.cjs.map +1 -1
  18. package/dist/express.d.cts +2 -2
  19. package/dist/express.d.ts +2 -2
  20. package/dist/express.js +3 -3
  21. package/dist/fastify.cjs +26 -4
  22. package/dist/fastify.cjs.map +1 -1
  23. package/dist/fastify.d.cts +2 -2
  24. package/dist/fastify.d.ts +2 -2
  25. package/dist/fastify.js +3 -3
  26. package/dist/{http-BI6ARZlD.d.ts → http-44ztBSXk.d.ts} +1 -1
  27. package/dist/{http-BXAiU-rN.d.cts → http-BQJkHBwQ.d.cts} +1 -1
  28. package/dist/index.cjs +93 -6
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +47 -4
  31. package/dist/index.d.ts +47 -4
  32. package/dist/index.js +60 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/{logger-CFMTzjsm.d.ts → logger-3Co9eVJf.d.cts} +4 -1
  35. package/dist/{logger-CFMTzjsm.d.cts → logger-3Co9eVJf.d.ts} +4 -1
  36. package/dist/nest.cjs +26 -4
  37. package/dist/nest.cjs.map +1 -1
  38. package/dist/nest.d.cts +2 -2
  39. package/dist/nest.d.ts +2 -2
  40. package/dist/nest.js +4 -4
  41. package/dist/next.cjs +26 -4
  42. package/dist/next.cjs.map +1 -1
  43. package/dist/next.d.cts +2 -2
  44. package/dist/next.d.ts +2 -2
  45. package/dist/next.js +3 -3
  46. package/dist/prisma.d.cts +1 -1
  47. package/dist/prisma.d.ts +1 -1
  48. package/dist/strapi.cjs +26 -9
  49. package/dist/strapi.cjs.map +1 -1
  50. package/dist/strapi.d.cts +2 -2
  51. package/dist/strapi.d.ts +2 -2
  52. package/dist/strapi.js +3 -7
  53. package/dist/strapi.js.map +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-754Y5TQX.js.map +0 -1
  56. package/dist/chunk-QMC6BWGK.js.map +0 -1
  57. /package/dist/{chunk-UXKPPF4G.js.map → chunk-2WNGXGP7.js.map} +0 -0
  58. /package/dist/{chunk-7DW57ECW.js.map → chunk-RVETZAK5.js.map} +0 -0
  59. /package/dist/{chunk-42ZPVKHN.js.map → chunk-UOIJCSP5.js.map} +0 -0
  60. /package/dist/{chunk-35VPKN2C.js.map → chunk-URPHVC4Y.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ Changed:
6
+ - `knexLogging` writes `db.error` at `warn` by default (option `errorLevel`): apps catch some
7
+ errors on purpose, such as a unique violation that means "already exists". `ignoreCodes` skips
8
+ more SQLSTATE codes besides 25P02.
9
+
10
+ ## 0.6.0
11
+
12
+ Added:
13
+ - `knexLogging(knex, log)`: `db.slow_query` (`warn`, 500 ms by default) for knex and Strapi
14
+ (`strapi.db.connection`), with `db_operation`, `db_table`, `duration_ms`, `db_rows`. Never logs
15
+ SQL text or bindings. `logErrors: true` adds a `db.error` line per failed query, except 25P02
16
+ ("current transaction is aborted"), which only repeats an earlier failure.
17
+ - `isClientDisconnect(err)` and `stripSql(message)` are exported.
18
+
19
+ Fixed:
20
+ - `REDACT_PATTERNS.phoneVN` matched inside hex ids: a UUID segment such as `0912345678e0` became
21
+ `[REDACTED]e0`, also in `request_id`, which broke tracing that request. A letter next to the
22
+ digits now rules a match out.
23
+ - `request_id`, `trace_id`, `span_id` and `*_request_id` are never passed through value
24
+ redaction when they are plain tokens.
25
+ - Postgres errors from knex: `err_message` was the whole statement followed by the server's
26
+ reason, and truncation at 1000 characters cut the reason off. A SQLSTATE error now keeps only
27
+ the reason and adds `db_table` and `db_constraint`. `err_type` is `DatabaseError` instead of pg's
28
+ lowercase `error`.
29
+
30
+ Changed:
31
+ - An `http.request` line whose recorded error is a client disconnect (`aborted`, `ECONNRESET`...)
32
+ is `warn` with `client_disconnect: true`, even when the app answered 500.
33
+
3
34
  ## 0.5.1
4
35
 
5
36
  Changed:
package/CONTRACT.md CHANGED
@@ -34,7 +34,7 @@ stream, nested fields flattened with `_`) → MCP for AI agents.
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) |
37
- | Database | `db_system`, `db_operation`, `db_table`, `db_rows`, `duration_ms`, `db_statement` (parameterized only) |
37
+ | Database | `db_system`, `db_operation`, `db_table`, `db_rows`, `duration_ms`, `db_statement` (parameterized only), `db_constraint` |
38
38
 
39
39
  Naming rules for domain fields:
40
40
  - ids: `<entity>_id`;
package/README.md CHANGED
@@ -346,6 +346,24 @@ export const prisma = new PrismaClient().$extends(prismaLogging(log, { db_system
346
346
  `http.request` line with `err_code` (`P2002`...), and the contract logs each error once. Pass
347
347
  `logErrors: true` to also get a `db.error` line.
348
348
 
349
+ ## knex and Strapi queries
350
+
351
+ ```ts
352
+ import { knexLogging } from "@tanngo11/log";
353
+
354
+ // Strapi: in bootstrap() of src/index.ts
355
+ knexLogging(strapi.db.connection, log, { db_system: "postgres" });
356
+ ```
357
+
358
+ - Logs `db.slow_query` (`warn`) for queries that take 500 ms or more (`slowMs`). Fields:
359
+ `db_operation`, `db_table`, `duration_ms`, `db_rows`. Never logs SQL text or bindings.
360
+ - `logErrors: true` also writes `db.error` for each failed query. Use it when the app catches
361
+ errors inside transactions: Postgres then fails every later query with 25P02, and only the
362
+ `db.error` line names the query that failed first. 25P02 lines are skipped. `db.error` is
363
+ `warn` by default (`errorLevel`); `ignoreCodes` skips expected codes such as `23505`.
364
+ - Postgres errors keep only the server's reason in `err_message` (knex prefixes the statement)
365
+ and carry `db_table` and `db_constraint`.
366
+
349
367
  ## Migrating from the shared winston logger
350
368
 
351
369
  For repos using the old shared winston logger (S3 transport, nested metadata):
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bullmq.ts","../src/context.ts","../src/global.ts","../src/error.ts","../src/job.ts"],"sourcesContent":["import { runJob } from \"./job\";\nimport type { Logger } from \"./logger\";\n\n/** Minimal shape of a BullMQ Job; avoids depending on bullmq. */\nexport interface BullJobLike {\n id?: string;\n attemptsMade: number;\n opts?: { attempts?: number };\n data?: unknown;\n}\n\nexport function loggedProcessor<J extends BullJobLike, R, A extends unknown[] = [token?: string, signal?: AbortSignal]>(\n log: Logger,\n name: string,\n processor: (job: J, ...rest: A) => Promise<R>,\n): (job: J, ...rest: A) => Promise<R> {\n return (job, ...rest) => {\n const attempts = job.opts?.attempts ?? 1;\n const attempt = job.attemptsMade + 1;\n const requestId = (job.data as { request_id?: unknown } | undefined)?.request_id;\n return runJob(\n log,\n {\n job_name: name,\n job_id: job.id,\n job_attempt: attempt,\n // UnrecoverableError skips the remaining retries.\n final_attempt: (err) => attempt >= attempts || (err as { name?: unknown } | null)?.name === \"UnrecoverableError\",\n request_id: typeof requestId === \"string\" ? requestId : undefined,\n },\n () => processor(job, ...rest),\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","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 { getContext, withContext } from \"./context\";\nimport { markLogged } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\nexport interface JobInfo {\n job_name: string;\n job_id?: string | number;\n job_attempt?: number;\n /** No retries left after this attempt (or decided per error). Defaults to true when unknown. */\n final_attempt?: boolean | ((err: unknown) => boolean);\n request_id?: string;\n fields?: Fields;\n}\n\nexport async function runJob<T>(log: Logger, info: JobInfo, fn: () => T | Promise<T>): Promise<T> {\n const ctx: Fields = { ...info.fields, job_name: info.job_name };\n if (info.job_id !== undefined) ctx.job_id = String(info.job_id);\n if (info.job_attempt !== undefined) ctx.job_attempt = info.job_attempt;\n if (info.request_id !== undefined) ctx.request_id = info.request_id;\n\n return withContext(ctx, async () => {\n const start = performance.now();\n try {\n const result = await fn();\n log.info(\"job completed\", { event: \"job.completed\", duration_ms: Math.round(performance.now() - start) });\n return result;\n } catch (err) {\n const final = typeof info.final_attempt === \"function\" ? info.final_attempt(err) : (info.final_attempt ?? true);\n log[final ? \"error\" : \"warn\"](\"job failed\", {\n event: \"job.failed\",\n duration_ms: Math.round(performance.now() - start),\n err,\n });\n markLogged(err);\n throw err;\n }\n });\n}\n\n/** Adds the current request_id to a job payload, so the job's logs link back to the request. */\nexport function jobData<T extends Record<string, unknown>>(data: T): T & { request_id?: string } {\n const requestId = getContext().request_id;\n if (typeof requestId !== \"string\" || data.request_id !== undefined) return data;\n return { ...data, request_id: requestId };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,8BAAkC;;;ACE3B,SAAS,gBAAmB,MAAc,MAAkB;AACjE,QAAM,MAAM,uBAAO,IAAI,iBAAiB,IAAI,EAAE;AAC9C,QAAM,IAAI;AACV,MAAI,QAAQ,EAAE,GAAG;AACjB,MAAI,UAAU,QAAW;AACvB,YAAQ,KAAK;AACb,MAAE,GAAG,IAAI;AAAA,EACX;AACA,SAAO;AACT;;;ADCA,IAAM,MAAM,gBAAgB,OAAO,MAAM,IAAI,0CAAgC,CAAC;AAIvE,SAAS,aAAgB,QAAgB,IAAmC;AACjF,QAAM,SAAS,IAAI,SAAS;AAC5B,QAAM,QAAsB,EAAE,QAAQ,EAAE,GAAG,QAAQ,QAAQ,GAAG,OAAO,GAAG,OAAO;AAC/E,SAAO,IAAI,IAAI,OAAO,MAAM,GAAG,KAAK,CAAC;AACvC;AAEO,SAAS,YAAe,QAAgB,IAAgB;AAC7D,SAAO,aAAa,QAAQ,MAAM,GAAG,CAAC;AACxC;;;AE8BA,IAAM,SAAS,gBAAgB,UAAU,MAAM,oBAAI,QAAgB,CAAC;AAI7D,SAAS,WAAW,KAAoB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO,IAAI,GAAG;AAC7D;;;AC7CA,eAAsB,OAAU,KAAa,MAAe,IAAsC;AAChG,QAAM,MAAc,EAAE,GAAG,KAAK,QAAQ,UAAU,KAAK,SAAS;AAC9D,MAAI,KAAK,WAAW,OAAW,KAAI,SAAS,OAAO,KAAK,MAAM;AAC9D,MAAI,KAAK,gBAAgB,OAAW,KAAI,cAAc,KAAK;AAC3D,MAAI,KAAK,eAAe,OAAW,KAAI,aAAa,KAAK;AAEzD,SAAO,YAAY,KAAK,YAAY;AAClC,UAAM,QAAQ,YAAY,IAAI;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,GAAG;AACxB,UAAI,KAAK,iBAAiB,EAAE,OAAO,iBAAiB,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK,EAAE,CAAC;AACxG,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAM,QAAQ,OAAO,KAAK,kBAAkB,aAAa,KAAK,cAAc,GAAG,IAAK,KAAK,iBAAiB;AAC1G,UAAI,QAAQ,UAAU,MAAM,EAAE,cAAc;AAAA,QAC1C,OAAO;AAAA,QACP,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,QACjD;AAAA,MACF,CAAC;AACD,iBAAW,GAAG;AACd,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;;;AJ3BO,SAAS,gBACd,KACA,MACA,WACoC;AACpC,SAAO,CAAC,QAAQ,SAAS;AACvB,UAAM,WAAW,IAAI,MAAM,YAAY;AACvC,UAAM,UAAU,IAAI,eAAe;AACnC,UAAM,YAAa,IAAI,MAA+C;AACtE,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,UAAU;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,aAAa;AAAA;AAAA,QAEb,eAAe,CAAC,QAAQ,WAAW,YAAa,KAAmC,SAAS;AAAA,QAC5F,YAAY,OAAO,cAAc,WAAW,YAAY;AAAA,MAC1D;AAAA,MACA,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/bullmq.ts","../src/context.ts","../src/global.ts","../src/error.ts","../src/job.ts"],"sourcesContent":["import { runJob } from \"./job\";\nimport type { Logger } from \"./logger\";\n\n/** Minimal shape of a BullMQ Job; avoids depending on bullmq. */\nexport interface BullJobLike {\n id?: string;\n attemptsMade: number;\n opts?: { attempts?: number };\n data?: unknown;\n}\n\nexport function loggedProcessor<J extends BullJobLike, R, A extends unknown[] = [token?: string, signal?: AbortSignal]>(\n log: Logger,\n name: string,\n processor: (job: J, ...rest: A) => Promise<R>,\n): (job: J, ...rest: A) => Promise<R> {\n return (job, ...rest) => {\n const attempts = job.opts?.attempts ?? 1;\n const attempt = job.attemptsMade + 1;\n const requestId = (job.data as { request_id?: unknown } | undefined)?.request_id;\n return runJob(\n log,\n {\n job_name: name,\n job_id: job.id,\n job_attempt: attempt,\n // UnrecoverableError skips the remaining retries.\n final_attempt: (err) => attempt >= attempts || (err as { name?: unknown } | null)?.name === \"UnrecoverableError\",\n request_id: typeof requestId === \"string\" ? requestId : undefined,\n },\n () => processor(job, ...rest),\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","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 /** Database errors (pg): the table and constraint the server reported. */\n db_table?: string;\n db_constraint?: 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 // pg's DatabaseError keeps the lowercase name \"error\"; its class name says more.\n if (e.name && e.name !== \"Error\" && 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; table?: unknown; constraint?: 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 (isSqlState(out.err_code)) {\n out.err_message = stripSql(out.err_message ?? \"\");\n if (typeof e.table === \"string\" && e.table) out.db_table = e.table;\n if (typeof e.constraint === \"string\" && e.constraint) out.db_constraint = e.constraint;\n }\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\n// Postgres SQLSTATE codes: five digits or uppercase letters (23505, 40P01, 25P02).\nfunction isSqlState(code: string | undefined): boolean {\n return code !== undefined && /^[0-9A-Z]{5}$/.test(code);\n}\n\nconst SQL_START_RE = /^\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n\n/**\n * knex prefixes a failed query's message with the whole statement (\"update ... - deadlock\n * detected\"). A long statement filled err_message and the truncation cut off the server's reason,\n * the only part worth reading. Keeps the reason; the statement never reaches the log.\n */\nexport function stripSql(message: string): string {\n if (!SQL_START_RE.test(message)) return message;\n const at = message.lastIndexOf(\" - \");\n return at === -1 ? message : message.slice(at + 3);\n}\n\n// The client hung up or sent a malformed request: worth a warn, not an error on our side.\nexport function isClientDisconnect(err: unknown): boolean {\n const e = err as { code?: unknown; message?: unknown } | null;\n const code = typeof e?.code === \"string\" ? e.code : \"\";\n return (\n code === \"ECONNRESET\" ||\n code === \"ECONNABORTED\" ||\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_PREMATURE_CLOSE\" ||\n code.startsWith(\"HPE_\") ||\n e?.message === \"aborted\"\n );\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 { getContext, withContext } from \"./context\";\nimport { markLogged } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\nexport interface JobInfo {\n job_name: string;\n job_id?: string | number;\n job_attempt?: number;\n /** No retries left after this attempt (or decided per error). Defaults to true when unknown. */\n final_attempt?: boolean | ((err: unknown) => boolean);\n request_id?: string;\n fields?: Fields;\n}\n\nexport async function runJob<T>(log: Logger, info: JobInfo, fn: () => T | Promise<T>): Promise<T> {\n const ctx: Fields = { ...info.fields, job_name: info.job_name };\n if (info.job_id !== undefined) ctx.job_id = String(info.job_id);\n if (info.job_attempt !== undefined) ctx.job_attempt = info.job_attempt;\n if (info.request_id !== undefined) ctx.request_id = info.request_id;\n\n return withContext(ctx, async () => {\n const start = performance.now();\n try {\n const result = await fn();\n log.info(\"job completed\", { event: \"job.completed\", duration_ms: Math.round(performance.now() - start) });\n return result;\n } catch (err) {\n const final = typeof info.final_attempt === \"function\" ? info.final_attempt(err) : (info.final_attempt ?? true);\n log[final ? \"error\" : \"warn\"](\"job failed\", {\n event: \"job.failed\",\n duration_ms: Math.round(performance.now() - start),\n err,\n });\n markLogged(err);\n throw err;\n }\n });\n}\n\n/** Adds the current request_id to a job payload, so the job's logs link back to the request. */\nexport function jobData<T extends Record<string, unknown>>(data: T): T & { request_id?: string } {\n const requestId = getContext().request_id;\n if (typeof requestId !== \"string\" || data.request_id !== undefined) return data;\n return { ...data, request_id: requestId };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,8BAAkC;;;ACE3B,SAAS,gBAAmB,MAAc,MAAkB;AACjE,QAAM,MAAM,uBAAO,IAAI,iBAAiB,IAAI,EAAE;AAC9C,QAAM,IAAI;AACV,MAAI,QAAQ,EAAE,GAAG;AACjB,MAAI,UAAU,QAAW;AACvB,YAAQ,KAAK;AACb,MAAE,GAAG,IAAI;AAAA,EACX;AACA,SAAO;AACT;;;ADCA,IAAM,MAAM,gBAAgB,OAAO,MAAM,IAAI,0CAAgC,CAAC;AAIvE,SAAS,aAAgB,QAAgB,IAAmC;AACjF,QAAM,SAAS,IAAI,SAAS;AAC5B,QAAM,QAAsB,EAAE,QAAQ,EAAE,GAAG,QAAQ,QAAQ,GAAG,OAAO,GAAG,OAAO;AAC/E,SAAO,IAAI,IAAI,OAAO,MAAM,GAAG,KAAK,CAAC;AACvC;AAEO,SAAS,YAAe,QAAgB,IAAgB;AAC7D,SAAO,aAAa,QAAQ,MAAM,GAAG,CAAC;AACxC;;;AEuEA,IAAM,SAAS,gBAAgB,UAAU,MAAM,oBAAI,QAAgB,CAAC;AAI7D,SAAS,WAAW,KAAoB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO,IAAI,GAAG;AAC7D;;;ACtFA,eAAsB,OAAU,KAAa,MAAe,IAAsC;AAChG,QAAM,MAAc,EAAE,GAAG,KAAK,QAAQ,UAAU,KAAK,SAAS;AAC9D,MAAI,KAAK,WAAW,OAAW,KAAI,SAAS,OAAO,KAAK,MAAM;AAC9D,MAAI,KAAK,gBAAgB,OAAW,KAAI,cAAc,KAAK;AAC3D,MAAI,KAAK,eAAe,OAAW,KAAI,aAAa,KAAK;AAEzD,SAAO,YAAY,KAAK,YAAY;AAClC,UAAM,QAAQ,YAAY,IAAI;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,GAAG;AACxB,UAAI,KAAK,iBAAiB,EAAE,OAAO,iBAAiB,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK,EAAE,CAAC;AACxG,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAM,QAAQ,OAAO,KAAK,kBAAkB,aAAa,KAAK,cAAc,GAAG,IAAK,KAAK,iBAAiB;AAC1G,UAAI,QAAQ,UAAU,MAAM,EAAE,cAAc;AAAA,QAC1C,OAAO;AAAA,QACP,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,QACjD;AAAA,MACF,CAAC;AACD,iBAAW,GAAG;AACd,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;;;AJ3BO,SAAS,gBACd,KACA,MACA,WACoC;AACpC,SAAO,CAAC,QAAQ,SAAS;AACvB,UAAM,WAAW,IAAI,MAAM,YAAY;AACvC,UAAM,UAAU,IAAI,eAAe;AACnC,UAAM,YAAa,IAAI,MAA+C;AACtE,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,UAAU;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,aAAa;AAAA;AAAA,QAEb,eAAe,CAAC,QAAQ,WAAW,YAAa,KAAmC,SAAS;AAAA,QAC5F,YAAY,OAAO,cAAc,WAAW,YAAY;AAAA,MAC1D;AAAA,MACA,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;","names":[]}
package/dist/bullmq.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { L as Logger } from './logger-CFMTzjsm.cjs';
1
+ import { L as Logger } from './logger-3Co9eVJf.cjs';
2
2
 
3
3
  /** Minimal shape of a BullMQ Job; avoids depending on bullmq. */
4
4
  interface BullJobLike {
package/dist/bullmq.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { L as Logger } from './logger-CFMTzjsm.js';
1
+ import { L as Logger } from './logger-3Co9eVJf.js';
2
2
 
3
3
  /** Minimal shape of a BullMQ Job; avoids depending on bullmq. */
4
4
  interface BullJobLike {
package/dist/bullmq.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runJob
3
- } from "./chunk-UXKPPF4G.js";
4
- import "./chunk-QMC6BWGK.js";
3
+ } from "./chunk-2WNGXGP7.js";
4
+ import "./chunk-UDS6S22P.js";
5
5
 
6
6
  // src/bullmq.ts
7
7
  function loggedProcessor(log, name, processor) {
@@ -2,7 +2,7 @@ import {
2
2
  getContext,
3
3
  markLogged,
4
4
  withContext
5
- } from "./chunk-QMC6BWGK.js";
5
+ } from "./chunk-UDS6S22P.js";
6
6
 
7
7
  // src/job.ts
8
8
  async function runJob(log, info, fn) {
@@ -38,4 +38,4 @@ export {
38
38
  runJob,
39
39
  jobData
40
40
  };
41
- //# sourceMappingURL=chunk-UXKPPF4G.js.map
41
+ //# sourceMappingURL=chunk-2WNGXGP7.js.map
@@ -5,11 +5,12 @@ import {
5
5
  import {
6
6
  REDACTED,
7
7
  getContext,
8
+ isClientDisconnect,
8
9
  isErrorLike,
9
10
  markLogged,
10
11
  serializeError,
11
12
  toSnakeCase
12
- } from "./chunk-QMC6BWGK.js";
13
+ } from "./chunk-UDS6S22P.js";
13
14
 
14
15
  // src/normalize.ts
15
16
  var HOOK_EXEMPT = /* @__PURE__ */ new Set(["level", "message", "event"]);
@@ -95,8 +96,13 @@ function flattenInto(out, prefix, obj, depth, opts) {
95
96
  }
96
97
  }
97
98
  }
99
+ var ID_VALUE_RE = /^[A-Za-z0-9._:-]{1,128}$/;
100
+ function isIdKey(key) {
101
+ return key === "request_id" || key === "trace_id" || key === "span_id" || key.endsWith("_request_id");
102
+ }
98
103
  function finalize(key, value, opts) {
99
104
  if (typeof value !== "string") return value;
105
+ if (isIdKey(key) && ID_VALUE_RE.test(value)) return value;
100
106
  const v = opts.redactor.scrub(isUrlKey(key) ? stripUrl(value) : value);
101
107
  return truncate(v, key === "err_stack" ? MAX_STACK : MAX_STRING);
102
108
  }
@@ -178,12 +184,15 @@ function shouldSkip(method, path, skipPaths = DEFAULT_SKIP_PATHS) {
178
184
  }
179
185
  function logHttpRequest(log, info, opts = {}) {
180
186
  if (info.status < 500 && shouldSkip(info.method, info.path, opts.skipPaths)) return;
181
- const level = info.status >= 500 ? "error" : info.status >= 400 ? "warn" : "info";
187
+ const hasErr = info.err !== void 0 && info.err !== null;
188
+ const clientGone = hasErr && isClientDisconnect(info.err);
189
+ const level = info.status >= 500 && !clientGone ? "error" : info.status >= 400 ? "warn" : "info";
182
190
  const path = stripUrl(info.path);
183
191
  const errFields = {};
184
- if (info.err !== void 0 && info.err !== null) {
192
+ if (hasErr) {
185
193
  Object.assign(errFields, serializeError(info.err));
186
- if (info.status < 500) delete errFields.err_stack;
194
+ if (info.status < 500 || clientGone) delete errFields.err_stack;
195
+ if (clientGone) errFields.client_disconnect = true;
187
196
  markLogged(info.err);
188
197
  }
189
198
  const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;
@@ -207,4 +216,4 @@ export {
207
216
  DEFAULT_SKIP_PATHS,
208
217
  logHttpRequest
209
218
  };
210
- //# sourceMappingURL=chunk-754Y5TQX.js.map
219
+ //# sourceMappingURL=chunk-AF4YR4Y4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/normalize.ts","../src/http.ts"],"sourcesContent":["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\n// Correlation ids are opaque tokens: value patterns such as phoneVN can match inside a hex\n// segment and break the join between services, so a well-formed id is never scrubbed.\nconst ID_VALUE_RE = /^[A-Za-z0-9._:-]{1,128}$/;\n\nfunction isIdKey(key: string): boolean {\n return key === \"request_id\" || key === \"trace_id\" || key === \"span_id\" || key.endsWith(\"_request_id\");\n}\n\nfunction finalize(key: string, value: Primitive, opts: NormalizeOptions): Primitive {\n if (typeof value !== \"string\") return value;\n if (isIdKey(key) && ID_VALUE_RE.test(value)) 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","import { getContext } from \"./context\";\nimport { isClientDisconnect, 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 // Also under a prefix such as /api/v1/health.\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. Skipped paths are still\n// logged when they fail with a 5xx, so a failing health check stays visible.\nexport function logHttpRequest(log: Logger, info: HttpRequestInfo, opts: HttpLogOptions = {}): void {\n if (info.status < 500 && shouldSkip(info.method, info.path, opts.skipPaths)) return;\n const hasErr = info.err !== undefined && info.err !== null;\n // A client that hung up mid-request is not a server failure, whatever status the app set.\n const clientGone = hasErr && isClientDisconnect(info.err);\n const level = info.status >= 500 && !clientGone ? \"error\" : info.status >= 400 ? \"warn\" : \"info\";\n const path = stripUrl(info.path);\n const errFields: Fields = {};\n if (hasErr) {\n Object.assign(errFields, serializeError(info.err));\n if (info.status < 500 || clientGone) delete errFields.err_stack;\n if (clientGone) errFields.client_disconnect = true;\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":[]}
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  logHttpRequest,
3
3
  requestContextFields
4
- } from "./chunk-754Y5TQX.js";
4
+ } from "./chunk-AF4YR4Y4.js";
5
5
  import {
6
6
  attachStore,
7
7
  recordErrorIn,
8
8
  runWithStore,
9
9
  storeOf
10
- } from "./chunk-QMC6BWGK.js";
10
+ } from "./chunk-UDS6S22P.js";
11
11
 
12
12
  // src/fastify.ts
13
13
  var starts = /* @__PURE__ */ new WeakMap();
@@ -63,4 +63,4 @@ var fastifyLogger = plugin;
63
63
  export {
64
64
  fastifyLogger
65
65
  };
66
- //# sourceMappingURL=chunk-7DW57ECW.js.map
66
+ //# sourceMappingURL=chunk-RVETZAK5.js.map
@@ -81,8 +81,11 @@ var DEFAULT_REDACT_KEYS = [
81
81
  "bearer"
82
82
  ];
83
83
  var REDACT_PATTERNS = {
84
- /** Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. */
85
- phoneVN: /(?<![\d+])(?:\+?84|0)[35789]\d{8}(?!\d)/g,
84
+ /**
85
+ * Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. Letters on either side
86
+ * rule a match out, so hex ids (a UUID segment such as 0912345678e0) are left alone.
87
+ */
88
+ phoneVN: /(?<![\dA-Za-z+])(?:\+?84|0)[35789]\d{8}(?![\dA-Za-z])/g,
86
89
  /** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */
87
90
  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,
88
91
  /** IPv4 addresses. */
@@ -144,7 +147,7 @@ function isErrorLike(v) {
144
147
  return typeof v === "object" && v !== null && typeof v.message === "string" && ("stack" in v || "name" in v);
145
148
  }
146
149
  function errorType(e) {
147
- if (e.name && e.name !== "Error") return e.name;
150
+ if (e.name && e.name !== "Error" && e.name !== "error") return e.name;
148
151
  const ctor = e.constructor?.name;
149
152
  return ctor && ctor !== "Object" ? ctor : "Error";
150
153
  }
@@ -156,6 +159,11 @@ function serializeError(err) {
156
159
  const e = err;
157
160
  const out = { err_type: errorType(e), err_message: String(e.message ?? "") };
158
161
  if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
162
+ if (isSqlState(out.err_code)) {
163
+ out.err_message = stripSql(out.err_message ?? "");
164
+ if (typeof e.table === "string" && e.table) out.db_table = e.table;
165
+ if (typeof e.constraint === "string" && e.constraint) out.db_constraint = e.constraint;
166
+ }
159
167
  if (typeof e.stack === "string") out.err_stack = e.stack;
160
168
  const causes = [];
161
169
  let cause = e.cause;
@@ -171,6 +179,20 @@ function serializeError(err) {
171
179
  if (causes.length > 0) out.err_cause = causes.join(" <- ");
172
180
  return out;
173
181
  }
182
+ function isSqlState(code) {
183
+ return code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
184
+ }
185
+ var SQL_START_RE = /^\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
186
+ function stripSql(message) {
187
+ if (!SQL_START_RE.test(message)) return message;
188
+ const at = message.lastIndexOf(" - ");
189
+ return at === -1 ? message : message.slice(at + 3);
190
+ }
191
+ function isClientDisconnect(err) {
192
+ const e = err;
193
+ const code = typeof e?.code === "string" ? e.code : "";
194
+ return code === "ECONNRESET" || code === "ECONNABORTED" || code === "EPIPE" || code === "ERR_STREAM_PREMATURE_CLOSE" || code.startsWith("HPE_") || e?.message === "aborted";
195
+ }
174
196
  var logged = globalSingleton("logged", () => /* @__PURE__ */ new WeakSet());
175
197
  function markLogged(err) {
176
198
  if (typeof err === "object" && err !== null) logged.add(err);
@@ -197,7 +219,9 @@ export {
197
219
  defaultRedactor,
198
220
  isErrorLike,
199
221
  serializeError,
222
+ stripSql,
223
+ isClientDisconnect,
200
224
  markLogged,
201
225
  isLogged
202
226
  };
203
- //# sourceMappingURL=chunk-QMC6BWGK.js.map
227
+ //# sourceMappingURL=chunk-UDS6S22P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/global.ts","../src/redact.ts","../src/error.ts"],"sourcesContent":["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 /**\n * Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. Letters on either side\n * rule a match out, so hex ids (a UUID segment such as 0912345678e0) are left alone.\n */\n phoneVN: /(?<![\\dA-Za-z+])(?:\\+?84|0)[35789]\\d{8}(?![\\dA-Za-z])/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 /** Database errors (pg): the table and constraint the server reported. */\n db_table?: string;\n db_constraint?: 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 // pg's DatabaseError keeps the lowercase name \"error\"; its class name says more.\n if (e.name && e.name !== \"Error\" && 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; table?: unknown; constraint?: 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 (isSqlState(out.err_code)) {\n out.err_message = stripSql(out.err_message ?? \"\");\n if (typeof e.table === \"string\" && e.table) out.db_table = e.table;\n if (typeof e.constraint === \"string\" && e.constraint) out.db_constraint = e.constraint;\n }\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\n// Postgres SQLSTATE codes: five digits or uppercase letters (23505, 40P01, 25P02).\nfunction isSqlState(code: string | undefined): boolean {\n return code !== undefined && /^[0-9A-Z]{5}$/.test(code);\n}\n\nconst SQL_START_RE = /^\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n\n/**\n * knex prefixes a failed query's message with the whole statement (\"update ... - deadlock\n * detected\"). A long statement filled err_message and the truncation cut off the server's reason,\n * the only part worth reading. Keeps the reason; the statement never reaches the log.\n */\nexport function stripSql(message: string): string {\n if (!SQL_START_RE.test(message)) return message;\n const at = message.lastIndexOf(\" - \");\n return at === -1 ? message : message.slice(at + 3);\n}\n\n// The client hung up or sent a malformed request: worth a warn, not an error on our side.\nexport function isClientDisconnect(err: unknown): boolean {\n const e = err as { code?: unknown; message?: unknown } | null;\n const code = typeof e?.code === \"string\" ? e.code : \"\";\n return (\n code === \"ECONNRESET\" ||\n code === \"ECONNABORTED\" ||\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_PREMATURE_CLOSE\" ||\n code.startsWith(\"HPE_\") ||\n e?.message === \"aborted\"\n );\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"],"mappings":"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","names":[]}
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  logHttpRequest,
3
3
  requestContextFields
4
- } from "./chunk-754Y5TQX.js";
4
+ } from "./chunk-AF4YR4Y4.js";
5
5
  import {
6
6
  attachStore,
7
7
  recordErrorIn,
8
8
  runWithStore,
9
9
  storeOf
10
- } from "./chunk-QMC6BWGK.js";
10
+ } from "./chunk-UDS6S22P.js";
11
11
 
12
12
  // src/express.ts
13
13
  function joinRoute(baseUrl, path) {
@@ -69,4 +69,4 @@ export {
69
69
  requestLogger,
70
70
  errorRecorder
71
71
  };
72
- //# sourceMappingURL=chunk-42ZPVKHN.js.map
72
+ //# sourceMappingURL=chunk-UOIJCSP5.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  globalSingleton
3
- } from "./chunk-QMC6BWGK.js";
3
+ } from "./chunk-UDS6S22P.js";
4
4
 
5
5
  // src/process.ts
6
6
  function logStartup(log, fields = {}) {
@@ -83,4 +83,4 @@ export {
83
83
  installProcessHandlers,
84
84
  logProcessWarnings
85
85
  };
86
- //# sourceMappingURL=chunk-35VPKN2C.js.map
86
+ //# sourceMappingURL=chunk-URPHVC4Y.js.map
package/dist/express.cjs CHANGED
@@ -147,7 +147,7 @@ function isErrorLike(v) {
147
147
  return typeof v === "object" && v !== null && typeof v.message === "string" && ("stack" in v || "name" in v);
148
148
  }
149
149
  function errorType(e) {
150
- if (e.name && e.name !== "Error") return e.name;
150
+ if (e.name && e.name !== "Error" && e.name !== "error") return e.name;
151
151
  const ctor = e.constructor?.name;
152
152
  return ctor && ctor !== "Object" ? ctor : "Error";
153
153
  }
@@ -159,6 +159,11 @@ function serializeError(err) {
159
159
  const e = err;
160
160
  const out = { err_type: errorType(e), err_message: String(e.message ?? "") };
161
161
  if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
162
+ if (isSqlState(out.err_code)) {
163
+ out.err_message = stripSql(out.err_message ?? "");
164
+ if (typeof e.table === "string" && e.table) out.db_table = e.table;
165
+ if (typeof e.constraint === "string" && e.constraint) out.db_constraint = e.constraint;
166
+ }
162
167
  if (typeof e.stack === "string") out.err_stack = e.stack;
163
168
  const causes = [];
164
169
  let cause = e.cause;
@@ -174,6 +179,20 @@ function serializeError(err) {
174
179
  if (causes.length > 0) out.err_cause = causes.join(" <- ");
175
180
  return out;
176
181
  }
182
+ function isSqlState(code) {
183
+ return code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
184
+ }
185
+ var SQL_START_RE = /^\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
186
+ function stripSql(message) {
187
+ if (!SQL_START_RE.test(message)) return message;
188
+ const at = message.lastIndexOf(" - ");
189
+ return at === -1 ? message : message.slice(at + 3);
190
+ }
191
+ function isClientDisconnect(err) {
192
+ const e = err;
193
+ const code = typeof e?.code === "string" ? e.code : "";
194
+ return code === "ECONNRESET" || code === "ECONNABORTED" || code === "EPIPE" || code === "ERR_STREAM_PREMATURE_CLOSE" || code.startsWith("HPE_") || e?.message === "aborted";
195
+ }
177
196
  var logged = globalSingleton("logged", () => /* @__PURE__ */ new WeakSet());
178
197
  function markLogged(err) {
179
198
  if (typeof err === "object" && err !== null) logged.add(err);
@@ -233,12 +252,15 @@ function shouldSkip(method, path, skipPaths = DEFAULT_SKIP_PATHS) {
233
252
  }
234
253
  function logHttpRequest(log, info, opts = {}) {
235
254
  if (info.status < 500 && shouldSkip(info.method, info.path, opts.skipPaths)) return;
236
- const level = info.status >= 500 ? "error" : info.status >= 400 ? "warn" : "info";
255
+ const hasErr = info.err !== void 0 && info.err !== null;
256
+ const clientGone = hasErr && isClientDisconnect(info.err);
257
+ const level = info.status >= 500 && !clientGone ? "error" : info.status >= 400 ? "warn" : "info";
237
258
  const path = stripUrl(info.path);
238
259
  const errFields = {};
239
- if (info.err !== void 0 && info.err !== null) {
260
+ if (hasErr) {
240
261
  Object.assign(errFields, serializeError(info.err));
241
- if (info.status < 500) delete errFields.err_stack;
262
+ if (info.status < 500 || clientGone) delete errFields.err_stack;
263
+ if (clientGone) errFields.client_disconnect = true;
242
264
  markLogged(info.err);
243
265
  }
244
266
  const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;