@tanngo11/log 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/CONTRACT.md +1 -1
- package/README.md +18 -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-UXKPPF4G.js → chunk-2WNGXGP7.js} +2 -2
- package/dist/{chunk-754Y5TQX.js → chunk-AF4YR4Y4.js} +14 -5
- package/dist/chunk-AF4YR4Y4.js.map +1 -0
- package/dist/{chunk-7DW57ECW.js → chunk-RVETZAK5.js} +3 -3
- package/dist/{chunk-QMC6BWGK.js → chunk-UDS6S22P.js} +28 -4
- package/dist/chunk-UDS6S22P.js.map +1 -0
- package/dist/{chunk-42ZPVKHN.js → chunk-UOIJCSP5.js} +3 -3
- package/dist/{chunk-35VPKN2C.js → chunk-URPHVC4Y.js} +2 -2
- package/dist/express.cjs +26 -4
- 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 +26 -4
- 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-BI6ARZlD.d.ts → http-44ztBSXk.d.ts} +1 -1
- package/dist/{http-BXAiU-rN.d.cts → http-BQJkHBwQ.d.cts} +1 -1
- package/dist/index.cjs +93 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -4
- package/dist/index.d.ts +47 -4
- package/dist/index.js +60 -4
- package/dist/index.js.map +1 -1
- package/dist/{logger-CFMTzjsm.d.ts → logger-3Co9eVJf.d.cts} +4 -1
- package/dist/{logger-CFMTzjsm.d.cts → logger-3Co9eVJf.d.ts} +4 -1
- package/dist/nest.cjs +26 -4
- 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 +26 -4
- 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 +26 -9
- 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 +3 -7
- package/dist/strapi.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-754Y5TQX.js.map +0 -1
- package/dist/chunk-QMC6BWGK.js.map +0 -1
- /package/dist/{chunk-UXKPPF4G.js.map → chunk-2WNGXGP7.js.map} +0 -0
- /package/dist/{chunk-7DW57ECW.js.map → chunk-RVETZAK5.js.map} +0 -0
- /package/dist/{chunk-42ZPVKHN.js.map → chunk-UOIJCSP5.js.map} +0 -0
- /package/dist/{chunk-35VPKN2C.js.map → chunk-URPHVC4Y.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
Changed:
|
|
6
|
+
- `knexLogging` writes `db.error` at `warn` by default (option `errorLevel`): apps catch some
|
|
7
|
+
errors on purpose, such as a unique violation that means "already exists". `ignoreCodes` skips
|
|
8
|
+
more SQLSTATE codes besides 25P02.
|
|
9
|
+
|
|
10
|
+
## 0.6.0
|
|
11
|
+
|
|
12
|
+
Added:
|
|
13
|
+
- `knexLogging(knex, log)`: `db.slow_query` (`warn`, 500 ms by default) for knex and Strapi
|
|
14
|
+
(`strapi.db.connection`), with `db_operation`, `db_table`, `duration_ms`, `db_rows`. Never logs
|
|
15
|
+
SQL text or bindings. `logErrors: true` adds a `db.error` line per failed query, except 25P02
|
|
16
|
+
("current transaction is aborted"), which only repeats an earlier failure.
|
|
17
|
+
- `isClientDisconnect(err)` and `stripSql(message)` are exported.
|
|
18
|
+
|
|
19
|
+
Fixed:
|
|
20
|
+
- `REDACT_PATTERNS.phoneVN` matched inside hex ids: a UUID segment such as `0912345678e0` became
|
|
21
|
+
`[REDACTED]e0`, also in `request_id`, which broke tracing that request. A letter next to the
|
|
22
|
+
digits now rules a match out.
|
|
23
|
+
- `request_id`, `trace_id`, `span_id` and `*_request_id` are never passed through value
|
|
24
|
+
redaction when they are plain tokens.
|
|
25
|
+
- Postgres errors from knex: `err_message` was the whole statement followed by the server's
|
|
26
|
+
reason, and truncation at 1000 characters cut the reason off. A SQLSTATE error now keeps only
|
|
27
|
+
the reason and adds `db_table` and `db_constraint`. `err_type` is `DatabaseError` instead of pg's
|
|
28
|
+
lowercase `error`.
|
|
29
|
+
|
|
30
|
+
Changed:
|
|
31
|
+
- An `http.request` line whose recorded error is a client disconnect (`aborted`, `ECONNRESET`...)
|
|
32
|
+
is `warn` with `client_disconnect: true`, even when the app answered 500.
|
|
33
|
+
|
|
3
34
|
## 0.5.1
|
|
4
35
|
|
|
5
36
|
Changed:
|
package/CONTRACT.md
CHANGED
|
@@ -34,7 +34,7 @@ stream, nested fields flattened with `_`) → MCP for AI agents.
|
|
|
34
34
|
| Inbound HTTP | `http_method`, `http_route` (template, e.g. `/orders/:id`), `http_path` (no query string), `http_status`, `duration_ms` |
|
|
35
35
|
| Outbound HTTP | `peer_service`, `http_method`, `http_url` (no query string or credentials), `http_status`, `duration_ms` |
|
|
36
36
|
| Error | `err_type`, `err_message`, `err_code` (string), `err_stack`, `err_cause` (cause chain joined with ` <- `, max 5) |
|
|
37
|
-
| Database | `db_system`, `db_operation`, `db_table`, `db_rows`, `duration_ms`, `db_statement` (parameterized only) |
|
|
37
|
+
| Database | `db_system`, `db_operation`, `db_table`, `db_rows`, `duration_ms`, `db_statement` (parameterized only), `db_constraint` |
|
|
38
38
|
|
|
39
39
|
Naming rules for domain fields:
|
|
40
40
|
- ids: `<entity>_id`;
|
package/README.md
CHANGED
|
@@ -346,6 +346,24 @@ export const prisma = new PrismaClient().$extends(prismaLogging(log, { db_system
|
|
|
346
346
|
`http.request` line with `err_code` (`P2002`...), and the contract logs each error once. Pass
|
|
347
347
|
`logErrors: true` to also get a `db.error` line.
|
|
348
348
|
|
|
349
|
+
## knex and Strapi queries
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { knexLogging } from "@tanngo11/log";
|
|
353
|
+
|
|
354
|
+
// Strapi: in bootstrap() of src/index.ts
|
|
355
|
+
knexLogging(strapi.db.connection, log, { db_system: "postgres" });
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- Logs `db.slow_query` (`warn`) for queries that take 500 ms or more (`slowMs`). Fields:
|
|
359
|
+
`db_operation`, `db_table`, `duration_ms`, `db_rows`. Never logs SQL text or bindings.
|
|
360
|
+
- `logErrors: true` also writes `db.error` for each failed query. Use it when the app catches
|
|
361
|
+
errors inside transactions: Postgres then fails every later query with 25P02, and only the
|
|
362
|
+
`db.error` line names the query that failed first. 25P02 lines are skipped. `db.error` is
|
|
363
|
+
`warn` by default (`errorLevel`); `ignoreCodes` skips expected codes such as `23505`.
|
|
364
|
+
- Postgres errors keep only the server's reason in `err_message` (knex prefixes the statement)
|
|
365
|
+
and carry `db_table` and `db_constraint`.
|
|
366
|
+
|
|
349
367
|
## Migrating from the shared winston logger
|
|
350
368
|
|
|
351
369
|
For repos using the old shared winston logger (S3 transport, nested metadata):
|
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}\n\nconst MAX_CAUSE_DEPTH = 5;\n\nexport function isErrorLike(v: unknown): v is Error {\n if (v instanceof Error) return true;\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as { message?: unknown }).message === \"string\" &&\n (\"stack\" in v || \"name\" in v)\n );\n}\n\nfunction errorType(e: Error): string {\n if (e.name && e.name !== \"Error\") return e.name;\n const ctor = (e as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== \"Object\" ? ctor : \"Error\";\n}\n\nexport function serializeError(err: unknown): ErrFields {\n if (!isErrorLike(err)) {\n if (typeof err === \"string\") return { err_type: \"string\", err_message: err };\n return { err_type: typeof err, err_message: defaultRedactor.json(err) };\n }\n const e = err as Error & { code?: unknown; cause?: unknown };\n const out: ErrFields = { err_type: errorType(e), err_message: String(e.message ?? \"\") };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n if (typeof e.stack === \"string\") out.err_stack = e.stack;\n\n const causes: string[] = [];\n let cause = e.cause;\n while (cause !== undefined && cause !== null && causes.length < MAX_CAUSE_DEPTH) {\n if (isErrorLike(cause)) {\n causes.push(`${errorType(cause)}: ${cause.message}`);\n cause = (cause as { cause?: unknown }).cause;\n } else {\n causes.push(defaultRedactor.json(cause));\n cause = undefined;\n }\n }\n if (causes.length > 0) out.err_cause = causes.join(\" <- \");\n return out;\n}\n\nconst logged = globalSingleton(\"logged\", () => new WeakSet<object>());\n\n// Marks an error as already written, so a later boundary (e.g. Next's onRequestError)\n// does not log it a second time.\nexport function markLogged(err: unknown): void {\n if (typeof err === \"object\" && err !== null) logged.add(err);\n}\n\nexport function isLogged(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && logged.has(err);\n}\n","import { getContext, withContext } from \"./context\";\nimport { markLogged } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\nexport interface JobInfo {\n job_name: string;\n job_id?: string | number;\n job_attempt?: number;\n /** No retries left after this attempt (or decided per error). Defaults to true when unknown. */\n final_attempt?: boolean | ((err: unknown) => boolean);\n request_id?: string;\n fields?: Fields;\n}\n\nexport async function runJob<T>(log: Logger, info: JobInfo, fn: () => T | Promise<T>): Promise<T> {\n const ctx: Fields = { ...info.fields, job_name: info.job_name };\n if (info.job_id !== undefined) ctx.job_id = String(info.job_id);\n if (info.job_attempt !== undefined) ctx.job_attempt = info.job_attempt;\n if (info.request_id !== undefined) ctx.request_id = info.request_id;\n\n return withContext(ctx, async () => {\n const start = performance.now();\n try {\n const result = await fn();\n log.info(\"job completed\", { event: \"job.completed\", duration_ms: Math.round(performance.now() - start) });\n return result;\n } catch (err) {\n const final = typeof info.final_attempt === \"function\" ? info.final_attempt(err) : (info.final_attempt ?? true);\n log[final ? \"error\" : \"warn\"](\"job failed\", {\n event: \"job.failed\",\n duration_ms: Math.round(performance.now() - start),\n err,\n });\n markLogged(err);\n throw err;\n }\n });\n}\n\n/** Adds the current request_id to a job payload, so the job's logs link back to the request. */\nexport function jobData<T extends Record<string, unknown>>(data: T): T & { request_id?: string } {\n const requestId = getContext().request_id;\n if (typeof requestId !== \"string\" || data.request_id !== undefined) return data;\n return { ...data, request_id: requestId };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,8BAAkC;;;ACE3B,SAAS,gBAAmB,MAAc,MAAkB;AACjE,QAAM,MAAM,uBAAO,IAAI,iBAAiB,IAAI,EAAE;AAC9C,QAAM,IAAI;AACV,MAAI,QAAQ,EAAE,GAAG;AACjB,MAAI,UAAU,QAAW;AACvB,YAAQ,KAAK;AACb,MAAE,GAAG,IAAI;AAAA,EACX;AACA,SAAO;AACT;;;ADCA,IAAM,MAAM,gBAAgB,OAAO,MAAM,IAAI,0CAAgC,CAAC;AAIvE,SAAS,aAAgB,QAAgB,IAAmC;AACjF,QAAM,SAAS,IAAI,SAAS;AAC5B,QAAM,QAAsB,EAAE,QAAQ,EAAE,GAAG,QAAQ,QAAQ,GAAG,OAAO,GAAG,OAAO;AAC/E,SAAO,IAAI,IAAI,OAAO,MAAM,GAAG,KAAK,CAAC;AACvC;AAEO,SAAS,YAAe,QAAgB,IAAgB;AAC7D,SAAO,aAAa,QAAQ,MAAM,GAAG,CAAC;AACxC;;;AE8BA,IAAM,SAAS,gBAAgB,UAAU,MAAM,oBAAI,QAAgB,CAAC;AAI7D,SAAS,WAAW,KAAoB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO,IAAI,GAAG;AAC7D;;;AC7CA,eAAsB,OAAU,KAAa,MAAe,IAAsC;AAChG,QAAM,MAAc,EAAE,GAAG,KAAK,QAAQ,UAAU,KAAK,SAAS;AAC9D,MAAI,KAAK,WAAW,OAAW,KAAI,SAAS,OAAO,KAAK,MAAM;AAC9D,MAAI,KAAK,gBAAgB,OAAW,KAAI,cAAc,KAAK;AAC3D,MAAI,KAAK,eAAe,OAAW,KAAI,aAAa,KAAK;AAEzD,SAAO,YAAY,KAAK,YAAY;AAClC,UAAM,QAAQ,YAAY,IAAI;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,GAAG;AACxB,UAAI,KAAK,iBAAiB,EAAE,OAAO,iBAAiB,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK,EAAE,CAAC;AACxG,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAM,QAAQ,OAAO,KAAK,kBAAkB,aAAa,KAAK,cAAc,GAAG,IAAK,KAAK,iBAAiB;AAC1G,UAAI,QAAQ,UAAU,MAAM,EAAE,cAAc;AAAA,QAC1C,OAAO;AAAA,QACP,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,QACjD;AAAA,MACF,CAAC;AACD,iBAAW,GAAG;AACd,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;;;AJ3BO,SAAS,gBACd,KACA,MACA,WACoC;AACpC,SAAO,CAAC,QAAQ,SAAS;AACvB,UAAM,WAAW,IAAI,MAAM,YAAY;AACvC,UAAM,UAAU,IAAI,eAAe;AACnC,UAAM,YAAa,IAAI,MAA+C;AACtE,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,UAAU;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,aAAa;AAAA;AAAA,QAEb,eAAe,CAAC,QAAQ,WAAW,YAAa,KAAmC,SAAS;AAAA,QAC5F,YAAY,OAAO,cAAc,WAAW,YAAY;AAAA,MAC1D;AAAA,MACA,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/bullmq.ts","../src/context.ts","../src/global.ts","../src/error.ts","../src/job.ts"],"sourcesContent":["import { runJob } from \"./job\";\nimport type { Logger } from \"./logger\";\n\n/** Minimal shape of a BullMQ Job; avoids depending on bullmq. */\nexport interface BullJobLike {\n id?: string;\n attemptsMade: number;\n opts?: { attempts?: number };\n data?: unknown;\n}\n\nexport function loggedProcessor<J extends BullJobLike, R, A extends unknown[] = [token?: string, signal?: AbortSignal]>(\n log: Logger,\n name: string,\n processor: (job: J, ...rest: A) => Promise<R>,\n): (job: J, ...rest: A) => Promise<R> {\n return (job, ...rest) => {\n const attempts = job.opts?.attempts ?? 1;\n const attempt = job.attemptsMade + 1;\n const requestId = (job.data as { request_id?: unknown } | undefined)?.request_id;\n return runJob(\n log,\n {\n job_name: name,\n job_id: job.id,\n job_attempt: attempt,\n // UnrecoverableError skips the remaining retries.\n final_attempt: (err) => attempt >= attempts || (err as { name?: unknown } | null)?.name === \"UnrecoverableError\",\n request_id: typeof requestId === \"string\" ? requestId : undefined,\n },\n () => processor(job, ...rest),\n );\n };\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { globalSingleton } from \"./global\";\nimport type { Fields } from \"./normalize\";\n\nexport interface ContextStore {\n fields: Fields;\n error?: unknown;\n /** Route template captured while the router still knew it (Express error path). */\n route?: string;\n parent?: ContextStore;\n}\n\nconst als = globalSingleton(\"als\", () => new AsyncLocalStorage<ContextStore>());\n\nexport const STORE_KEY: symbol = Symbol.for(\"@tanngo11/log.store\");\n\nexport function runWithStore<T>(fields: Fields, fn: (store: ContextStore) => T): T {\n const parent = als.getStore();\n const store: ContextStore = { fields: { ...parent?.fields, ...fields }, parent };\n return als.run(store, () => fn(store));\n}\n\nexport function withContext<T>(fields: Fields, fn: () => T): T {\n return runWithStore(fields, () => fn());\n}\n\nexport function currentStore(): ContextStore | undefined {\n return als.getStore();\n}\n\nexport function getContext(): Fields {\n return als.getStore()?.fields ?? {};\n}\n\n// Applies to the current context and every enclosing one, so a field added inside a nested\n// withContext/runJob still reaches the request's http.request line.\nexport function addContext(fields: Fields): void {\n for (let s = als.getStore(); s; s = s.parent) Object.assign(s.fields, fields);\n}\n\n// Sets the error on the store and on enclosing stores that have none yet.\nexport function recordErrorIn(store: ContextStore, err: unknown): void {\n store.error = err;\n for (let s = store.parent; s; s = s.parent) if (s.error === undefined) s.error = err;\n}\n\n// Marks the error that the current request/job summary line should carry. Does not write a line.\nexport function recordError(err: unknown): void {\n const store = als.getStore();\n if (store) recordErrorIn(store, err);\n}\n\nexport function attachStore(target: object, store: ContextStore): void {\n (target as Record<symbol, ContextStore>)[STORE_KEY] = store;\n}\n\nexport function storeOf(target: unknown): ContextStore | undefined {\n if (typeof target !== \"object\" || target === null) return undefined;\n return (target as Record<symbol, ContextStore | undefined>)[STORE_KEY];\n}\n","// Singletons shared by every copy of this package loaded in the process (ESM and CJS builds,\n// or duplicated installs), so context and dedupe state never split.\nexport function globalSingleton<T>(name: string, make: () => T): T {\n const key = Symbol.for(`@tanngo11/log.${name}`);\n const g = globalThis as unknown as Record<symbol, T | undefined>;\n let value = g[key];\n if (value === undefined) {\n value = make();\n g[key] = value;\n }\n return value;\n}\n","import { globalSingleton } from \"./global\";\nimport { defaultRedactor } from \"./redact\";\n\nexport interface ErrFields {\n err_type?: string;\n err_message?: string;\n err_code?: string;\n err_stack?: string;\n err_cause?: string;\n /** Database errors (pg): the table and constraint the server reported. */\n db_table?: string;\n db_constraint?: string;\n}\n\nconst MAX_CAUSE_DEPTH = 5;\n\nexport function isErrorLike(v: unknown): v is Error {\n if (v instanceof Error) return true;\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as { message?: unknown }).message === \"string\" &&\n (\"stack\" in v || \"name\" in v)\n );\n}\n\nfunction errorType(e: Error): string {\n // pg's DatabaseError keeps the lowercase name \"error\"; its class name says more.\n if (e.name && e.name !== \"Error\" && e.name !== \"error\") return e.name;\n const ctor = (e as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== \"Object\" ? ctor : \"Error\";\n}\n\nexport function serializeError(err: unknown): ErrFields {\n if (!isErrorLike(err)) {\n if (typeof err === \"string\") return { err_type: \"string\", err_message: err };\n return { err_type: typeof err, err_message: defaultRedactor.json(err) };\n }\n const e = err as Error & { code?: unknown; cause?: unknown; table?: unknown; constraint?: unknown };\n const out: ErrFields = { err_type: errorType(e), err_message: String(e.message ?? \"\") };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n if (isSqlState(out.err_code)) {\n out.err_message = stripSql(out.err_message ?? \"\");\n if (typeof e.table === \"string\" && e.table) out.db_table = e.table;\n if (typeof e.constraint === \"string\" && e.constraint) out.db_constraint = e.constraint;\n }\n if (typeof e.stack === \"string\") out.err_stack = e.stack;\n\n const causes: string[] = [];\n let cause = e.cause;\n while (cause !== undefined && cause !== null && causes.length < MAX_CAUSE_DEPTH) {\n if (isErrorLike(cause)) {\n causes.push(`${errorType(cause)}: ${cause.message}`);\n cause = (cause as { cause?: unknown }).cause;\n } else {\n causes.push(defaultRedactor.json(cause));\n cause = undefined;\n }\n }\n if (causes.length > 0) out.err_cause = causes.join(\" <- \");\n return out;\n}\n\n// Postgres SQLSTATE codes: five digits or uppercase letters (23505, 40P01, 25P02).\nfunction isSqlState(code: string | undefined): boolean {\n return code !== undefined && /^[0-9A-Z]{5}$/.test(code);\n}\n\nconst SQL_START_RE = /^\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n\n/**\n * knex prefixes a failed query's message with the whole statement (\"update ... - deadlock\n * detected\"). A long statement filled err_message and the truncation cut off the server's reason,\n * the only part worth reading. Keeps the reason; the statement never reaches the log.\n */\nexport function stripSql(message: string): string {\n if (!SQL_START_RE.test(message)) return message;\n const at = message.lastIndexOf(\" - \");\n return at === -1 ? message : message.slice(at + 3);\n}\n\n// The client hung up or sent a malformed request: worth a warn, not an error on our side.\nexport function isClientDisconnect(err: unknown): boolean {\n const e = err as { code?: unknown; message?: unknown } | null;\n const code = typeof e?.code === \"string\" ? e.code : \"\";\n return (\n code === \"ECONNRESET\" ||\n code === \"ECONNABORTED\" ||\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_PREMATURE_CLOSE\" ||\n code.startsWith(\"HPE_\") ||\n e?.message === \"aborted\"\n );\n}\n\nconst logged = globalSingleton(\"logged\", () => new WeakSet<object>());\n\n// Marks an error as already written, so a later boundary (e.g. Next's onRequestError)\n// does not log it a second time.\nexport function markLogged(err: unknown): void {\n if (typeof err === \"object\" && err !== null) logged.add(err);\n}\n\nexport function isLogged(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && logged.has(err);\n}\n","import { getContext, withContext } from \"./context\";\nimport { markLogged } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport type { Fields } from \"./normalize\";\n\nexport interface JobInfo {\n job_name: string;\n job_id?: string | number;\n job_attempt?: number;\n /** No retries left after this attempt (or decided per error). Defaults to true when unknown. */\n final_attempt?: boolean | ((err: unknown) => boolean);\n request_id?: string;\n fields?: Fields;\n}\n\nexport async function runJob<T>(log: Logger, info: JobInfo, fn: () => T | Promise<T>): Promise<T> {\n const ctx: Fields = { ...info.fields, job_name: info.job_name };\n if (info.job_id !== undefined) ctx.job_id = String(info.job_id);\n if (info.job_attempt !== undefined) ctx.job_attempt = info.job_attempt;\n if (info.request_id !== undefined) ctx.request_id = info.request_id;\n\n return withContext(ctx, async () => {\n const start = performance.now();\n try {\n const result = await fn();\n log.info(\"job completed\", { event: \"job.completed\", duration_ms: Math.round(performance.now() - start) });\n return result;\n } catch (err) {\n const final = typeof info.final_attempt === \"function\" ? info.final_attempt(err) : (info.final_attempt ?? true);\n log[final ? \"error\" : \"warn\"](\"job failed\", {\n event: \"job.failed\",\n duration_ms: Math.round(performance.now() - start),\n err,\n });\n markLogged(err);\n throw err;\n }\n });\n}\n\n/** Adds the current request_id to a job payload, so the job's logs link back to the request. */\nexport function jobData<T extends Record<string, unknown>>(data: T): T & { request_id?: string } {\n const requestId = getContext().request_id;\n if (typeof requestId !== \"string\" || data.request_id !== undefined) return data;\n return { ...data, request_id: requestId };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,8BAAkC;;;ACE3B,SAAS,gBAAmB,MAAc,MAAkB;AACjE,QAAM,MAAM,uBAAO,IAAI,iBAAiB,IAAI,EAAE;AAC9C,QAAM,IAAI;AACV,MAAI,QAAQ,EAAE,GAAG;AACjB,MAAI,UAAU,QAAW;AACvB,YAAQ,KAAK;AACb,MAAE,GAAG,IAAI;AAAA,EACX;AACA,SAAO;AACT;;;ADCA,IAAM,MAAM,gBAAgB,OAAO,MAAM,IAAI,0CAAgC,CAAC;AAIvE,SAAS,aAAgB,QAAgB,IAAmC;AACjF,QAAM,SAAS,IAAI,SAAS;AAC5B,QAAM,QAAsB,EAAE,QAAQ,EAAE,GAAG,QAAQ,QAAQ,GAAG,OAAO,GAAG,OAAO;AAC/E,SAAO,IAAI,IAAI,OAAO,MAAM,GAAG,KAAK,CAAC;AACvC;AAEO,SAAS,YAAe,QAAgB,IAAgB;AAC7D,SAAO,aAAa,QAAQ,MAAM,GAAG,CAAC;AACxC;;;AEuEA,IAAM,SAAS,gBAAgB,UAAU,MAAM,oBAAI,QAAgB,CAAC;AAI7D,SAAS,WAAW,KAAoB;AAC7C,MAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM,QAAO,IAAI,GAAG;AAC7D;;;ACtFA,eAAsB,OAAU,KAAa,MAAe,IAAsC;AAChG,QAAM,MAAc,EAAE,GAAG,KAAK,QAAQ,UAAU,KAAK,SAAS;AAC9D,MAAI,KAAK,WAAW,OAAW,KAAI,SAAS,OAAO,KAAK,MAAM;AAC9D,MAAI,KAAK,gBAAgB,OAAW,KAAI,cAAc,KAAK;AAC3D,MAAI,KAAK,eAAe,OAAW,KAAI,aAAa,KAAK;AAEzD,SAAO,YAAY,KAAK,YAAY;AAClC,UAAM,QAAQ,YAAY,IAAI;AAC9B,QAAI;AACF,YAAM,SAAS,MAAM,GAAG;AACxB,UAAI,KAAK,iBAAiB,EAAE,OAAO,iBAAiB,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK,EAAE,CAAC;AACxG,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAM,QAAQ,OAAO,KAAK,kBAAkB,aAAa,KAAK,cAAc,GAAG,IAAK,KAAK,iBAAiB;AAC1G,UAAI,QAAQ,UAAU,MAAM,EAAE,cAAc;AAAA,QAC1C,OAAO;AAAA,QACP,aAAa,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,QACjD;AAAA,MACF,CAAC;AACD,iBAAW,GAAG;AACd,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;;;AJ3BO,SAAS,gBACd,KACA,MACA,WACoC;AACpC,SAAO,CAAC,QAAQ,SAAS;AACvB,UAAM,WAAW,IAAI,MAAM,YAAY;AACvC,UAAM,UAAU,IAAI,eAAe;AACnC,UAAM,YAAa,IAAI,MAA+C;AACtE,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,UAAU;AAAA,QACV,QAAQ,IAAI;AAAA,QACZ,aAAa;AAAA;AAAA,QAEb,eAAe,CAAC,QAAQ,WAAW,YAAa,KAAmC,SAAS;AAAA,QAC5F,YAAY,OAAO,cAAc,WAAW,YAAY;AAAA,MAC1D;AAAA,MACA,MAAM,UAAU,KAAK,GAAG,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;","names":[]}
|
package/dist/bullmq.d.cts
CHANGED
package/dist/bullmq.d.ts
CHANGED
package/dist/bullmq.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
getContext,
|
|
3
3
|
markLogged,
|
|
4
4
|
withContext
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-UDS6S22P.js";
|
|
6
6
|
|
|
7
7
|
// src/job.ts
|
|
8
8
|
async function runJob(log, info, fn) {
|
|
@@ -38,4 +38,4 @@ export {
|
|
|
38
38
|
runJob,
|
|
39
39
|
jobData
|
|
40
40
|
};
|
|
41
|
-
//# sourceMappingURL=chunk-
|
|
41
|
+
//# sourceMappingURL=chunk-2WNGXGP7.js.map
|
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
REDACTED,
|
|
7
7
|
getContext,
|
|
8
|
+
isClientDisconnect,
|
|
8
9
|
isErrorLike,
|
|
9
10
|
markLogged,
|
|
10
11
|
serializeError,
|
|
11
12
|
toSnakeCase
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-UDS6S22P.js";
|
|
13
14
|
|
|
14
15
|
// src/normalize.ts
|
|
15
16
|
var HOOK_EXEMPT = /* @__PURE__ */ new Set(["level", "message", "event"]);
|
|
@@ -95,8 +96,13 @@ function flattenInto(out, prefix, obj, depth, opts) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
var ID_VALUE_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
100
|
+
function isIdKey(key) {
|
|
101
|
+
return key === "request_id" || key === "trace_id" || key === "span_id" || key.endsWith("_request_id");
|
|
102
|
+
}
|
|
98
103
|
function finalize(key, value, opts) {
|
|
99
104
|
if (typeof value !== "string") return value;
|
|
105
|
+
if (isIdKey(key) && ID_VALUE_RE.test(value)) return value;
|
|
100
106
|
const v = opts.redactor.scrub(isUrlKey(key) ? stripUrl(value) : value);
|
|
101
107
|
return truncate(v, key === "err_stack" ? MAX_STACK : MAX_STRING);
|
|
102
108
|
}
|
|
@@ -178,12 +184,15 @@ function shouldSkip(method, path, skipPaths = DEFAULT_SKIP_PATHS) {
|
|
|
178
184
|
}
|
|
179
185
|
function logHttpRequest(log, info, opts = {}) {
|
|
180
186
|
if (info.status < 500 && shouldSkip(info.method, info.path, opts.skipPaths)) return;
|
|
181
|
-
const
|
|
187
|
+
const hasErr = info.err !== void 0 && info.err !== null;
|
|
188
|
+
const clientGone = hasErr && isClientDisconnect(info.err);
|
|
189
|
+
const level = info.status >= 500 && !clientGone ? "error" : info.status >= 400 ? "warn" : "info";
|
|
182
190
|
const path = stripUrl(info.path);
|
|
183
191
|
const errFields = {};
|
|
184
|
-
if (
|
|
192
|
+
if (hasErr) {
|
|
185
193
|
Object.assign(errFields, serializeError(info.err));
|
|
186
|
-
if (info.status < 500) delete errFields.err_stack;
|
|
194
|
+
if (info.status < 500 || clientGone) delete errFields.err_stack;
|
|
195
|
+
if (clientGone) errFields.client_disconnect = true;
|
|
187
196
|
markLogged(info.err);
|
|
188
197
|
}
|
|
189
198
|
const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;
|
|
@@ -207,4 +216,4 @@ export {
|
|
|
207
216
|
DEFAULT_SKIP_PATHS,
|
|
208
217
|
logHttpRequest
|
|
209
218
|
};
|
|
210
|
-
//# sourceMappingURL=chunk-
|
|
219
|
+
//# sourceMappingURL=chunk-AF4YR4Y4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/normalize.ts","../src/http.ts"],"sourcesContent":["import { isErrorLike, serializeError } from \"./error\";\nimport type { Level } from \"./levels\";\nimport { REDACTED, toSnakeCase, type Redactor } from \"./redact\";\n\nexport { DEFAULT_REDACT_KEYS, toSnakeCase } from \"./redact\";\n\n/** Final per-field hook: return a replacement, or undefined to drop the field. */\nexport type RedactHook = (key: string, value: Primitive) => unknown;\n\nexport type Fields = Record<string, unknown>;\nexport type Primitive = string | number | boolean | null;\nexport type LogRecord = Record<string, Primitive>;\n\nexport interface NormalizeOptions {\n snakeCase: boolean;\n redactor: Redactor;\n hook?: RedactHook;\n}\n\n// Fields the redact hook never sees, so every line keeps its level, message and event.\nconst HOOK_EXEMPT = new Set([\"level\", \"message\", \"event\"]);\n\n// Keys owned by Vector (overwritten or deleted on ingest) or by the logger itself.\nexport const RESERVED_KEYS = new Set([\n \"project\", \"service\", \"vps\", \"version\", \"container_name\", \"container_id\", \"image\", \"stream\",\n \"_timestamp\", \"label\", \"host\", \"source_type\", \"msg\", \"lvl\", \"severity\", \"level\", \"message\",\n]);\n\nexport const MAX_LINE_BYTES = 16000;\nconst MAX_MESSAGE = 500;\nconst MAX_STACK = 4000;\nconst MAX_STRING = 1000;\nconst MAX_DEPTH = 3;\nconst TRUNCATED = \"…[truncated]\";\n\n// Fields kept when a line is too large.\nconst KEEP_ON_SHRINK = new Set([\n \"level\", \"message\", \"event\", \"request_id\", \"trace_id\", \"span_id\", \"job_name\", \"job_id\", \"job_attempt\",\n \"user_id\", \"tenant_id\", \"err_type\", \"err_message\", \"err_code\",\n]);\n\nexport function stripUrl(url: string): string {\n const noQuery = url.split(/[?#]/, 1)[0] ?? \"\";\n return noQuery.replace(/^([a-z][a-z0-9+.-]*:\\/\\/)[^/@\\s]*@/i, \"$1\");\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max - TRUNCATED.length) + TRUNCATED : s;\n}\n\nfunction isPlainObject(v: unknown): v is Fields {\n if (typeof v !== \"object\" || v === null) return false;\n const proto = Object.getPrototypeOf(v);\n return proto === Object.prototype || proto === null;\n}\n\nfunction isUrlKey(key: string): boolean {\n return key === \"http_path\" || key === \"http_url\" || key.endsWith(\"_url\");\n}\n\nfunction flattenInto(out: LogRecord, prefix: string, obj: Fields, depth: number, opts: NormalizeOptions): void {\n for (const [rawKey, value] of Object.entries(obj)) {\n const key = opts.snakeCase ? toSnakeCase(rawKey) : rawKey;\n const full = prefix ? `${prefix}_${key}` : key;\n if (value === undefined || typeof value === \"function\" || typeof value === \"symbol\") continue;\n if (value !== null && (opts.redactor.isSensitive(rawKey) || opts.redactor.isSensitive(full))) {\n out[full] = REDACTED;\n continue;\n }\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") {\n out[full] = value;\n } else if (typeof value === \"number\") {\n out[full] = Number.isFinite(value) ? value : String(value);\n } else if (typeof value === \"bigint\") {\n out[full] = value.toString();\n } else if (value instanceof Date) {\n out[full] = Number.isNaN(value.getTime()) ? \"Invalid Date\" : value.toISOString();\n } else if (isErrorLike(value)) {\n out[full] = `${value.name}: ${value.message}`;\n } else if (isPlainObject(value) && depth < MAX_DEPTH - 1) {\n flattenInto(out, full, value, depth + 1, opts);\n } else {\n // Arrays, deep objects, class instances: stringified with sensitive keys redacted.\n out[full] = opts.redactor.json(value);\n }\n }\n}\n\n// Correlation ids are opaque tokens: value patterns such as phoneVN can match inside a hex\n// segment and break the join between services, so a well-formed id is never scrubbed.\nconst ID_VALUE_RE = /^[A-Za-z0-9._:-]{1,128}$/;\n\nfunction isIdKey(key: string): boolean {\n return key === \"request_id\" || key === \"trace_id\" || key === \"span_id\" || key.endsWith(\"_request_id\");\n}\n\nfunction finalize(key: string, value: Primitive, opts: NormalizeOptions): Primitive {\n if (typeof value !== \"string\") return value;\n if (isIdKey(key) && ID_VALUE_RE.test(value)) return value;\n const v = opts.redactor.scrub(isUrlKey(key) ? stripUrl(value) : value);\n return truncate(v, key === \"err_stack\" ? MAX_STACK : MAX_STRING);\n}\n\nexport function buildRecord(\n level: Level,\n message: unknown,\n sources: (Fields | undefined)[],\n opts: NormalizeOptions,\n): LogRecord {\n const merged: Fields = {};\n for (const source of sources) if (source) Object.assign(merged, source);\n\n let err: unknown;\n if (merged.err !== undefined && merged.err !== null) {\n err = merged.err;\n delete merged.err;\n } else if (isErrorLike(merged.error)) {\n err = merged.error;\n delete merged.error;\n }\n\n const flat: LogRecord = {};\n flattenInto(flat, \"\", merged, 0, opts);\n if (err !== undefined) Object.assign(flat, serializeError(err));\n\n const text = typeof message === \"string\" ? message : opts.redactor.json(message);\n const out: LogRecord = { level, message: truncate(opts.redactor.scrub(text), MAX_MESSAGE) };\n for (const [k, v] of Object.entries(flat)) {\n const key = RESERVED_KEYS.has(k) ? `app_${k}` : k;\n let value = finalize(key, v, opts);\n if (opts.hook && !HOOK_EXEMPT.has(key)) {\n const replaced = opts.hook(key, value);\n if (replaced === undefined) continue;\n value = typeof replaced === \"object\" && replaced !== null ? opts.redactor.json(replaced) : (replaced as Primitive);\n }\n out[key] = value;\n }\n return out;\n}\n\nfunction byteLength(s: string): number {\n // UTF-8 uses at most 3 bytes per UTF-16 code unit: skip encoding short lines.\n if (s.length * 3 <= MAX_LINE_BYTES) return s.length;\n return new TextEncoder().encode(s).length;\n}\n\nexport function serializeRecord(record: LogRecord): string {\n const line = JSON.stringify(record);\n if (byteLength(line) <= MAX_LINE_BYTES) return line;\n const small: LogRecord = {};\n for (const [k, v] of Object.entries(record)) {\n if (KEEP_ON_SHRINK.has(k) || k.startsWith(\"http_\")) small[k] = v;\n }\n small.log_truncated = true;\n return JSON.stringify(small);\n}\n","import { getContext } from \"./context\";\nimport { isClientDisconnect, markLogged, serializeError } from \"./error\";\nimport type { Logger } from \"./logger\";\nimport { stripUrl, type Fields } from \"./normalize\";\nimport { parseTraceparent, resolveRequestId } from \"./request-id\";\n\nexport { newRequestId, parseTraceparent, REQUEST_ID_RE, resolveRequestId } from \"./request-id\";\n\nexport type HeaderSource = Headers | Record<string, string | string[] | undefined>;\n\nfunction header(headers: HeaderSource, name: string): string | string[] | undefined {\n if (typeof (headers as Headers).get === \"function\") return (headers as Headers).get(name) ?? undefined;\n return (headers as Record<string, string | string[] | undefined>)[name];\n}\n\nexport function requestContextFields(headers: HeaderSource): { request_id: string; trace_id?: string; span_id?: string } {\n const out: { request_id: string; trace_id?: string; span_id?: string } = {\n request_id: resolveRequestId(header(headers, \"x-request-id\")),\n };\n const tp = parseTraceparent(header(headers, \"traceparent\"));\n if (tp) Object.assign(out, tp);\n return out;\n}\n\n// Headers to forward to services we own, so their logs share request_id / trace_id.\nexport function propagationHeaders(): Record<string, string> {\n const ctx = getContext();\n const out: Record<string, string> = {};\n if (typeof ctx.request_id === \"string\") out[\"x-request-id\"] = ctx.request_id;\n if (typeof ctx.trace_id === \"string\" && typeof ctx.span_id === \"string\") {\n out.traceparent = `00-${ctx.trace_id}-${ctx.span_id}-01`;\n }\n return out;\n}\n\nexport const DEFAULT_SKIP_PATHS: (string | RegExp)[] = [\n // Also under a prefix such as /api/v1/health.\n /(^|\\/)(health|healthz|ready|readyz|livez)\\/?$/,\n \"/favicon.ico\",\n /^\\/_next\\/static\\//,\n];\n\nexport interface HttpLogOptions {\n skipPaths?: (string | RegExp)[];\n}\n\nexport interface HttpRequestInfo {\n method: string;\n route?: string;\n path: string;\n status: number;\n duration_ms?: number;\n err?: unknown;\n fields?: Fields;\n}\n\nexport function shouldSkip(method: string, path: string, skipPaths: (string | RegExp)[] = DEFAULT_SKIP_PATHS): boolean {\n if (method.toUpperCase() === \"OPTIONS\") return true;\n const p = stripUrl(path);\n return skipPaths.some((s) => (typeof s === \"string\" ? p === s : s.test(p)));\n}\n\n// Writes the one `http.request` summary line for an inbound request. Skipped paths are still\n// logged when they fail with a 5xx, so a failing health check stays visible.\nexport function logHttpRequest(log: Logger, info: HttpRequestInfo, opts: HttpLogOptions = {}): void {\n if (info.status < 500 && shouldSkip(info.method, info.path, opts.skipPaths)) return;\n const hasErr = info.err !== undefined && info.err !== null;\n // A client that hung up mid-request is not a server failure, whatever status the app set.\n const clientGone = hasErr && isClientDisconnect(info.err);\n const level = info.status >= 500 && !clientGone ? \"error\" : info.status >= 400 ? \"warn\" : \"info\";\n const path = stripUrl(info.path);\n const errFields: Fields = {};\n if (hasErr) {\n Object.assign(errFields, serializeError(info.err));\n if (info.status < 500 || clientGone) delete errFields.err_stack;\n if (clientGone) errFields.client_disconnect = true;\n markLogged(info.err);\n }\n // Route templates keep the message bounded; the raw path would make one message per URL.\n const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;\n log[level](message, {\n ...info.fields,\n event: \"http.request\",\n http_method: info.method,\n http_route: info.route,\n http_path: path,\n http_status: info.status,\n duration_ms: info.duration_ms === undefined ? undefined : Math.round(info.duration_ms),\n ...errFields,\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
logHttpRequest,
|
|
3
3
|
requestContextFields
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-AF4YR4Y4.js";
|
|
5
5
|
import {
|
|
6
6
|
attachStore,
|
|
7
7
|
recordErrorIn,
|
|
8
8
|
runWithStore,
|
|
9
9
|
storeOf
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-UDS6S22P.js";
|
|
11
11
|
|
|
12
12
|
// src/fastify.ts
|
|
13
13
|
var starts = /* @__PURE__ */ new WeakMap();
|
|
@@ -63,4 +63,4 @@ var fastifyLogger = plugin;
|
|
|
63
63
|
export {
|
|
64
64
|
fastifyLogger
|
|
65
65
|
};
|
|
66
|
-
//# sourceMappingURL=chunk-
|
|
66
|
+
//# sourceMappingURL=chunk-RVETZAK5.js.map
|
|
@@ -81,8 +81,11 @@ var DEFAULT_REDACT_KEYS = [
|
|
|
81
81
|
"bearer"
|
|
82
82
|
];
|
|
83
83
|
var REDACT_PATTERNS = {
|
|
84
|
-
/**
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. Letters on either side
|
|
86
|
+
* rule a match out, so hex ids (a UUID segment such as 0912345678e0) are left alone.
|
|
87
|
+
*/
|
|
88
|
+
phoneVN: /(?<![\dA-Za-z+])(?:\+?84|0)[35789]\d{8}(?![\dA-Za-z])/g,
|
|
86
89
|
/** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */
|
|
87
90
|
paymentCard: /(?<!\d)(?:4\d{3}|5[1-5]\d{2}|2[2-7]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))(?:[ -]?\d{4}){2}[ -]?\d{1,4}(?!\d)/g,
|
|
88
91
|
/** IPv4 addresses. */
|
|
@@ -144,7 +147,7 @@ function isErrorLike(v) {
|
|
|
144
147
|
return typeof v === "object" && v !== null && typeof v.message === "string" && ("stack" in v || "name" in v);
|
|
145
148
|
}
|
|
146
149
|
function errorType(e) {
|
|
147
|
-
if (e.name && e.name !== "Error") return e.name;
|
|
150
|
+
if (e.name && e.name !== "Error" && e.name !== "error") return e.name;
|
|
148
151
|
const ctor = e.constructor?.name;
|
|
149
152
|
return ctor && ctor !== "Object" ? ctor : "Error";
|
|
150
153
|
}
|
|
@@ -156,6 +159,11 @@ function serializeError(err) {
|
|
|
156
159
|
const e = err;
|
|
157
160
|
const out = { err_type: errorType(e), err_message: String(e.message ?? "") };
|
|
158
161
|
if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
|
|
162
|
+
if (isSqlState(out.err_code)) {
|
|
163
|
+
out.err_message = stripSql(out.err_message ?? "");
|
|
164
|
+
if (typeof e.table === "string" && e.table) out.db_table = e.table;
|
|
165
|
+
if (typeof e.constraint === "string" && e.constraint) out.db_constraint = e.constraint;
|
|
166
|
+
}
|
|
159
167
|
if (typeof e.stack === "string") out.err_stack = e.stack;
|
|
160
168
|
const causes = [];
|
|
161
169
|
let cause = e.cause;
|
|
@@ -171,6 +179,20 @@ function serializeError(err) {
|
|
|
171
179
|
if (causes.length > 0) out.err_cause = causes.join(" <- ");
|
|
172
180
|
return out;
|
|
173
181
|
}
|
|
182
|
+
function isSqlState(code) {
|
|
183
|
+
return code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
|
|
184
|
+
}
|
|
185
|
+
var SQL_START_RE = /^\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
|
|
186
|
+
function stripSql(message) {
|
|
187
|
+
if (!SQL_START_RE.test(message)) return message;
|
|
188
|
+
const at = message.lastIndexOf(" - ");
|
|
189
|
+
return at === -1 ? message : message.slice(at + 3);
|
|
190
|
+
}
|
|
191
|
+
function isClientDisconnect(err) {
|
|
192
|
+
const e = err;
|
|
193
|
+
const code = typeof e?.code === "string" ? e.code : "";
|
|
194
|
+
return code === "ECONNRESET" || code === "ECONNABORTED" || code === "EPIPE" || code === "ERR_STREAM_PREMATURE_CLOSE" || code.startsWith("HPE_") || e?.message === "aborted";
|
|
195
|
+
}
|
|
174
196
|
var logged = globalSingleton("logged", () => /* @__PURE__ */ new WeakSet());
|
|
175
197
|
function markLogged(err) {
|
|
176
198
|
if (typeof err === "object" && err !== null) logged.add(err);
|
|
@@ -197,7 +219,9 @@ export {
|
|
|
197
219
|
defaultRedactor,
|
|
198
220
|
isErrorLike,
|
|
199
221
|
serializeError,
|
|
222
|
+
stripSql,
|
|
223
|
+
isClientDisconnect,
|
|
200
224
|
markLogged,
|
|
201
225
|
isLogged
|
|
202
226
|
};
|
|
203
|
-
//# sourceMappingURL=chunk-
|
|
227
|
+
//# sourceMappingURL=chunk-UDS6S22P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts","../src/global.ts","../src/redact.ts","../src/error.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { globalSingleton } from \"./global\";\nimport type { Fields } from \"./normalize\";\n\nexport interface ContextStore {\n fields: Fields;\n error?: unknown;\n /** Route template captured while the router still knew it (Express error path). */\n route?: string;\n parent?: ContextStore;\n}\n\nconst als = globalSingleton(\"als\", () => new AsyncLocalStorage<ContextStore>());\n\nexport const STORE_KEY: symbol = Symbol.for(\"@tanngo11/log.store\");\n\nexport function runWithStore<T>(fields: Fields, fn: (store: ContextStore) => T): T {\n const parent = als.getStore();\n const store: ContextStore = { fields: { ...parent?.fields, ...fields }, parent };\n return als.run(store, () => fn(store));\n}\n\nexport function withContext<T>(fields: Fields, fn: () => T): T {\n return runWithStore(fields, () => fn());\n}\n\nexport function currentStore(): ContextStore | undefined {\n return als.getStore();\n}\n\nexport function getContext(): Fields {\n return als.getStore()?.fields ?? {};\n}\n\n// Applies to the current context and every enclosing one, so a field added inside a nested\n// withContext/runJob still reaches the request's http.request line.\nexport function addContext(fields: Fields): void {\n for (let s = als.getStore(); s; s = s.parent) Object.assign(s.fields, fields);\n}\n\n// Sets the error on the store and on enclosing stores that have none yet.\nexport function recordErrorIn(store: ContextStore, err: unknown): void {\n store.error = err;\n for (let s = store.parent; s; s = s.parent) if (s.error === undefined) s.error = err;\n}\n\n// Marks the error that the current request/job summary line should carry. Does not write a line.\nexport function recordError(err: unknown): void {\n const store = als.getStore();\n if (store) recordErrorIn(store, err);\n}\n\nexport function attachStore(target: object, store: ContextStore): void {\n (target as Record<symbol, ContextStore>)[STORE_KEY] = store;\n}\n\nexport function storeOf(target: unknown): ContextStore | undefined {\n if (typeof target !== \"object\" || target === null) return undefined;\n return (target as Record<symbol, ContextStore | undefined>)[STORE_KEY];\n}\n","// Singletons shared by every copy of this package loaded in the process (ESM and CJS builds,\n// or duplicated installs), so context and dedupe state never split.\nexport function globalSingleton<T>(name: string, make: () => T): T {\n const key = Symbol.for(`@tanngo11/log.${name}`);\n const g = globalThis as unknown as Record<symbol, T | undefined>;\n let value = g[key];\n if (value === undefined) {\n value = make();\n g[key] = value;\n }\n return value;\n}\n","// Redaction shared by the logger and the error serializer. The collector redacts again,\n// but it cannot see secrets inside JSON-encoded strings, so the app side must catch them.\n\nexport const DEFAULT_REDACT_KEYS = [\n \"password\", \"passwd\", \"pwd\", \"passphrase\", \"secret\", \"client_secret\",\n \"token\", \"access_token\", \"refresh_token\", \"id_token\", \"auth_token\",\n \"api_key\", \"apikey\", \"access_key\", \"secret_key\", \"private_key\",\n \"authorization\", \"cookie\", \"set_cookie\", \"session_id\", \"sessionid\",\n \"credential\", \"credentials\", \"dsn\", \"connection_string\", \"conn_string\", \"jwt\", \"bearer\",\n];\n\n/** Opt-in value patterns for `redactValues`. Not on by default: they can hit ordinary numbers. */\nexport const REDACT_PATTERNS = {\n /**\n * Vietnamese mobile numbers: 0912345678, +84912345678, 84912345678. Letters on either side\n * rule a match out, so hex ids (a UUID segment such as 0912345678e0) are left alone.\n */\n phoneVN: /(?<![\\dA-Za-z+])(?:\\+?84|0)[35789]\\d{8}(?![\\dA-Za-z])/g,\n /** Visa, Mastercard, Amex, Discover-like card numbers, with optional spaces or dashes. */\n paymentCard: /(?<!\\d)(?:4\\d{3}|5[1-5]\\d{2}|2[2-7]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))(?:[ -]?\\d{4}){2}[ -]?\\d{1,4}(?!\\d)/g,\n /** IPv4 addresses. */\n ipv4: /(?<![\\d.])(?:\\d{1,3}\\.){3}\\d{1,3}(?![\\d.])/g,\n} as const;\n\n// Keys that only describe a secret (count, type, timestamps, flags) stay readable.\nconst SAFE_SUFFIXES = [\"_count\", \"_type\", \"_length\", \"_expires_at\", \"_expires_in\", \"_ttl\", \"_at\", \"_ms\", \"_enabled\"];\n\nexport const REDACTED = \"[REDACTED]\";\n\nexport function toSnakeCase(key: string): string {\n return key\n .replace(/([a-z0-9])([A-Z])/g, \"$1_$2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1_$2\")\n .replace(/[^A-Za-z0-9_]+/g, \"_\")\n .toLowerCase();\n}\n\nconst URL_CREDENTIALS_RE = /([a-z][a-z0-9+.-]*:\\/\\/)[^\\s/@:\"']+:[^\\s/@\"']+@/gi;\nconst BEARER_RE = /\\bBearer\\s+[A-Za-z0-9._~+/=-]+/gi;\nconst JWT_RE = /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g;\nconst EMAIL_RE = /[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,})/g;\n\nexport type KeyPattern = string | RegExp;\n\nexport interface RedactorOptions {\n /** Extra sensitive keys: names (matched like the defaults) or patterns tested on the snake_case key. */\n keys?: readonly KeyPattern[];\n /** Extra value patterns; every match inside any string becomes [REDACTED]. */\n values?: readonly RegExp[];\n}\n\nexport interface Redactor {\n isSensitive(key: string): boolean;\n /** Masks secrets and emails that appear inside free text. */\n scrub(s: string): string;\n /** JSON.stringify that redacts sensitive keys at any depth and never throws. */\n json(value: unknown): string;\n}\n\nfunction globalize(re: RegExp): RegExp {\n return re.flags.includes(\"g\") ? new RegExp(re.source, re.flags) : new RegExp(re.source, `${re.flags}g`);\n}\n\nexport function createRedactor(opts: RedactorOptions = {}): Redactor {\n const words = [...DEFAULT_REDACT_KEYS];\n const keyPatterns: RegExp[] = [];\n for (const k of opts.keys ?? []) {\n if (typeof k === \"string\") words.push(toSnakeCase(k));\n else keyPatterns.push(new RegExp(k.source, k.flags.replace(\"g\", \"\")));\n }\n const valuePatterns = (opts.values ?? []).map(globalize);\n\n const isSensitive = (key: string): boolean => {\n const k = toSnakeCase(key);\n if (keyPatterns.some((re) => re.test(k))) return true;\n if (!words.includes(k) && SAFE_SUFFIXES.some((s) => k.endsWith(s))) return false;\n return words.some((w) => k === w || k.startsWith(`${w}_`) || k.endsWith(`_${w}`) || k.includes(`_${w}_`));\n };\n\n const scrub = (s: string): string => {\n let out = s\n .replace(URL_CREDENTIALS_RE, `$1${REDACTED}@`)\n .replace(BEARER_RE, `Bearer ${REDACTED}`)\n .replace(JWT_RE, \"[REDACTED_JWT]\")\n .replace(EMAIL_RE, \"***@$1\");\n for (const re of valuePatterns) out = out.replace(re, REDACTED);\n return out;\n };\n\n const json = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n try {\n const text = JSON.stringify(value, (key, val: unknown) => {\n if (key && val !== null && val !== undefined && isSensitive(key)) return REDACTED;\n if (typeof val === \"bigint\") return val.toString();\n if (val instanceof Error) return { name: val.name, message: val.message };\n return val;\n });\n return text ?? String(value);\n } catch {\n return String(value);\n }\n };\n\n return { isSensitive, scrub, json };\n}\n\nexport const defaultRedactor: Redactor = createRedactor();\n","import { globalSingleton } from \"./global\";\nimport { defaultRedactor } from \"./redact\";\n\nexport interface ErrFields {\n err_type?: string;\n err_message?: string;\n err_code?: string;\n err_stack?: string;\n err_cause?: string;\n /** Database errors (pg): the table and constraint the server reported. */\n db_table?: string;\n db_constraint?: string;\n}\n\nconst MAX_CAUSE_DEPTH = 5;\n\nexport function isErrorLike(v: unknown): v is Error {\n if (v instanceof Error) return true;\n return (\n typeof v === \"object\" &&\n v !== null &&\n typeof (v as { message?: unknown }).message === \"string\" &&\n (\"stack\" in v || \"name\" in v)\n );\n}\n\nfunction errorType(e: Error): string {\n // pg's DatabaseError keeps the lowercase name \"error\"; its class name says more.\n if (e.name && e.name !== \"Error\" && e.name !== \"error\") return e.name;\n const ctor = (e as { constructor?: { name?: string } }).constructor?.name;\n return ctor && ctor !== \"Object\" ? ctor : \"Error\";\n}\n\nexport function serializeError(err: unknown): ErrFields {\n if (!isErrorLike(err)) {\n if (typeof err === \"string\") return { err_type: \"string\", err_message: err };\n return { err_type: typeof err, err_message: defaultRedactor.json(err) };\n }\n const e = err as Error & { code?: unknown; cause?: unknown; table?: unknown; constraint?: unknown };\n const out: ErrFields = { err_type: errorType(e), err_message: String(e.message ?? \"\") };\n if (e.code !== undefined && e.code !== null) out.err_code = String(e.code);\n if (isSqlState(out.err_code)) {\n out.err_message = stripSql(out.err_message ?? \"\");\n if (typeof e.table === \"string\" && e.table) out.db_table = e.table;\n if (typeof e.constraint === \"string\" && e.constraint) out.db_constraint = e.constraint;\n }\n if (typeof e.stack === \"string\") out.err_stack = e.stack;\n\n const causes: string[] = [];\n let cause = e.cause;\n while (cause !== undefined && cause !== null && causes.length < MAX_CAUSE_DEPTH) {\n if (isErrorLike(cause)) {\n causes.push(`${errorType(cause)}: ${cause.message}`);\n cause = (cause as { cause?: unknown }).cause;\n } else {\n causes.push(defaultRedactor.json(cause));\n cause = undefined;\n }\n }\n if (causes.length > 0) out.err_cause = causes.join(\" <- \");\n return out;\n}\n\n// Postgres SQLSTATE codes: five digits or uppercase letters (23505, 40P01, 25P02).\nfunction isSqlState(code: string | undefined): boolean {\n return code !== undefined && /^[0-9A-Z]{5}$/.test(code);\n}\n\nconst SQL_START_RE = /^\\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\\b/i;\n\n/**\n * knex prefixes a failed query's message with the whole statement (\"update ... - deadlock\n * detected\"). A long statement filled err_message and the truncation cut off the server's reason,\n * the only part worth reading. Keeps the reason; the statement never reaches the log.\n */\nexport function stripSql(message: string): string {\n if (!SQL_START_RE.test(message)) return message;\n const at = message.lastIndexOf(\" - \");\n return at === -1 ? message : message.slice(at + 3);\n}\n\n// The client hung up or sent a malformed request: worth a warn, not an error on our side.\nexport function isClientDisconnect(err: unknown): boolean {\n const e = err as { code?: unknown; message?: unknown } | null;\n const code = typeof e?.code === \"string\" ? e.code : \"\";\n return (\n code === \"ECONNRESET\" ||\n code === \"ECONNABORTED\" ||\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_PREMATURE_CLOSE\" ||\n code.startsWith(\"HPE_\") ||\n e?.message === \"aborted\"\n );\n}\n\nconst logged = globalSingleton(\"logged\", () => new WeakSet<object>());\n\n// Marks an error as already written, so a later boundary (e.g. Next's onRequestError)\n// does not log it a second time.\nexport function markLogged(err: unknown): void {\n if (typeof err === \"object\" && err !== null) logged.add(err);\n}\n\nexport function isLogged(err: unknown): boolean {\n return typeof err === \"object\" && err !== null && logged.has(err);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
logHttpRequest,
|
|
3
3
|
requestContextFields
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-AF4YR4Y4.js";
|
|
5
5
|
import {
|
|
6
6
|
attachStore,
|
|
7
7
|
recordErrorIn,
|
|
8
8
|
runWithStore,
|
|
9
9
|
storeOf
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-UDS6S22P.js";
|
|
11
11
|
|
|
12
12
|
// src/express.ts
|
|
13
13
|
function joinRoute(baseUrl, path) {
|
|
@@ -69,4 +69,4 @@ export {
|
|
|
69
69
|
requestLogger,
|
|
70
70
|
errorRecorder
|
|
71
71
|
};
|
|
72
|
-
//# sourceMappingURL=chunk-
|
|
72
|
+
//# sourceMappingURL=chunk-UOIJCSP5.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
globalSingleton
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UDS6S22P.js";
|
|
4
4
|
|
|
5
5
|
// src/process.ts
|
|
6
6
|
function logStartup(log, fields = {}) {
|
|
@@ -83,4 +83,4 @@ export {
|
|
|
83
83
|
installProcessHandlers,
|
|
84
84
|
logProcessWarnings
|
|
85
85
|
};
|
|
86
|
-
//# sourceMappingURL=chunk-
|
|
86
|
+
//# sourceMappingURL=chunk-URPHVC4Y.js.map
|
package/dist/express.cjs
CHANGED
|
@@ -147,7 +147,7 @@ function isErrorLike(v) {
|
|
|
147
147
|
return typeof v === "object" && v !== null && typeof v.message === "string" && ("stack" in v || "name" in v);
|
|
148
148
|
}
|
|
149
149
|
function errorType(e) {
|
|
150
|
-
if (e.name && e.name !== "Error") return e.name;
|
|
150
|
+
if (e.name && e.name !== "Error" && e.name !== "error") return e.name;
|
|
151
151
|
const ctor = e.constructor?.name;
|
|
152
152
|
return ctor && ctor !== "Object" ? ctor : "Error";
|
|
153
153
|
}
|
|
@@ -159,6 +159,11 @@ function serializeError(err) {
|
|
|
159
159
|
const e = err;
|
|
160
160
|
const out = { err_type: errorType(e), err_message: String(e.message ?? "") };
|
|
161
161
|
if (e.code !== void 0 && e.code !== null) out.err_code = String(e.code);
|
|
162
|
+
if (isSqlState(out.err_code)) {
|
|
163
|
+
out.err_message = stripSql(out.err_message ?? "");
|
|
164
|
+
if (typeof e.table === "string" && e.table) out.db_table = e.table;
|
|
165
|
+
if (typeof e.constraint === "string" && e.constraint) out.db_constraint = e.constraint;
|
|
166
|
+
}
|
|
162
167
|
if (typeof e.stack === "string") out.err_stack = e.stack;
|
|
163
168
|
const causes = [];
|
|
164
169
|
let cause = e.cause;
|
|
@@ -174,6 +179,20 @@ function serializeError(err) {
|
|
|
174
179
|
if (causes.length > 0) out.err_cause = causes.join(" <- ");
|
|
175
180
|
return out;
|
|
176
181
|
}
|
|
182
|
+
function isSqlState(code) {
|
|
183
|
+
return code !== void 0 && /^[0-9A-Z]{5}$/.test(code);
|
|
184
|
+
}
|
|
185
|
+
var SQL_START_RE = /^\s*(?:select|insert|update|delete|with|create|alter|drop|truncate|lock)\b/i;
|
|
186
|
+
function stripSql(message) {
|
|
187
|
+
if (!SQL_START_RE.test(message)) return message;
|
|
188
|
+
const at = message.lastIndexOf(" - ");
|
|
189
|
+
return at === -1 ? message : message.slice(at + 3);
|
|
190
|
+
}
|
|
191
|
+
function isClientDisconnect(err) {
|
|
192
|
+
const e = err;
|
|
193
|
+
const code = typeof e?.code === "string" ? e.code : "";
|
|
194
|
+
return code === "ECONNRESET" || code === "ECONNABORTED" || code === "EPIPE" || code === "ERR_STREAM_PREMATURE_CLOSE" || code.startsWith("HPE_") || e?.message === "aborted";
|
|
195
|
+
}
|
|
177
196
|
var logged = globalSingleton("logged", () => /* @__PURE__ */ new WeakSet());
|
|
178
197
|
function markLogged(err) {
|
|
179
198
|
if (typeof err === "object" && err !== null) logged.add(err);
|
|
@@ -233,12 +252,15 @@ function shouldSkip(method, path, skipPaths = DEFAULT_SKIP_PATHS) {
|
|
|
233
252
|
}
|
|
234
253
|
function logHttpRequest(log, info, opts = {}) {
|
|
235
254
|
if (info.status < 500 && shouldSkip(info.method, info.path, opts.skipPaths)) return;
|
|
236
|
-
const
|
|
255
|
+
const hasErr = info.err !== void 0 && info.err !== null;
|
|
256
|
+
const clientGone = hasErr && isClientDisconnect(info.err);
|
|
257
|
+
const level = info.status >= 500 && !clientGone ? "error" : info.status >= 400 ? "warn" : "info";
|
|
237
258
|
const path = stripUrl(info.path);
|
|
238
259
|
const errFields = {};
|
|
239
|
-
if (
|
|
260
|
+
if (hasErr) {
|
|
240
261
|
Object.assign(errFields, serializeError(info.err));
|
|
241
|
-
if (info.status < 500) delete errFields.err_stack;
|
|
262
|
+
if (info.status < 500 || clientGone) delete errFields.err_stack;
|
|
263
|
+
if (clientGone) errFields.client_disconnect = true;
|
|
242
264
|
markLogged(info.err);
|
|
243
265
|
}
|
|
244
266
|
const message = info.route ? `${info.method} ${info.route} ${info.status}` : `${info.method} ${info.status}`;
|