@easyweb/logging 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/context.d.ts +60 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +42 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +44 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +69 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/http-logger.d.ts +14 -0
- package/dist/middleware/http-logger.d.ts.map +1 -0
- package/dist/middleware/http-logger.js +119 -0
- package/dist/middleware/http-logger.js.map +1 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/redact.d.ts +16 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +69 -0
- package/dist/redact.js.map +1 -0
- package/dist/serialize.d.ts +27 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +124 -0
- package/dist/serialize.js.map +1 -0
- package/dist/transport.d.ts +36 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +63 -0
- package/dist/transport.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @easyweb/logging
|
|
2
|
+
|
|
3
|
+
One pino configuration for every Easyweb service, request-scoped context that
|
|
4
|
+
does not have to be threaded by hand, an HTTP access log, and the redaction a
|
|
5
|
+
log needs before it leaves the pod.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
`src/lib/logger.ts` was copy-pasted into 21 services. It had drifted into two
|
|
10
|
+
variants; `domain` and `moderation` both defaulted their service name to
|
|
11
|
+
`"billing-service"`; and seven services were missing the guard that stops
|
|
12
|
+
pino-pretty's worker thread making Jest exit 1 under a green report.
|
|
13
|
+
|
|
14
|
+
The bigger problem was correlation. `getLogger(req)` returned a child bound to
|
|
15
|
+
`requestId` and had to be passed down as an explicit `log` parameter. Roughly
|
|
16
|
+
100 call sites did that; the other ~1,300 logged through the module-level root
|
|
17
|
+
logger and carried no request id at all.
|
|
18
|
+
|
|
19
|
+
## The mixin
|
|
20
|
+
|
|
21
|
+
`createServiceLogger` installs a pino `mixin` that reads an `AsyncLocalStorage`
|
|
22
|
+
store. Every existing call site gains the ambient fields with no edit:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// unchanged call site
|
|
26
|
+
log.info({ projectId }, "Project created");
|
|
27
|
+
|
|
28
|
+
// what it now emits
|
|
29
|
+
{ "service": "project-service", "requestId": "…", "userId": "…", "projectId": "…" }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
pino merges the mixin UNDER the object the call site passed, so an explicit
|
|
33
|
+
field always wins over an ambient one.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// src/lib/logger.ts
|
|
39
|
+
import { createServiceLogger } from "@easyweb/logging";
|
|
40
|
+
import config from "../config";
|
|
41
|
+
|
|
42
|
+
const { logger, getLogger } = createServiceLogger({
|
|
43
|
+
serviceName: config.serviceName,
|
|
44
|
+
logLevel: config.logLevel,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export { getLogger };
|
|
48
|
+
export default logger;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// src/app.ts — the access log needs the scope, so it goes after requestContext
|
|
53
|
+
app.use(requestContext);
|
|
54
|
+
app.use(createHttpLogger(logger));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Opening a scope outside a request — a BullMQ job, a broker handler:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
await runWithContext({ jobId: job.id, job: job.data.type }, () => handle(job));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Adding to a scope already running — `authenticate` does this once it has
|
|
64
|
+
verified the token:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
bindContext({ userId: decoded.sub });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## The access log
|
|
71
|
+
|
|
72
|
+
One line per finished request, `msg: "request"`.
|
|
73
|
+
|
|
74
|
+
`route` is the route **pattern** (`/billing/me/invoices/:invoiceId`), and that
|
|
75
|
+
is a contract rather than a convenience: the RED metrics are recording rules
|
|
76
|
+
over it, so a per-id value would make the series unbounded. `path` carries the
|
|
77
|
+
real path beside it, from `req.originalUrl` — Express strips the mount prefix
|
|
78
|
+
off `req.path` during router dispatch, so reading that in the finish callback
|
|
79
|
+
names a route that does not exist.
|
|
80
|
+
|
|
81
|
+
The query string is never logged. `?token=…` on the verification route is a live
|
|
82
|
+
credential.
|
|
83
|
+
|
|
84
|
+
`/health`, `/livez`, `/readyz` and `/metrics` are skipped — the kubelet would
|
|
85
|
+
otherwise make the readiness probe the largest log stream in the cluster.
|
|
86
|
+
|
|
87
|
+
## Redaction
|
|
88
|
+
|
|
89
|
+
Two mechanisms, covering different shapes:
|
|
90
|
+
|
|
91
|
+
- **`REDACT_PATHS`** — pino's fixed-path redaction, for our own payloads.
|
|
92
|
+
Passwords, tokens, cookies and auth headers, at the top level and one level
|
|
93
|
+
down.
|
|
94
|
+
- **`scrubResponseData`** — a recursive, depth- and size-capped walk over
|
|
95
|
+
`err.response.data`, for payloads we do not control. Xendit, Stripe, Resend,
|
|
96
|
+
Meta, Apify, Cloudflare and Gitea all put personal data in an error body, and
|
|
97
|
+
407 `logger.error({ err })` call sites fed it into the log verbatim and
|
|
98
|
+
uncapped.
|
|
99
|
+
|
|
100
|
+
`err.response.data` is capped at 2 KB **after** scrubbing, not instead of it —
|
|
101
|
+
truncating first would keep whatever fitted, which for a failed charge is the
|
|
102
|
+
payer block.
|
|
103
|
+
|
|
104
|
+
## What is deliberately NOT redacted
|
|
105
|
+
|
|
106
|
+
Email addresses. auth-service logs one on three lines — signup, an
|
|
107
|
+
operator-create refusal, and a Google registration — and they are the signup
|
|
108
|
+
audit trail: `userId` alone cannot answer "which address did they sign up
|
|
109
|
+
with". The exposure is real and bounded by log retention. See
|
|
110
|
+
`docs/platform/ADR-0001`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fields every log line in a scope carries, without any call site naming
|
|
3
|
+
* them.
|
|
4
|
+
*
|
|
5
|
+
* This is the whole reason the package exists. The platform had ~1,389
|
|
6
|
+
* `logger.info({...}, "...")` call sites and correlation reached almost none of
|
|
7
|
+
* them: `getLogger(req)` returned a child bound to `requestId` and had to be
|
|
8
|
+
* threaded down by hand as an explicit `log` parameter, so anything not handed
|
|
9
|
+
* one fell back to the module-level root logger and logged no requestId at all.
|
|
10
|
+
* Threading it through the remaining ~1,300 signatures was never going to
|
|
11
|
+
* happen. A pino `mixin` reading this store injects the fields at log time
|
|
12
|
+
* instead, so every existing call site correlates with no edit.
|
|
13
|
+
*
|
|
14
|
+
* The index signature is deliberate: a caller may add anything it wants to see
|
|
15
|
+
* on every line for the rest of the scope. What must NOT go in are values that
|
|
16
|
+
* are large, that change per line, or that are secret — the whole object is
|
|
17
|
+
* serialized onto every single log record inside the scope.
|
|
18
|
+
*/
|
|
19
|
+
export interface LogContext {
|
|
20
|
+
/** From `x-request-id`, or minted. The join key across services. */
|
|
21
|
+
requestId?: string;
|
|
22
|
+
/** Bound by `authenticate` once the access token is verified. */
|
|
23
|
+
userId?: string;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
projectId?: string;
|
|
26
|
+
/** Broker consume scope. */
|
|
27
|
+
exchange?: string;
|
|
28
|
+
routingKey?: string;
|
|
29
|
+
queue?: string;
|
|
30
|
+
retryCount?: number;
|
|
31
|
+
/** BullMQ job scope. */
|
|
32
|
+
jobId?: string;
|
|
33
|
+
job?: string;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Runs `fn` with `fields` merged over whatever scope is already active.
|
|
38
|
+
*
|
|
39
|
+
* Nesting is additive rather than replacing, so a broker handler that opens a
|
|
40
|
+
* job scope inside a request scope keeps the requestId. A FRESH object is
|
|
41
|
+
* created each time, which is what makes `bindContext`'s mutation safe.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runWithContext<T>(fields: LogContext, fn: () => T): T;
|
|
44
|
+
/**
|
|
45
|
+
* Adds fields to the scope that is ALREADY running.
|
|
46
|
+
*
|
|
47
|
+
* `authenticate` needs this: the scope is opened by `requestContext` before any
|
|
48
|
+
* token has been read, and `userId` only becomes known several middlewares
|
|
49
|
+
* later. Mutating in place is what makes that visible to a log line emitted
|
|
50
|
+
* earlier-registered-but-later-run — notably the access log's `res.on("finish")`
|
|
51
|
+
* callback, which captures the store by reference at request start.
|
|
52
|
+
*
|
|
53
|
+
* A no-op outside a scope. That is deliberate: a seeder or a script calling a
|
|
54
|
+
* service method directly has no request, and throwing there would make the
|
|
55
|
+
* logging layer the thing that breaks `npm run seed`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function bindContext(fields: LogContext): void;
|
|
58
|
+
/** The active scope, or undefined outside one. */
|
|
59
|
+
export declare function getContext(): LogContext | undefined;
|
|
60
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,UAAU;IACzB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,wBAAwB;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAGpE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAIpD;AAED,kDAAkD;AAClD,wBAAgB,UAAU,IAAI,UAAU,GAAG,SAAS,CAEnD"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runWithContext = runWithContext;
|
|
4
|
+
exports.bindContext = bindContext;
|
|
5
|
+
exports.getContext = getContext;
|
|
6
|
+
const async_hooks_1 = require("async_hooks");
|
|
7
|
+
const storage = new async_hooks_1.AsyncLocalStorage();
|
|
8
|
+
/**
|
|
9
|
+
* Runs `fn` with `fields` merged over whatever scope is already active.
|
|
10
|
+
*
|
|
11
|
+
* Nesting is additive rather than replacing, so a broker handler that opens a
|
|
12
|
+
* job scope inside a request scope keeps the requestId. A FRESH object is
|
|
13
|
+
* created each time, which is what makes `bindContext`'s mutation safe.
|
|
14
|
+
*/
|
|
15
|
+
function runWithContext(fields, fn) {
|
|
16
|
+
const parent = storage.getStore();
|
|
17
|
+
return storage.run({ ...parent, ...fields }, fn);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Adds fields to the scope that is ALREADY running.
|
|
21
|
+
*
|
|
22
|
+
* `authenticate` needs this: the scope is opened by `requestContext` before any
|
|
23
|
+
* token has been read, and `userId` only becomes known several middlewares
|
|
24
|
+
* later. Mutating in place is what makes that visible to a log line emitted
|
|
25
|
+
* earlier-registered-but-later-run — notably the access log's `res.on("finish")`
|
|
26
|
+
* callback, which captures the store by reference at request start.
|
|
27
|
+
*
|
|
28
|
+
* A no-op outside a scope. That is deliberate: a seeder or a script calling a
|
|
29
|
+
* service method directly has no request, and throwing there would make the
|
|
30
|
+
* logging layer the thing that breaks `npm run seed`.
|
|
31
|
+
*/
|
|
32
|
+
function bindContext(fields) {
|
|
33
|
+
const store = storage.getStore();
|
|
34
|
+
if (!store)
|
|
35
|
+
return;
|
|
36
|
+
Object.assign(store, fields);
|
|
37
|
+
}
|
|
38
|
+
/** The active scope, or undefined outside one. */
|
|
39
|
+
function getContext() {
|
|
40
|
+
return storage.getStore();
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":";;AAkDA,wCAGC;AAeD,kCAIC;AAGD,gCAEC;AA7ED,6CAAgD;AAyChD,MAAM,OAAO,GAAG,IAAI,+BAAiB,EAAc,CAAC;AAEpD;;;;;;GAMG;AACH,SAAgB,cAAc,CAAI,MAAkB,EAAE,EAAW;IAC/D,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAClC,OAAO,OAAO,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAgB,WAAW,CAAC,MAAkB;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IACjC,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,kDAAkD;AAClD,SAAgB,UAAU;IACxB,OAAO,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC5B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./context"), exports);
|
|
18
|
+
__exportStar(require("./logger"), exports);
|
|
19
|
+
__exportStar(require("./middleware"), exports);
|
|
20
|
+
__exportStar(require("./redact"), exports);
|
|
21
|
+
__exportStar(require("./serialize"), exports);
|
|
22
|
+
__exportStar(require("./transport"), exports);
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,4CAA0B;AAC1B,2CAAyB;AACzB,+CAA6B;AAC7B,2CAAyB;AACzB,8CAA4B;AAC5B,8CAA4B"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
import type { Request } from "express";
|
|
3
|
+
export interface ServiceLoggerOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The `service` field on every line, and the rotated filename under
|
|
6
|
+
* `LOG_DIR`. Pass `config.serviceName` — every service already has one and
|
|
7
|
+
* every one of them is correct, which the duplicated logger.ts was not:
|
|
8
|
+
* domain's and moderation's both defaulted to "billing-service".
|
|
9
|
+
*/
|
|
10
|
+
serviceName: string;
|
|
11
|
+
/** `config.logLevel`. Falls back to info in production, debug elsewhere. */
|
|
12
|
+
logLevel?: string | undefined;
|
|
13
|
+
logDir?: string | undefined;
|
|
14
|
+
env?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Where the logger writes, when no `transport` is built.
|
|
17
|
+
*
|
|
18
|
+
* Exists for tests, which need to read back what was actually emitted —
|
|
19
|
+
* redaction and the mixin both happen inside pino, so asserting on them from
|
|
20
|
+
* the outside means capturing the stream. Under NODE_ENV=test there is no
|
|
21
|
+
* transport anyway (see `shouldPretty`), so this never competes with one.
|
|
22
|
+
*/
|
|
23
|
+
destination?: pino.DestinationStream | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface ServiceLogger {
|
|
26
|
+
logger: pino.Logger;
|
|
27
|
+
/**
|
|
28
|
+
* Kept for the ~42 files that import it. It is now redundant — the mixin
|
|
29
|
+
* already puts `requestId` on every line inside a request — but harmless, and
|
|
30
|
+
* removing it would be a 42-file change for no behaviour.
|
|
31
|
+
*/
|
|
32
|
+
getLogger: (req: Request) => pino.Logger;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Builds the service's root logger.
|
|
36
|
+
*
|
|
37
|
+
* The one genuinely new thing here is `mixin`. pino calls it on every level
|
|
38
|
+
* method and merges the result UNDER the object the call site passed, so an
|
|
39
|
+
* explicit field always wins over the ambient one. That is what lets every
|
|
40
|
+
* existing `log.info({ projectId }, "...")` gain a requestId without being
|
|
41
|
+
* touched.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createServiceLogger(options: ServiceLoggerOptions): ServiceLogger;
|
|
44
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMvC,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,IAAI,CAAC,iBAAiB,GAAG,SAAS,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,MAAM,CAAC;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAoDf"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createServiceLogger = createServiceLogger;
|
|
7
|
+
const pino_1 = __importDefault(require("pino"));
|
|
8
|
+
const context_1 = require("./context");
|
|
9
|
+
const redact_1 = require("./redact");
|
|
10
|
+
const serialize_1 = require("./serialize");
|
|
11
|
+
const transport_1 = require("./transport");
|
|
12
|
+
/**
|
|
13
|
+
* Builds the service's root logger.
|
|
14
|
+
*
|
|
15
|
+
* The one genuinely new thing here is `mixin`. pino calls it on every level
|
|
16
|
+
* method and merges the result UNDER the object the call site passed, so an
|
|
17
|
+
* explicit field always wins over the ambient one. That is what lets every
|
|
18
|
+
* existing `log.info({ projectId }, "...")` gain a requestId without being
|
|
19
|
+
* touched.
|
|
20
|
+
*/
|
|
21
|
+
function createServiceLogger(options) {
|
|
22
|
+
const env = options.env ?? process.env.NODE_ENV ?? "development";
|
|
23
|
+
const logDir = options.logDir ?? process.env.LOG_DIR;
|
|
24
|
+
// `...(x ? { x } : {})` rather than `transport: buildTransport(...)`.
|
|
25
|
+
// `exactOptionalPropertyTypes` is on in this package and OFF in every
|
|
26
|
+
// service, so the shape that compiled when this file was duplicated 21 times
|
|
27
|
+
// does not compile here. The spread is the repo's documented idiom for it.
|
|
28
|
+
const transport = (0, transport_1.buildTransport)(logDir, options.serviceName, (0, transport_1.shouldPretty)(env));
|
|
29
|
+
const pinoOptions = {
|
|
30
|
+
level: options.logLevel || (env === "production" ? "info" : "debug"),
|
|
31
|
+
base: {
|
|
32
|
+
service: options.serviceName,
|
|
33
|
+
env,
|
|
34
|
+
},
|
|
35
|
+
timestamp: pino_1.default.stdTimeFunctions.isoTime,
|
|
36
|
+
/**
|
|
37
|
+
* Never throws. A mixin that threw would take down the log call, and log
|
|
38
|
+
* calls are overwhelmingly on error paths — the failure would swallow
|
|
39
|
+
* exactly the line somebody needed.
|
|
40
|
+
*/
|
|
41
|
+
mixin() {
|
|
42
|
+
try {
|
|
43
|
+
return (0, context_1.getContext)() ?? {};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
serializers: {
|
|
50
|
+
err: serialize_1.serializeError,
|
|
51
|
+
error: serialize_1.serializeError,
|
|
52
|
+
},
|
|
53
|
+
redact: redact_1.REDACT,
|
|
54
|
+
...(transport ? { transport } : {}),
|
|
55
|
+
};
|
|
56
|
+
// Two call forms rather than `pino(opts, undefined)`: pino's second parameter
|
|
57
|
+
// is a real destination, and handing it an explicit undefined relies on an
|
|
58
|
+
// internal falsy check rather than the documented overload.
|
|
59
|
+
const logger = options.destination
|
|
60
|
+
? (0, pino_1.default)(pinoOptions, options.destination)
|
|
61
|
+
: (0, pino_1.default)(pinoOptions);
|
|
62
|
+
// Read defensively rather than importing the global `Express.Request`
|
|
63
|
+
// augmentation from @easyweb/authentication: that package depends on THIS
|
|
64
|
+
// one, so importing it back would be a cycle. Same call common-errors'
|
|
65
|
+
// errorHandler makes, for the same reason.
|
|
66
|
+
const getLogger = (req) => logger.child({ requestId: req.requestId });
|
|
67
|
+
return { logger, getLogger };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":";;;;;AAiDA,kDAsDC;AAvGD,gDAAwB;AAExB,uCAAuC;AACvC,qCAAkC;AAClC,2CAA6C;AAC7C,2CAA2D;AAmC3D;;;;;;;;GAQG;AACH,SAAgB,mBAAmB,CACjC,OAA6B;IAE7B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IAErD,sEAAsE;IACtE,sEAAsE;IACtE,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAM,SAAS,GAAG,IAAA,0BAAc,EAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,IAAA,wBAAY,EAAC,GAAG,CAAC,CAAC,CAAC;IAEjF,MAAM,WAAW,GAAuB;QACtC,KAAK,EAAE,OAAO,CAAC,QAAQ,IAAI,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QACpE,IAAI,EAAE;YACJ,OAAO,EAAE,OAAO,CAAC,WAAW;YAC5B,GAAG;SACJ;QACD,SAAS,EAAE,cAAI,CAAC,gBAAgB,CAAC,OAAO;QACxC;;;;WAIG;QACH,KAAK;YACH,IAAI,CAAC;gBACH,OAAO,IAAA,oBAAU,GAAE,IAAI,EAAE,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QACD,WAAW,EAAE;YACX,GAAG,EAAE,0BAAc;YACnB,KAAK,EAAE,0BAAc;SACtB;QACD,MAAM,EAAE,eAAM;QACd,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAC;IAEF,8EAA8E;IAC9E,2EAA2E;IAC3E,4DAA4D;IAC5D,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW;QAChC,CAAC,CAAC,IAAA,cAAI,EAAC,WAAW,EAAE,OAAO,CAAC,WAAW,CAAC;QACxC,CAAC,CAAC,IAAA,cAAI,EAAC,WAAW,CAAC,CAAC;IAEtB,sEAAsE;IACtE,0EAA0E;IAC1E,uEAAuE;IACvE,2CAA2C;IAC3C,MAAM,SAAS,GAAG,CAAC,GAAY,EAAe,EAAE,CAC9C,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAG,GAA8B,CAAC,SAAS,EAAE,CAAC,CAAC;IAEzE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RequestHandler } from "express";
|
|
2
|
+
import type pino from "pino";
|
|
3
|
+
/**
|
|
4
|
+
* One line per finished request.
|
|
5
|
+
*
|
|
6
|
+
* The platform had NO access log of any kind: the only per-request line came
|
|
7
|
+
* from the shared error handler, and only for 5xx — `logClientErrors` defaults
|
|
8
|
+
* false, so every 400, 401, 403, 404 and 422 in the platform was invisible. A
|
|
9
|
+
* customer reporting "it says my session expired" produced nothing to look at.
|
|
10
|
+
*
|
|
11
|
+
* Mount it immediately after `requestContext`, so the ALS scope exists.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createHttpLogger(logger: pino.Logger): RequestHandler;
|
|
14
|
+
//# sourceMappingURL=http-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-logger.d.ts","sourceRoot":"","sources":["../../src/middleware/http-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AA6D7B;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,cAAc,CA6DpE"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHttpLogger = createHttpLogger;
|
|
4
|
+
const context_1 = require("../context");
|
|
5
|
+
/**
|
|
6
|
+
* Probe paths. The kubelet hits `/livez` every 20s and `/health` every 10s
|
|
7
|
+
* against ~30 pods, so logging them would make the readiness probe the single
|
|
8
|
+
* largest log stream in the cluster and push everything else out of retention.
|
|
9
|
+
*
|
|
10
|
+
* A probe FAILURE is still visible — the readiness gate shows up as an endpoint
|
|
11
|
+
* leaving the Service, which is a metric rather than a log line.
|
|
12
|
+
*/
|
|
13
|
+
const SKIP_PATHS = new Set(["/health", "/livez", "/readyz", "/metrics"]);
|
|
14
|
+
/**
|
|
15
|
+
* The route PATTERN, not the URL.
|
|
16
|
+
*
|
|
17
|
+
* This field is a contract, not a convenience: the RED metrics are recording
|
|
18
|
+
* rules over it, so `/billing/me/invoices/:id` must stay one value rather than
|
|
19
|
+
* one per invoice. `req.path` is logged alongside it for the actual id, and
|
|
20
|
+
* carries no query string — same reason admin-service's audit row excludes one:
|
|
21
|
+
* `?token=…` on the verification route would put a live credential in the log.
|
|
22
|
+
*/
|
|
23
|
+
function routeOf(req) {
|
|
24
|
+
const raw = req.route?.path;
|
|
25
|
+
const base = req.baseUrl ?? "";
|
|
26
|
+
const join = (p) => `${base}${p === "/" ? "" : p}` || "/";
|
|
27
|
+
if (typeof raw === "string")
|
|
28
|
+
return join(raw);
|
|
29
|
+
if (Array.isArray(raw))
|
|
30
|
+
return raw.map((p) => join(String(p))).join("|");
|
|
31
|
+
if (raw !== undefined)
|
|
32
|
+
return `${base}(pattern)`;
|
|
33
|
+
// No route matched — a 404, or a middleware that answered before routing.
|
|
34
|
+
return base ? `${base}(unmatched)` : "(unmatched)";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The caller's address, best effort.
|
|
38
|
+
*
|
|
39
|
+
* `req.ip` is only the real client where `trust proxy` is set, which is true in
|
|
40
|
+
* exactly three services (admin, analytics, auth). Everywhere else it is the
|
|
41
|
+
* ingress-nginx pod. Reading the first `x-forwarded-for` hop directly gives a
|
|
42
|
+
* useful answer in the other eighteen without changing `trust proxy`, which
|
|
43
|
+
* feeds real security decisions and is not something a logging change should
|
|
44
|
+
* touch.
|
|
45
|
+
*
|
|
46
|
+
* It is spoofable by anyone who can set the header. That is acceptable for a
|
|
47
|
+
* log field and would NOT be for an authorization one.
|
|
48
|
+
*/
|
|
49
|
+
function clientIp(req) {
|
|
50
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
51
|
+
const first = typeof forwarded === "string"
|
|
52
|
+
? forwarded.split(",")[0]
|
|
53
|
+
: Array.isArray(forwarded)
|
|
54
|
+
? forwarded[0]
|
|
55
|
+
: undefined;
|
|
56
|
+
return first?.trim() || req.ip;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One line per finished request.
|
|
60
|
+
*
|
|
61
|
+
* The platform had NO access log of any kind: the only per-request line came
|
|
62
|
+
* from the shared error handler, and only for 5xx — `logClientErrors` defaults
|
|
63
|
+
* false, so every 400, 401, 403, 404 and 422 in the platform was invisible. A
|
|
64
|
+
* customer reporting "it says my session expired" produced nothing to look at.
|
|
65
|
+
*
|
|
66
|
+
* Mount it immediately after `requestContext`, so the ALS scope exists.
|
|
67
|
+
*/
|
|
68
|
+
function createHttpLogger(logger) {
|
|
69
|
+
return function httpLogger(req, res, next) {
|
|
70
|
+
/**
|
|
71
|
+
* From `originalUrl`, never `req.path`.
|
|
72
|
+
*
|
|
73
|
+
* Express STRIPS the mount point off `req.url` while a mounted router
|
|
74
|
+
* dispatches, and `req.path` is a getter over it — so by the time the
|
|
75
|
+
* finish callback runs, a request to `/billing/me/invoices/x` reports
|
|
76
|
+
* `/me/invoices/x`. Every service in this platform mounts its router at a
|
|
77
|
+
* prefix, so that is every request, and the logged path would name a route
|
|
78
|
+
* that does not exist. `originalUrl` is not mutated.
|
|
79
|
+
*
|
|
80
|
+
* The query string is cut rather than logged: `?token=…` on the
|
|
81
|
+
* verification route is a live credential, and admin-service's audit row
|
|
82
|
+
* excludes one for the same reason.
|
|
83
|
+
*/
|
|
84
|
+
const originalUrl = req.originalUrl || req.url;
|
|
85
|
+
const queryAt = originalUrl.indexOf("?");
|
|
86
|
+
const path = queryAt === -1 ? originalUrl : originalUrl.slice(0, queryAt);
|
|
87
|
+
if (SKIP_PATHS.has(path))
|
|
88
|
+
return next();
|
|
89
|
+
const startedAt = process.hrtime.bigint();
|
|
90
|
+
/**
|
|
91
|
+
* Captured by REFERENCE at request start, not read inside the callback.
|
|
92
|
+
*
|
|
93
|
+
* `res.on("finish")` fires from the socket write path, and whether the ALS
|
|
94
|
+
* scope is still active there depends on how the response was flushed —
|
|
95
|
+
* which is not a thing to make the access log depend on. `bindContext`
|
|
96
|
+
* mutates this same object, so `userId` set later by `authenticate` is
|
|
97
|
+
* still visible here.
|
|
98
|
+
*/
|
|
99
|
+
const context = (0, context_1.getContext)();
|
|
100
|
+
const fallback = req.requestId;
|
|
101
|
+
res.on("finish", () => {
|
|
102
|
+
const status = res.statusCode;
|
|
103
|
+
const level = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
|
104
|
+
const contentLength = Number(res.getHeader("content-length"));
|
|
105
|
+
logger[level]({
|
|
106
|
+
...(context ?? (fallback ? { requestId: fallback } : {})),
|
|
107
|
+
method: req.method,
|
|
108
|
+
route: routeOf(req),
|
|
109
|
+
path,
|
|
110
|
+
status,
|
|
111
|
+
durationMs: Math.round(Number(process.hrtime.bigint() - startedAt) / 1e5) / 10,
|
|
112
|
+
...(Number.isFinite(contentLength) ? { bytes: contentLength } : {}),
|
|
113
|
+
...(clientIp(req) ? { ip: clientIp(req) } : {}),
|
|
114
|
+
}, "request");
|
|
115
|
+
});
|
|
116
|
+
next();
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=http-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-logger.js","sourceRoot":"","sources":["../../src/middleware/http-logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./http-logger"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gDAA8B"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pino's `redact.paths`, which is a fixed-path mechanism: `a.b.c`, `a[*].b` and
|
|
3
|
+
* `*.b` only. It cannot walk to arbitrary depth, which is why
|
|
4
|
+
* `scrubResponseData` in serialize.ts exists as well — these two are not
|
|
5
|
+
* alternatives, they cover different shapes.
|
|
6
|
+
*
|
|
7
|
+
* This list covers OUR payloads. The service inherited only the last ten
|
|
8
|
+
* entries, all of them axios auth headers, so a request body reaching a log
|
|
9
|
+
* carried its password in the clear.
|
|
10
|
+
*/
|
|
11
|
+
export declare const REDACT_PATHS: string[];
|
|
12
|
+
export declare const REDACT: {
|
|
13
|
+
paths: string[];
|
|
14
|
+
censor: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,EAqDhC,CAAC;AAEF,eAAO,MAAM,MAAM;;;CAGlB,CAAC"}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REDACT = exports.REDACT_PATHS = void 0;
|
|
4
|
+
const serialize_1 = require("./serialize");
|
|
5
|
+
/**
|
|
6
|
+
* pino's `redact.paths`, which is a fixed-path mechanism: `a.b.c`, `a[*].b` and
|
|
7
|
+
* `*.b` only. It cannot walk to arbitrary depth, which is why
|
|
8
|
+
* `scrubResponseData` in serialize.ts exists as well — these two are not
|
|
9
|
+
* alternatives, they cover different shapes.
|
|
10
|
+
*
|
|
11
|
+
* This list covers OUR payloads. The service inherited only the last ten
|
|
12
|
+
* entries, all of them axios auth headers, so a request body reaching a log
|
|
13
|
+
* carried its password in the clear.
|
|
14
|
+
*/
|
|
15
|
+
exports.REDACT_PATHS = [
|
|
16
|
+
// Credentials at the top level of a logged object.
|
|
17
|
+
"password",
|
|
18
|
+
"newPassword",
|
|
19
|
+
"currentPassword",
|
|
20
|
+
"confirmPassword",
|
|
21
|
+
"passwordHash",
|
|
22
|
+
"token",
|
|
23
|
+
"accessToken",
|
|
24
|
+
"refreshToken",
|
|
25
|
+
"apiKey",
|
|
26
|
+
"secret",
|
|
27
|
+
"clientSecret",
|
|
28
|
+
// …and one level down, which is where a logged `body` or `payload` puts them.
|
|
29
|
+
"*.password",
|
|
30
|
+
"*.newPassword",
|
|
31
|
+
"*.currentPassword",
|
|
32
|
+
"*.confirmPassword",
|
|
33
|
+
"*.passwordHash",
|
|
34
|
+
"*.token",
|
|
35
|
+
"*.accessToken",
|
|
36
|
+
"*.refreshToken",
|
|
37
|
+
"*.apiKey",
|
|
38
|
+
"*.secret",
|
|
39
|
+
"*.clientSecret",
|
|
40
|
+
// Session material. `set-cookie` needs bracket form — the hyphen is not a
|
|
41
|
+
// valid bare path segment.
|
|
42
|
+
"cookie",
|
|
43
|
+
"*.cookie",
|
|
44
|
+
'headers["set-cookie"]',
|
|
45
|
+
'res.headers["set-cookie"]',
|
|
46
|
+
"headers.cookie",
|
|
47
|
+
"headers.Cookie",
|
|
48
|
+
"req.headers.cookie",
|
|
49
|
+
"req.headers.Cookie",
|
|
50
|
+
// The ten inherited entries — axios request/response auth headers.
|
|
51
|
+
"err.config.auth",
|
|
52
|
+
"err.config.headers.authorization",
|
|
53
|
+
"err.config.headers.Authorization",
|
|
54
|
+
"err.request._header",
|
|
55
|
+
"config.auth",
|
|
56
|
+
"config.headers.authorization",
|
|
57
|
+
"config.headers.Authorization",
|
|
58
|
+
"headers.authorization",
|
|
59
|
+
"headers.Authorization",
|
|
60
|
+
"*.auth.username",
|
|
61
|
+
// The same, on the shape the access log and error handler produce.
|
|
62
|
+
"req.headers.authorization",
|
|
63
|
+
"req.headers.Authorization",
|
|
64
|
+
];
|
|
65
|
+
exports.REDACT = {
|
|
66
|
+
paths: exports.REDACT_PATHS,
|
|
67
|
+
censor: serialize_1.CENSOR,
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":";;;AAAA,2CAAqC;AAErC;;;;;;;;;GASG;AACU,QAAA,YAAY,GAAa;IACpC,mDAAmD;IACnD,UAAU;IACV,aAAa;IACb,iBAAiB;IACjB,iBAAiB;IACjB,cAAc;IACd,OAAO;IACP,aAAa;IACb,cAAc;IACd,QAAQ;IACR,QAAQ;IACR,cAAc;IAEd,8EAA8E;IAC9E,YAAY;IACZ,eAAe;IACf,mBAAmB;IACnB,mBAAmB;IACnB,gBAAgB;IAChB,SAAS;IACT,eAAe;IACf,gBAAgB;IAChB,UAAU;IACV,UAAU;IACV,gBAAgB;IAEhB,0EAA0E;IAC1E,2BAA2B;IAC3B,QAAQ;IACR,UAAU;IACV,uBAAuB;IACvB,2BAA2B;IAC3B,gBAAgB;IAChB,gBAAgB;IAChB,oBAAoB;IACpB,oBAAoB;IAEpB,mEAAmE;IACnE,iBAAiB;IACjB,kCAAkC;IAClC,kCAAkC;IAClC,qBAAqB;IACrB,aAAa;IACb,8BAA8B;IAC9B,8BAA8B;IAC9B,uBAAuB;IACvB,uBAAuB;IACvB,iBAAiB;IAEjB,mEAAmE;IACnE,2BAA2B;IAC3B,2BAA2B;CAC5B,CAAC;AAEW,QAAA,MAAM,GAAG;IACpB,KAAK,EAAE,oBAAY;IACnB,MAAM,EAAE,kBAAM;CACf,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a third party's error body is worth keeping. Enough for the
|
|
3
|
+
* gateway's own message and error code, not enough for a page of JSON.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_DATA_BYTES = 2048;
|
|
6
|
+
export declare const CENSOR = "[redacted]";
|
|
7
|
+
/**
|
|
8
|
+
* Redacts personal and credential keys inside a third party's response body,
|
|
9
|
+
* then caps the whole thing.
|
|
10
|
+
*
|
|
11
|
+
* The cap is applied AFTER redaction rather than instead of it: truncating
|
|
12
|
+
* first would leave whatever fitted in the first 2 KB, which for a Xendit
|
|
13
|
+
* charge failure is the payer block.
|
|
14
|
+
*
|
|
15
|
+
* Exported so a service can scrub a body it logs deliberately, outside an
|
|
16
|
+
* error.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scrubResponseData(data: unknown): unknown;
|
|
19
|
+
/**
|
|
20
|
+
* The `err` / `error` serializer every service uses.
|
|
21
|
+
*
|
|
22
|
+
* The axios branch is unchanged in shape from the copy that was duplicated 21
|
|
23
|
+
* times — the same eight fields, so nothing that reads these logs has to
|
|
24
|
+
* change. What is new is that `data` and `stack` are bounded and scrubbed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function serializeError(err: any): unknown;
|
|
27
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAKnC,eAAO,MAAM,MAAM,eAAe,CAAC;AAkDnC;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAuBxD;AASD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAehD"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CENSOR = exports.MAX_DATA_BYTES = void 0;
|
|
7
|
+
exports.scrubResponseData = scrubResponseData;
|
|
8
|
+
exports.serializeError = serializeError;
|
|
9
|
+
const pino_1 = __importDefault(require("pino"));
|
|
10
|
+
/**
|
|
11
|
+
* How much of a third party's error body is worth keeping. Enough for the
|
|
12
|
+
* gateway's own message and error code, not enough for a page of JSON.
|
|
13
|
+
*/
|
|
14
|
+
exports.MAX_DATA_BYTES = 2048;
|
|
15
|
+
const MAX_STACK_CHARS = 4096;
|
|
16
|
+
const MAX_DEPTH = 6;
|
|
17
|
+
const MAX_ARRAY_ITEMS = 20;
|
|
18
|
+
exports.CENSOR = "[redacted]";
|
|
19
|
+
/**
|
|
20
|
+
* Keys whose VALUES are personal data, matched case-insensitively at any depth
|
|
21
|
+
* inside a third party's response body.
|
|
22
|
+
*
|
|
23
|
+
* This list is not about our own payloads — `redact.paths` covers those. It is
|
|
24
|
+
* about what Xendit, Stripe, Resend, Meta, Apify, Cloudflare and Gitea put in
|
|
25
|
+
* an error body, which we neither control nor review. Before this, 407
|
|
26
|
+
* `logger.error({ err, ... })` call sites fed `err.response.data` into the log
|
|
27
|
+
* verbatim and uncapped, and the payment gateways alone return payer name,
|
|
28
|
+
* email, VA number and card last-4 in a failed-charge body.
|
|
29
|
+
*/
|
|
30
|
+
const PII_KEY = /(^|_)(email|phone|msisdn|whatsapp|address|nik|npwp|dob|birth_?date)($|_)|(^|_)(full|first|last|given|family|customer|payer|holder|account|recipient|sender)_?name($|_)|card|va_?number|account_?(number|holder)|payer|recipient/i;
|
|
31
|
+
/**
|
|
32
|
+
* Keys whose values are credentials. Same matching, same reason: a gateway that
|
|
33
|
+
* echoes the request back in its error body echoes the key with it.
|
|
34
|
+
*/
|
|
35
|
+
const SECRET_KEY = /(^|_)(password|passwd|secret|token|apikey|api_key|authorization|auth|signature|cookie|credential|private_?key)($|_)|token$|secret$/i;
|
|
36
|
+
function scrubValue(value, depth) {
|
|
37
|
+
if (value === null || value === undefined)
|
|
38
|
+
return value;
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
if (depth >= MAX_DEPTH)
|
|
41
|
+
return "[depth]";
|
|
42
|
+
const kept = value.slice(0, MAX_ARRAY_ITEMS).map((v) => scrubValue(v, depth + 1));
|
|
43
|
+
return value.length > MAX_ARRAY_ITEMS
|
|
44
|
+
? [...kept, `[+${value.length - MAX_ARRAY_ITEMS} more]`]
|
|
45
|
+
: kept;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "object") {
|
|
48
|
+
if (depth >= MAX_DEPTH)
|
|
49
|
+
return "[depth]";
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
52
|
+
if (SECRET_KEY.test(key) || PII_KEY.test(key)) {
|
|
53
|
+
out[key] = exports.CENSOR;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out[key] = scrubValue(inner, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Redacts personal and credential keys inside a third party's response body,
|
|
64
|
+
* then caps the whole thing.
|
|
65
|
+
*
|
|
66
|
+
* The cap is applied AFTER redaction rather than instead of it: truncating
|
|
67
|
+
* first would leave whatever fitted in the first 2 KB, which for a Xendit
|
|
68
|
+
* charge failure is the payer block.
|
|
69
|
+
*
|
|
70
|
+
* Exported so a service can scrub a body it logs deliberately, outside an
|
|
71
|
+
* error.
|
|
72
|
+
*/
|
|
73
|
+
function scrubResponseData(data) {
|
|
74
|
+
if (data === null || data === undefined)
|
|
75
|
+
return data;
|
|
76
|
+
if (typeof data === "string") {
|
|
77
|
+
return data.length > exports.MAX_DATA_BYTES
|
|
78
|
+
? `${data.slice(0, exports.MAX_DATA_BYTES)}…[truncated]`
|
|
79
|
+
: data;
|
|
80
|
+
}
|
|
81
|
+
const scrubbed = scrubValue(data, 0);
|
|
82
|
+
let serialized;
|
|
83
|
+
try {
|
|
84
|
+
serialized = JSON.stringify(scrubbed) ?? "";
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Circular, or a BigInt — `JSON.stringify` throws on both, and a logger
|
|
88
|
+
// that throws while logging an error loses the error it was called about.
|
|
89
|
+
return "[unserializable]";
|
|
90
|
+
}
|
|
91
|
+
if (serialized.length <= exports.MAX_DATA_BYTES)
|
|
92
|
+
return scrubbed;
|
|
93
|
+
return `${serialized.slice(0, exports.MAX_DATA_BYTES)}…[truncated ${serialized.length} bytes]`;
|
|
94
|
+
}
|
|
95
|
+
function capStack(stack) {
|
|
96
|
+
if (typeof stack !== "string")
|
|
97
|
+
return stack;
|
|
98
|
+
return stack.length > MAX_STACK_CHARS
|
|
99
|
+
? `${stack.slice(0, MAX_STACK_CHARS)}…[truncated]`
|
|
100
|
+
: stack;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The `err` / `error` serializer every service uses.
|
|
104
|
+
*
|
|
105
|
+
* The axios branch is unchanged in shape from the copy that was duplicated 21
|
|
106
|
+
* times — the same eight fields, so nothing that reads these logs has to
|
|
107
|
+
* change. What is new is that `data` and `stack` are bounded and scrubbed.
|
|
108
|
+
*/
|
|
109
|
+
function serializeError(err) {
|
|
110
|
+
if (err && (err.isAxiosError || err.config)) {
|
|
111
|
+
return {
|
|
112
|
+
name: err.name,
|
|
113
|
+
message: err.message,
|
|
114
|
+
code: err.code,
|
|
115
|
+
status: err.response?.status,
|
|
116
|
+
data: scrubResponseData(err.response?.data),
|
|
117
|
+
method: err.config?.method,
|
|
118
|
+
url: err.config?.url,
|
|
119
|
+
stack: capStack(err.stack),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return pino_1.default.stdSerializers.err(err);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":";;;;;;AAwEA,8CAuBC;AAgBD,wCAeC;AA9HD,gDAAwB;AAExB;;;GAGG;AACU,QAAA,cAAc,GAAG,IAAI,CAAC;AACnC,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,SAAS,GAAG,CAAC,CAAC;AACpB,MAAM,eAAe,GAAG,EAAE,CAAC;AAEd,QAAA,MAAM,GAAG,YAAY,CAAC;AAEnC;;;;;;;;;;GAUG;AACH,MAAM,OAAO,GACX,kOAAkO,CAAC;AAErO;;;GAGG;AACH,MAAM,UAAU,GACd,qIAAqI,CAAC;AAExI,SAAS,UAAU,CAAC,KAAc,EAAE,KAAa;IAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAExD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QAClF,OAAO,KAAK,CAAC,MAAM,GAAG,eAAe;YACnC,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,CAAC,MAAM,GAAG,eAAe,QAAQ,CAAC;YACxD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,KAAK,IAAI,SAAS;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YAC5E,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9C,GAAG,CAAC,GAAG,CAAC,GAAG,cAAM,CAAC;gBAClB,SAAS;YACX,CAAC;YACD,GAAG,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,iBAAiB,CAAC,IAAa;IAC7C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAErD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC,MAAM,GAAG,sBAAc;YACjC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAc,CAAC,cAAc;YAChD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAErC,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;QACxE,0EAA0E;QAC1E,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,IAAI,UAAU,CAAC,MAAM,IAAI,sBAAc;QAAE,OAAO,QAAQ,CAAC;IAEzD,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAc,CAAC,eAAe,UAAU,CAAC,MAAM,SAAS,CAAC;AACzF,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,MAAM,GAAG,eAAe;QACnC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,cAAc;QAClD,CAAC,CAAC,KAAK,CAAC;AACZ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,GAAQ;IACrC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,OAAO;YACL,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM;YAC5B,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC;YAC3C,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM;YAC1B,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG;YACpB,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;SAC3B,CAAC;IACJ,CAAC;IAED,OAAO,cAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
/**
|
|
3
|
+
* Unchanged from the copy that lived in all 21 services, deliberately — the
|
|
4
|
+
* local-dev experience and the `LOG_DIR` rotation behaviour are not what this
|
|
5
|
+
* package is changing.
|
|
6
|
+
*
|
|
7
|
+
* `LOG_DIR` is unset in every k8s ConfigMap on purpose, so nothing here writes
|
|
8
|
+
* to disk in a deployed environment. That is what allows
|
|
9
|
+
* `readOnlyRootFilesystem: true` on every Deployment, and it is why the
|
|
10
|
+
* aggregation story is "pino writes JSON to stdout and an agent tails the
|
|
11
|
+
* node's pod logs" rather than a sidecar over a shared volume.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildTransport(logDir?: string, name?: string, pretty?: boolean): {
|
|
14
|
+
targets: pino.TransportTargetOptions[];
|
|
15
|
+
} | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Pretty everywhere but production — EXCEPT under test.
|
|
18
|
+
*
|
|
19
|
+
* pino-pretty is a `transport`, and a transport is a worker thread
|
|
20
|
+
* (thread-stream). Jest cannot reap it: every suite passes, Jest prints a green
|
|
21
|
+
* summary, and then the process exits 1 because the worker outlived it. It is a
|
|
22
|
+
* race, so it stays invisible on a fast machine and fails on a contended CI
|
|
23
|
+
* runner — the failure reads as "Process completed with exit code 1" under a
|
|
24
|
+
* fully green report.
|
|
25
|
+
*
|
|
26
|
+
* `tests/setup/env.ts` deletes LOG_DIR believing that disables the transport.
|
|
27
|
+
* It does not: `buildTransport` pushes pino-pretty on `pretty` alone,
|
|
28
|
+
* independent of logDir.
|
|
29
|
+
*
|
|
30
|
+
* Fourteen services carried this guard and SEVEN did not — domain, moderation,
|
|
31
|
+
* analytics, admin, support, memory and design were all one contended runner
|
|
32
|
+
* away from the same green-but-exit-1 failure. Having it in one place is half
|
|
33
|
+
* the reason this package exists.
|
|
34
|
+
*/
|
|
35
|
+
export declare function shouldPretty(nodeEnv?: string | undefined): boolean;
|
|
36
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,SAAQ,EACZ,MAAM,UAAQ,GACb;IAAE,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAA;CAAE,GAAG,SAAS,CA2BxD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,OAAO,qBAAuB,GAAG,OAAO,CAEpE"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTransport = buildTransport;
|
|
4
|
+
exports.shouldPretty = shouldPretty;
|
|
5
|
+
/**
|
|
6
|
+
* Unchanged from the copy that lived in all 21 services, deliberately — the
|
|
7
|
+
* local-dev experience and the `LOG_DIR` rotation behaviour are not what this
|
|
8
|
+
* package is changing.
|
|
9
|
+
*
|
|
10
|
+
* `LOG_DIR` is unset in every k8s ConfigMap on purpose, so nothing here writes
|
|
11
|
+
* to disk in a deployed environment. That is what allows
|
|
12
|
+
* `readOnlyRootFilesystem: true` on every Deployment, and it is why the
|
|
13
|
+
* aggregation story is "pino writes JSON to stdout and an agent tails the
|
|
14
|
+
* node's pod logs" rather than a sidecar over a shared volume.
|
|
15
|
+
*/
|
|
16
|
+
function buildTransport(logDir, name = "app", pretty = false) {
|
|
17
|
+
const targets = [];
|
|
18
|
+
if (pretty) {
|
|
19
|
+
targets.push({ target: "pino-pretty", options: { colorize: true } });
|
|
20
|
+
}
|
|
21
|
+
if (logDir) {
|
|
22
|
+
if (!pretty) {
|
|
23
|
+
// destination 1 = stdout fd
|
|
24
|
+
targets.push({ target: "pino/file", options: { destination: 1 } });
|
|
25
|
+
}
|
|
26
|
+
targets.push({
|
|
27
|
+
target: "pino-roll",
|
|
28
|
+
options: {
|
|
29
|
+
file: `${logDir}/${name}`,
|
|
30
|
+
frequency: "daily",
|
|
31
|
+
dateFormat: "yyyy-MM-dd",
|
|
32
|
+
extension: ".log",
|
|
33
|
+
mkdir: true,
|
|
34
|
+
symlink: true,
|
|
35
|
+
limit: { count: 30, removeOtherLogFiles: true },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return targets.length ? { targets } : undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pretty everywhere but production — EXCEPT under test.
|
|
43
|
+
*
|
|
44
|
+
* pino-pretty is a `transport`, and a transport is a worker thread
|
|
45
|
+
* (thread-stream). Jest cannot reap it: every suite passes, Jest prints a green
|
|
46
|
+
* summary, and then the process exits 1 because the worker outlived it. It is a
|
|
47
|
+
* race, so it stays invisible on a fast machine and fails on a contended CI
|
|
48
|
+
* runner — the failure reads as "Process completed with exit code 1" under a
|
|
49
|
+
* fully green report.
|
|
50
|
+
*
|
|
51
|
+
* `tests/setup/env.ts` deletes LOG_DIR believing that disables the transport.
|
|
52
|
+
* It does not: `buildTransport` pushes pino-pretty on `pretty` alone,
|
|
53
|
+
* independent of logDir.
|
|
54
|
+
*
|
|
55
|
+
* Fourteen services carried this guard and SEVEN did not — domain, moderation,
|
|
56
|
+
* analytics, admin, support, memory and design were all one contended runner
|
|
57
|
+
* away from the same green-but-exit-1 failure. Having it in one place is half
|
|
58
|
+
* the reason this package exists.
|
|
59
|
+
*/
|
|
60
|
+
function shouldPretty(nodeEnv = process.env.NODE_ENV) {
|
|
61
|
+
return nodeEnv !== "production" && nodeEnv !== "test";
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":";;AAaA,wCA+BC;AAqBD,oCAEC;AAjED;;;;;;;;;;GAUG;AACH,SAAgB,cAAc,CAC5B,MAAe,EACf,IAAI,GAAG,KAAK,EACZ,MAAM,GAAG,KAAK;IAEd,MAAM,OAAO,GAAkC,EAAE,CAAC;IAElD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,4BAA4B;YAC5B,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,CAAC,IAAI,CAAC;YACX,MAAM,EAAE,WAAW;YACnB,OAAO,EAAE;gBACP,IAAI,EAAE,GAAG,MAAM,IAAI,IAAI,EAAE;gBACzB,SAAS,EAAE,OAAO;gBAClB,UAAU,EAAE,YAAY;gBACxB,SAAS,EAAE,MAAM;gBACjB,KAAK,EAAE,IAAI;gBACX,OAAO,EAAE,IAAI;gBACb,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE;aAChD;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,YAAY,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ;IACzD,OAAO,OAAO,KAAK,YAAY,IAAI,OAAO,KAAK,MAAM,CAAC;AACxD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@easyweb/logging",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared structured logging for Easyweb microservices: one pino configuration, request-scoped context over AsyncLocalStorage, an HTTP access log, and the redaction every service needs before its logs leave the pod",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/index.js",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "npm run clean && tsc",
|
|
20
|
+
"build:watch": "tsc --watch",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"clean": "rimraf dist",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"typecheck:test": "tsc -p tsconfig.test.json --noEmit"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"microservices",
|
|
29
|
+
"express",
|
|
30
|
+
"logging",
|
|
31
|
+
"pino",
|
|
32
|
+
"observability"
|
|
33
|
+
],
|
|
34
|
+
"author": "Easy Web Team",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/Alpine-Solusi/website-builder.git",
|
|
39
|
+
"directory": "common-logging"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"express": "^5.0.0",
|
|
43
|
+
"pino": "^10.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@swc/core": "^1.15.47",
|
|
47
|
+
"@swc/jest": "^0.2.39",
|
|
48
|
+
"@types/express": "^5.0.6",
|
|
49
|
+
"@types/jest": "^30.0.0",
|
|
50
|
+
"@types/node": "^26.0.1",
|
|
51
|
+
"express": "^5.1.0",
|
|
52
|
+
"jest": "^30.4.2",
|
|
53
|
+
"pino": "^10.3.1",
|
|
54
|
+
"rimraf": "^6.1.3",
|
|
55
|
+
"typescript": "^6.0.3"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|