@tanngo11/log 0.6.2 → 0.6.3
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 +13 -0
- package/dist/bullmq.cjs.map +1 -1
- package/dist/bullmq.d.cts +1 -1
- package/dist/bullmq.d.ts +1 -1
- package/dist/bullmq.js +2 -2
- package/dist/{chunk-UR7V5WL2.js → chunk-2IIUQFMK.js} +2 -2
- package/dist/{chunk-RZRM4TPC.js → chunk-BFMFXDIJ.js} +2 -2
- package/dist/{chunk-56ZXAHBA.js → chunk-EZCIMEVL.js} +3 -3
- package/dist/{chunk-4KULL3KT.js → chunk-JKCR57WJ.js} +25 -9
- package/dist/chunk-JKCR57WJ.js.map +1 -0
- package/dist/{chunk-22KZ7NGW.js → chunk-MGAEY7UZ.js} +2 -2
- package/dist/{chunk-3IK6W7P2.js → chunk-RQWNJJIT.js} +3 -3
- package/dist/express.cjs +20 -5
- package/dist/express.cjs.map +1 -1
- package/dist/express.d.cts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/express.js +3 -3
- package/dist/fastify.cjs +20 -5
- package/dist/fastify.cjs.map +1 -1
- package/dist/fastify.d.cts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/fastify.js +3 -3
- package/dist/{http-44ztBSXk.d.ts → http-Dp6Qtr9M.d.ts} +1 -1
- package/dist/{http-BQJkHBwQ.d.cts → http-eeog5tR8.d.cts} +1 -1
- package/dist/index.cjs +24 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -4
- package/dist/{logger-3Co9eVJf.d.cts → logger-CtlC9SUE.d.cts} +3 -2
- package/dist/{logger-3Co9eVJf.d.ts → logger-CtlC9SUE.d.ts} +3 -2
- package/dist/nest.cjs +20 -5
- package/dist/nest.cjs.map +1 -1
- package/dist/nest.d.cts +2 -2
- package/dist/nest.d.ts +2 -2
- package/dist/nest.js +4 -4
- package/dist/next.cjs +20 -5
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +2 -2
- package/dist/next.d.ts +2 -2
- package/dist/next.js +3 -3
- package/dist/prisma.d.cts +1 -1
- package/dist/prisma.d.ts +1 -1
- package/dist/strapi.cjs +20 -5
- package/dist/strapi.cjs.map +1 -1
- package/dist/strapi.d.cts +2 -2
- package/dist/strapi.d.ts +2 -2
- package/dist/strapi.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-4KULL3KT.js.map +0 -1
- /package/dist/{chunk-UR7V5WL2.js.map → chunk-2IIUQFMK.js.map} +0 -0
- /package/dist/{chunk-RZRM4TPC.js.map → chunk-BFMFXDIJ.js.map} +0 -0
- /package/dist/{chunk-56ZXAHBA.js.map → chunk-EZCIMEVL.js.map} +0 -0
- /package/dist/{chunk-22KZ7NGW.js.map → chunk-MGAEY7UZ.js.map} +0 -0
- /package/dist/{chunk-3IK6W7P2.js.map → chunk-RQWNJJIT.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.3
|
|
4
|
+
|
|
5
|
+
Fixed:
|
|
6
|
+
- `err_stack` of a failed knex query repeated the statement: knex rewrites the message before the
|
|
7
|
+
stack is first read, so V8 renders the statement into the stack's first line. The statement is
|
|
8
|
+
now removed from `err_stack` and `err_cause` as well as `err_message`.
|
|
9
|
+
- The statement is removed whatever the error code: pg errors without a SQLSTATE ("Connection
|
|
10
|
+
terminated unexpectedly"), MySQL `ER_*` and SQLite errors, and statements behind a comment or an
|
|
11
|
+
opening parenthesis. Without a SQLSTATE the prefix must read like SQL, so "Update failed - retry"
|
|
12
|
+
stays whole.
|
|
13
|
+
- `REDACT_PATTERNS.phoneVN` again masks a number glued to a label such as `SDT0912345678`; only a
|
|
14
|
+
hex letter (a-f) next to the number rules a match out (0.6.0 ruled out any letter).
|
|
15
|
+
|
|
3
16
|
## 0.6.2
|
|
4
17
|
|
|
5
18
|
Fixed:
|
package/dist/bullmq.cjs.map
CHANGED
|
@@ -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 /** 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/**\n * For errors Koa emits on the inbound socket (strapiServerErrors): the client hung up or sent a\n * malformed request. Not valid for errors in general: knex's pool rejects with Error(\"aborted\")\n * when it is destroyed, and pg or outbound sockets reset with ECONNRESET.\n */\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;;;AE2EA,IAAM,SAAS,gBAAgB,UAAU,MAAM,oBAAI,QAAgB,CAAC;AAI7D,SAAS,WAAW,KAAoB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO,IAAI,GAAG;AAC7D;;;AC1FA,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 message = String(e.message ?? \"\");\n const out: ErrFields = { err_type: errorType(e), err_message: message };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n out.err_message = cleanMessage(message, out.err_code);\n if (isSqlState(out.err_code)) {\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 = replaceOnce(e.stack, message, out.err_message);\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 const code = (cause as { code?: unknown }).code;\n const causeCode = code === undefined || code === null ? undefined : String(code);\n causes.push(`${errorType(cause)}: ${cleanMessage(String(cause.message ?? \"\"), causeCode)}`);\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\n// A statement, optionally behind comments or opening parentheses (\"(select ...) union ...\").\nconst SQL_START_RE = /^\\s*(?:\\/\\*[\\s\\S]*?\\*\\/\\s*)*\\(*\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n// Without a SQLSTATE code, the part before \" - \" must also read like SQL, so a message such as\n// \"Update failed - retry later\" stays whole.\nconst SQL_BODY_RE = /[\"`]|\\b(?:from|into|set|values|where|table|returning)\\b/i;\n\n/**\n * knex prefixes every failed query's message with the statement, whatever the driver or error\n * (pg with or without a SQLSTATE, MySQL ER_*, SQLite, \"Connection terminated\"). Drops it.\n */\nfunction cleanMessage(message: string, code: string | undefined): string {\n const at = message.lastIndexOf(\" - \");\n if (at === -1 || !SQL_START_RE.test(message)) return message;\n if (!isSqlState(code) && !SQL_BODY_RE.test(message.slice(0, at))) return message;\n return stripSql(message);\n}\n\n// V8 renders the stack's first line from the message when .stack is first read. For knex errors\n// that is after the statement was prefixed, so the stack repeats it.\nfunction replaceOnce(text: string, find: string, replacement: string | undefined): string {\n if (replacement === undefined || find === replacement || find === \"\") return text;\n const at = text.indexOf(find);\n return at === -1 ? text : text.slice(0, at) + replacement + text.slice(at + find.length);\n}\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. Cuts at the\n * last \" - \": a reason that contains \" - \" loses its start, which is safer than keeping part of a\n * statement that contains one (arithmetic, string literals).\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/**\n * For errors Koa emits on the inbound socket (strapiServerErrors): the client hung up or sent a\n * malformed request. Not valid for errors in general: knex's pool rejects with Error(\"aborted\")\n * when it is destroyed, and pg or outbound sockets reset with ECONNRESET.\n */\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;;;AEuGA,IAAM,SAAS,gBAAgB,UAAU,MAAM,oBAAI,QAAgB,CAAC;AAI7D,SAAS,WAAW,KAAoB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO,IAAI,GAAG;AAC7D;;;ACtHA,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
package/dist/bullmq.d.ts
CHANGED
package/dist/bullmq.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
globalSingleton
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JKCR57WJ.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-
|
|
86
|
+
//# sourceMappingURL=chunk-2IIUQFMK.js.map
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
markLogged,
|
|
10
10
|
serializeError,
|
|
11
11
|
toSnakeCase
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JKCR57WJ.js";
|
|
13
13
|
|
|
14
14
|
// src/normalize.ts
|
|
15
15
|
var HOOK_EXEMPT = /* @__PURE__ */ new Set(["level", "message", "event"]);
|
|
@@ -212,4 +212,4 @@ export {
|
|
|
212
212
|
DEFAULT_SKIP_PATHS,
|
|
213
213
|
logHttpRequest
|
|
214
214
|
};
|
|
215
|
-
//# sourceMappingURL=chunk-
|
|
215
|
+
//# sourceMappingURL=chunk-BFMFXDIJ.js.map
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
logHttpRequest,
|
|
3
3
|
requestContextFields
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BFMFXDIJ.js";
|
|
5
5
|
import {
|
|
6
6
|
attachStore,
|
|
7
7
|
recordErrorIn,
|
|
8
8
|
runWithStore,
|
|
9
9
|
storeOf
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JKCR57WJ.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-
|
|
72
|
+
//# sourceMappingURL=chunk-EZCIMEVL.js.map
|
|
@@ -82,10 +82,11 @@ var DEFAULT_REDACT_KEYS = [
|
|
|
82
82
|
];
|
|
83
83
|
var REDACT_PATTERNS = {
|
|
84
84
|
/**
|
|
85
|
-
* Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678
|
|
86
|
-
*
|
|
85
|
+
* Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678, also "SDT0912345678". A
|
|
86
|
+
* hex letter (a-f) on either side rules a match out, so hex ids (a UUID segment such as
|
|
87
|
+
* 0912345678e0) are left alone.
|
|
87
88
|
*/
|
|
88
|
-
phoneVN: /(?<![\dA-
|
|
89
|
+
phoneVN: /(?<![\dA-Fa-f+])(?:\+?84|0)[35789]\d{8}(?![\dA-Fa-f])/g,
|
|
89
90
|
/** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */
|
|
90
91
|
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,
|
|
91
92
|
/** IPv4 addresses. */
|
|
@@ -157,19 +158,22 @@ function serializeError(err) {
|
|
|
157
158
|
return { err_type: typeof err, err_message: defaultRedactor.json(err) };
|
|
158
159
|
}
|
|
159
160
|
const e = err;
|
|
160
|
-
const
|
|
161
|
+
const message = String(e.message ?? "");
|
|
162
|
+
const out = { err_type: errorType(e), err_message: message };
|
|
161
163
|
if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
|
|
164
|
+
out.err_message = cleanMessage(message, out.err_code);
|
|
162
165
|
if (isSqlState(out.err_code)) {
|
|
163
|
-
out.err_message = stripSql(out.err_message ?? "");
|
|
164
166
|
if (typeof e.table === "string" && e.table) out.db_table = e.table;
|
|
165
167
|
if (typeof e.constraint === "string" && e.constraint) out.db_constraint = e.constraint;
|
|
166
168
|
}
|
|
167
|
-
if (typeof e.stack === "string") out.err_stack = e.stack;
|
|
169
|
+
if (typeof e.stack === "string") out.err_stack = replaceOnce(e.stack, message, out.err_message);
|
|
168
170
|
const causes = [];
|
|
169
171
|
let cause = e.cause;
|
|
170
172
|
while (cause !== void 0 && cause !== null && causes.length < MAX_CAUSE_DEPTH) {
|
|
171
173
|
if (isErrorLike(cause)) {
|
|
172
|
-
|
|
174
|
+
const code = cause.code;
|
|
175
|
+
const causeCode = code === void 0 || code === null ? void 0 : String(code);
|
|
176
|
+
causes.push(`${errorType(cause)}: ${cleanMessage(String(cause.message ?? ""), causeCode)}`);
|
|
173
177
|
cause = cause.cause;
|
|
174
178
|
} else {
|
|
175
179
|
causes.push(defaultRedactor.json(cause));
|
|
@@ -182,7 +186,19 @@ function serializeError(err) {
|
|
|
182
186
|
function isSqlState(code) {
|
|
183
187
|
return code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
|
|
184
188
|
}
|
|
185
|
-
var SQL_START_RE = /^\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
|
|
189
|
+
var SQL_START_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*)*\(*\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
|
|
190
|
+
var SQL_BODY_RE = /["`]|\b(?:from|into|set|values|where|table|returning)\b/i;
|
|
191
|
+
function cleanMessage(message, code) {
|
|
192
|
+
const at = message.lastIndexOf(" - ");
|
|
193
|
+
if (at === -1 || !SQL_START_RE.test(message)) return message;
|
|
194
|
+
if (!isSqlState(code) && !SQL_BODY_RE.test(message.slice(0, at))) return message;
|
|
195
|
+
return stripSql(message);
|
|
196
|
+
}
|
|
197
|
+
function replaceOnce(text, find, replacement) {
|
|
198
|
+
if (replacement === void 0 || find === replacement || find === "") return text;
|
|
199
|
+
const at = text.indexOf(find);
|
|
200
|
+
return at === -1 ? text : text.slice(0, at) + replacement + text.slice(at + find.length);
|
|
201
|
+
}
|
|
186
202
|
function stripSql(message) {
|
|
187
203
|
if (!SQL_START_RE.test(message)) return message;
|
|
188
204
|
const at = message.lastIndexOf(" - ");
|
|
@@ -224,4 +240,4 @@ export {
|
|
|
224
240
|
markLogged,
|
|
225
241
|
isLogged
|
|
226
242
|
};
|
|
227
|
-
//# sourceMappingURL=chunk-
|
|
243
|
+
//# sourceMappingURL=chunk-JKCR57WJ.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, also \"SDT0912345678\". A\n * hex letter (a-f) on either side rules a match out, so hex ids (a UUID segment such as\n * 0912345678e0) are left alone.\n */\n phoneVN: /(?<![\\dA-Fa-f+])(?:\\+?84|0)[35789]\\d{8}(?![\\dA-Fa-f])/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 message = String(e.message ?? \"\");\n const out: ErrFields = { err_type: errorType(e), err_message: message };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n out.err_message = cleanMessage(message, out.err_code);\n if (isSqlState(out.err_code)) {\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 = replaceOnce(e.stack, message, out.err_message);\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 const code = (cause as { code?: unknown }).code;\n const causeCode = code === undefined || code === null ? undefined : String(code);\n causes.push(`${errorType(cause)}: ${cleanMessage(String(cause.message ?? \"\"), causeCode)}`);\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\n// A statement, optionally behind comments or opening parentheses (\"(select ...) union ...\").\nconst SQL_START_RE = /^\\s*(?:\\/\\*[\\s\\S]*?\\*\\/\\s*)*\\(*\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n// Without a SQLSTATE code, the part before \" - \" must also read like SQL, so a message such as\n// \"Update failed - retry later\" stays whole.\nconst SQL_BODY_RE = /[\"`]|\\b(?:from|into|set|values|where|table|returning)\\b/i;\n\n/**\n * knex prefixes every failed query's message with the statement, whatever the driver or error\n * (pg with or without a SQLSTATE, MySQL ER_*, SQLite, \"Connection terminated\"). Drops it.\n */\nfunction cleanMessage(message: string, code: string | undefined): string {\n const at = message.lastIndexOf(\" - \");\n if (at === -1 || !SQL_START_RE.test(message)) return message;\n if (!isSqlState(code) && !SQL_BODY_RE.test(message.slice(0, at))) return message;\n return stripSql(message);\n}\n\n// V8 renders the stack's first line from the message when .stack is first read. For knex errors\n// that is after the statement was prefixed, so the stack repeats it.\nfunction replaceOnce(text: string, find: string, replacement: string | undefined): string {\n if (replacement === undefined || find === replacement || find === \"\") return text;\n const at = text.indexOf(find);\n return at === -1 ? text : text.slice(0, at) + replacement + text.slice(at + find.length);\n}\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. Cuts at the\n * last \" - \": a reason that contains \" - \" loses its start, which is safer than keeping part of a\n * statement that contains one (arithmetic, string literals).\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/**\n * For errors Koa emits on the inbound socket (strapiServerErrors): the client hung up or sent a\n * malformed request. Not valid for errors in general: knex's pool rejects with Error(\"aborted\")\n * when it is destroyed, and pg or outbound sockets reset with ECONNRESET.\n */\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":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getContext,
|
|
3
3
|
markLogged,
|
|
4
4
|
withContext
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-JKCR57WJ.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-
|
|
41
|
+
//# sourceMappingURL=chunk-MGAEY7UZ.js.map
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
logHttpRequest,
|
|
3
3
|
requestContextFields
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BFMFXDIJ.js";
|
|
5
5
|
import {
|
|
6
6
|
attachStore,
|
|
7
7
|
recordErrorIn,
|
|
8
8
|
runWithStore,
|
|
9
9
|
storeOf
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JKCR57WJ.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-
|
|
66
|
+
//# sourceMappingURL=chunk-RQWNJJIT.js.map
|
package/dist/express.cjs
CHANGED
|
@@ -157,19 +157,22 @@ function serializeError(err) {
|
|
|
157
157
|
return { err_type: typeof err, err_message: defaultRedactor.json(err) };
|
|
158
158
|
}
|
|
159
159
|
const e = err;
|
|
160
|
-
const
|
|
160
|
+
const message = String(e.message ?? "");
|
|
161
|
+
const out = { err_type: errorType(e), err_message: message };
|
|
161
162
|
if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
|
|
163
|
+
out.err_message = cleanMessage(message, out.err_code);
|
|
162
164
|
if (isSqlState(out.err_code)) {
|
|
163
|
-
out.err_message = stripSql(out.err_message ?? "");
|
|
164
165
|
if (typeof e.table === "string" && e.table) out.db_table = e.table;
|
|
165
166
|
if (typeof e.constraint === "string" && e.constraint) out.db_constraint = e.constraint;
|
|
166
167
|
}
|
|
167
|
-
if (typeof e.stack === "string") out.err_stack = e.stack;
|
|
168
|
+
if (typeof e.stack === "string") out.err_stack = replaceOnce(e.stack, message, out.err_message);
|
|
168
169
|
const causes = [];
|
|
169
170
|
let cause = e.cause;
|
|
170
171
|
while (cause !== void 0 && cause !== null && causes.length < MAX_CAUSE_DEPTH) {
|
|
171
172
|
if (isErrorLike(cause)) {
|
|
172
|
-
|
|
173
|
+
const code = cause.code;
|
|
174
|
+
const causeCode = code === void 0 || code === null ? void 0 : String(code);
|
|
175
|
+
causes.push(`${errorType(cause)}: ${cleanMessage(String(cause.message ?? ""), causeCode)}`);
|
|
173
176
|
cause = cause.cause;
|
|
174
177
|
} else {
|
|
175
178
|
causes.push(defaultRedactor.json(cause));
|
|
@@ -182,7 +185,19 @@ function serializeError(err) {
|
|
|
182
185
|
function isSqlState(code) {
|
|
183
186
|
return code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
|
|
184
187
|
}
|
|
185
|
-
var SQL_START_RE = /^\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
|
|
188
|
+
var SQL_START_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*)*\(*\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
|
|
189
|
+
var SQL_BODY_RE = /["`]|\b(?:from|into|set|values|where|table|returning)\b/i;
|
|
190
|
+
function cleanMessage(message, code) {
|
|
191
|
+
const at = message.lastIndexOf(" - ");
|
|
192
|
+
if (at === -1 || !SQL_START_RE.test(message)) return message;
|
|
193
|
+
if (!isSqlState(code) && !SQL_BODY_RE.test(message.slice(0, at))) return message;
|
|
194
|
+
return stripSql(message);
|
|
195
|
+
}
|
|
196
|
+
function replaceOnce(text, find, replacement) {
|
|
197
|
+
if (replacement === void 0 || find === replacement || find === "") return text;
|
|
198
|
+
const at = text.indexOf(find);
|
|
199
|
+
return at === -1 ? text : text.slice(0, at) + replacement + text.slice(at + find.length);
|
|
200
|
+
}
|
|
186
201
|
function stripSql(message) {
|
|
187
202
|
if (!SQL_START_RE.test(message)) return message;
|
|
188
203
|
const at = message.lastIndexOf(" - ");
|
package/dist/express.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/express.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 type { ErrorRequestHandler, RequestHandler } from \"express\";\nimport { attachStore, recordErrorIn, runWithStore, storeOf, type ContextStore } from \"./context\";\nimport { logHttpRequest, requestContextFields, type HttpLogOptions } from \"./http\";\nimport type { Logger } from \"./logger\";\n\nfunction joinRoute(baseUrl: string | undefined, path: unknown): string {\n return `${baseUrl ?? \"\"}${String(path)}` || \"/\";\n}\n\n// Express assigns req.route when a route matches, while req.baseUrl still holds the mount\n// path. It resets baseUrl before the response ends, so capture the full template here.\nfunction captureRoute(req: { baseUrl?: string }, store: ContextStore): void {\n let value: unknown;\n Object.defineProperty(req, \"route\", {\n configurable: true,\n enumerable: true,\n get: () => value,\n set: (route: { path?: unknown } | undefined) => {\n value = route;\n if (route?.path !== undefined) store.route = joinRoute(req.baseUrl, route.path);\n },\n });\n}\n\n/** Mount before routes. Opens the request context and writes the http.request line. */\nexport function requestLogger(log: Logger, opts: HttpLogOptions = {}): RequestHandler {\n return (req, res, next) => {\n const ctx = requestContextFields(req.headers);\n res.setHeader(\"x-request-id\", ctx.request_id);\n const start = performance.now();\n runWithStore(ctx, (store) => {\n attachStore(res, store);\n captureRoute(req, store);\n let done = false;\n const finish = () => {\n if (done) return;\n done = true;\n logHttpRequest(\n log,\n {\n method: req.method,\n route: store.route,\n path: req.originalUrl,\n status: res.writableFinished || res.headersSent ? res.statusCode : 499,\n duration_ms: performance.now() - start,\n err: store.error,\n fields: store.fields,\n },\n opts,\n );\n };\n res.once(\"finish\", finish);\n res.once(\"close\", finish);\n next();\n });\n };\n}\n\n/** Mount after routes, before your own error handler. Records the error for the summary line. */\nexport function errorRecorder(): ErrorRequestHandler {\n return (err, _req, res, next) => {\n const store = storeOf(res);\n if (store) recordErrorIn(store, err);\n next(err);\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 /**\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/**\n * For errors Koa emits on the inbound socket (strapiServerErrors): the client hung up or sent a\n * malformed request. Not valid for errors in general: knex's pool rejects with Error(\"aborted\")\n * when it is destroyed, and pg or outbound sockets reset with ECONNRESET.\n */\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 { 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","// 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 // 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 // The level follows the status only. An error's shape cannot tell a client hang-up apart:\n // knex's pool (tarn) rejects in-flight queries with Error(\"aborted\") when it is destroyed on\n // shutdown, and a pg or outbound socket reset is ECONNRESET too. Both are server-side failures.\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":["header"]}
|
|
1
|
+
{"version":3,"sources":["../src/express.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 type { ErrorRequestHandler, RequestHandler } from \"express\";\nimport { attachStore, recordErrorIn, runWithStore, storeOf, type ContextStore } from \"./context\";\nimport { logHttpRequest, requestContextFields, type HttpLogOptions } from \"./http\";\nimport type { Logger } from \"./logger\";\n\nfunction joinRoute(baseUrl: string | undefined, path: unknown): string {\n return `${baseUrl ?? \"\"}${String(path)}` || \"/\";\n}\n\n// Express assigns req.route when a route matches, while req.baseUrl still holds the mount\n// path. It resets baseUrl before the response ends, so capture the full template here.\nfunction captureRoute(req: { baseUrl?: string }, store: ContextStore): void {\n let value: unknown;\n Object.defineProperty(req, \"route\", {\n configurable: true,\n enumerable: true,\n get: () => value,\n set: (route: { path?: unknown } | undefined) => {\n value = route;\n if (route?.path !== undefined) store.route = joinRoute(req.baseUrl, route.path);\n },\n });\n}\n\n/** Mount before routes. Opens the request context and writes the http.request line. */\nexport function requestLogger(log: Logger, opts: HttpLogOptions = {}): RequestHandler {\n return (req, res, next) => {\n const ctx = requestContextFields(req.headers);\n res.setHeader(\"x-request-id\", ctx.request_id);\n const start = performance.now();\n runWithStore(ctx, (store) => {\n attachStore(res, store);\n captureRoute(req, store);\n let done = false;\n const finish = () => {\n if (done) return;\n done = true;\n logHttpRequest(\n log,\n {\n method: req.method,\n route: store.route,\n path: req.originalUrl,\n status: res.writableFinished || res.headersSent ? res.statusCode : 499,\n duration_ms: performance.now() - start,\n err: store.error,\n fields: store.fields,\n },\n opts,\n );\n };\n res.once(\"finish\", finish);\n res.once(\"close\", finish);\n next();\n });\n };\n}\n\n/** Mount after routes, before your own error handler. Records the error for the summary line. */\nexport function errorRecorder(): ErrorRequestHandler {\n return (err, _req, res, next) => {\n const store = storeOf(res);\n if (store) recordErrorIn(store, err);\n next(err);\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 /**\n * Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678, also \"SDT0912345678\". A\n * hex letter (a-f) on either side rules a match out, so hex ids (a UUID segment such as\n * 0912345678e0) are left alone.\n */\n phoneVN: /(?<![\\dA-Fa-f+])(?:\\+?84|0)[35789]\\d{8}(?![\\dA-Fa-f])/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 message = String(e.message ?? \"\");\n const out: ErrFields = { err_type: errorType(e), err_message: message };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n out.err_message = cleanMessage(message, out.err_code);\n if (isSqlState(out.err_code)) {\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 = replaceOnce(e.stack, message, out.err_message);\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 const code = (cause as { code?: unknown }).code;\n const causeCode = code === undefined || code === null ? undefined : String(code);\n causes.push(`${errorType(cause)}: ${cleanMessage(String(cause.message ?? \"\"), causeCode)}`);\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\n// A statement, optionally behind comments or opening parentheses (\"(select ...) union ...\").\nconst SQL_START_RE = /^\\s*(?:\\/\\*[\\s\\S]*?\\*\\/\\s*)*\\(*\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n// Without a SQLSTATE code, the part before \" - \" must also read like SQL, so a message such as\n// \"Update failed - retry later\" stays whole.\nconst SQL_BODY_RE = /[\"`]|\\b(?:from|into|set|values|where|table|returning)\\b/i;\n\n/**\n * knex prefixes every failed query's message with the statement, whatever the driver or error\n * (pg with or without a SQLSTATE, MySQL ER_*, SQLite, \"Connection terminated\"). Drops it.\n */\nfunction cleanMessage(message: string, code: string | undefined): string {\n const at = message.lastIndexOf(\" - \");\n if (at === -1 || !SQL_START_RE.test(message)) return message;\n if (!isSqlState(code) && !SQL_BODY_RE.test(message.slice(0, at))) return message;\n return stripSql(message);\n}\n\n// V8 renders the stack's first line from the message when .stack is first read. For knex errors\n// that is after the statement was prefixed, so the stack repeats it.\nfunction replaceOnce(text: string, find: string, replacement: string | undefined): string {\n if (replacement === undefined || find === replacement || find === \"\") return text;\n const at = text.indexOf(find);\n return at === -1 ? text : text.slice(0, at) + replacement + text.slice(at + find.length);\n}\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. Cuts at the\n * last \" - \": a reason that contains \" - \" loses its start, which is safer than keeping part of a\n * statement that contains one (arithmetic, string literals).\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/**\n * For errors Koa emits on the inbound socket (strapiServerErrors): the client hung up or sent a\n * malformed request. Not valid for errors in general: knex's pool rejects with Error(\"aborted\")\n * when it is destroyed, and pg or outbound sockets reset with ECONNRESET.\n */\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 { 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","// 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 // 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 // The level follows the status only. An error's shape cannot tell a client hang-up apart:\n // knex's pool (tarn) rejects in-flight queries with Error(\"aborted\") when it is destroyed on\n // shutdown, and a pg or outbound socket reset is ECONNRESET too. Both are server-side failures.\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":["header"]}
|
package/dist/express.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ErrorRequestHandler, RequestHandler } from 'express';
|
|
2
|
-
import { H as HttpLogOptions } from './http-
|
|
3
|
-
import { L as Logger } from './logger-
|
|
2
|
+
import { H as HttpLogOptions } from './http-eeog5tR8.cjs';
|
|
3
|
+
import { L as Logger } from './logger-CtlC9SUE.cjs';
|
|
4
4
|
|
|
5
5
|
/** Mount before routes. Opens the request context and writes the http.request line. */
|
|
6
6
|
declare function requestLogger(log: Logger, opts?: HttpLogOptions): RequestHandler;
|
package/dist/express.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ErrorRequestHandler, RequestHandler } from 'express';
|
|
2
|
-
import { H as HttpLogOptions } from './http-
|
|
3
|
-
import { L as Logger } from './logger-
|
|
2
|
+
import { H as HttpLogOptions } from './http-Dp6Qtr9M.js';
|
|
3
|
+
import { L as Logger } from './logger-CtlC9SUE.js';
|
|
4
4
|
|
|
5
5
|
/** Mount before routes. Opens the request context and writes the http.request line. */
|
|
6
6
|
declare function requestLogger(log: Logger, opts?: HttpLogOptions): RequestHandler;
|
package/dist/express.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
errorRecorder,
|
|
3
3
|
requestLogger
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-EZCIMEVL.js";
|
|
5
|
+
import "./chunk-BFMFXDIJ.js";
|
|
6
6
|
import "./chunk-KLBL7QEN.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-JKCR57WJ.js";
|
|
8
8
|
export {
|
|
9
9
|
errorRecorder,
|
|
10
10
|
requestLogger
|