@loggerjs/processors 0.0.2 → 0.3.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 +68 -30
- package/dist/coalesce.d.ts +10 -0
- package/dist/coalesce.d.ts.map +1 -0
- package/dist/index.cjs +123 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -7
- package/dist/index.js.map +1 -1
- package/dist/redact.d.ts +2 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/symbolicate-stack.d.ts +15 -0
- package/dist/symbolicate-stack.d.ts.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
# @loggerjs/processors
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> The composable middleware and processor toolbox for LoggerJS — redact, sample, dedupe, rate-limit, fingerprint, enrich, route, and buffer.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@loggerjs/processors)
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
Synchronous, error-isolated steps that run inside the [LoggerJS](../../README.md) pipeline before delivery. **Middleware** run on the raw `LogRecord` (before id/message/error work); **processors** run on the projected `LogEvent`. Both are sandboxed — a throwing step is reported to logger meta and never corrupts other records or blocks delivery.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @loggerjs/processors
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { createLogger } from "@loggerjs/node";
|
|
20
|
+
import { redactProcessor, sampleProcessor, tagsMiddleware } from "@loggerjs/processors";
|
|
21
|
+
|
|
22
|
+
const logger = createLogger({
|
|
23
|
+
category: ["api"],
|
|
24
|
+
middleware: [tagsMiddleware({ service: "checkout" })],
|
|
25
|
+
processors: [
|
|
26
|
+
redactProcessor({ keys: ["password", "token", /secret/i] }),
|
|
27
|
+
sampleProcessor({ rates: { debug: 0.1, info: 1, warn: 1, error: 1, fatal: 1 } }),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary>Full example — middleware and processors side by side</summary>
|
|
4
34
|
|
|
5
35
|
```ts
|
|
6
36
|
import {
|
|
7
|
-
contextMiddleware,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
breadcrumbBufferProcessor,
|
|
13
|
-
dedupeProcessor,
|
|
14
|
-
dynamicSamplerProcessor,
|
|
15
|
-
enrichProcessor,
|
|
16
|
-
filterProcessor,
|
|
17
|
-
fingerprintProcessor,
|
|
18
|
-
fingersCrossedProcessor,
|
|
19
|
-
levelOverrideProcessor,
|
|
20
|
-
normalizeErrorProcessor,
|
|
21
|
-
privacyGuardProcessor,
|
|
22
|
-
rateLimitProcessor,
|
|
23
|
-
redactProcessor,
|
|
24
|
-
routeProcessor,
|
|
25
|
-
sampleProcessor,
|
|
26
|
-
schemaDevCheckProcessor,
|
|
27
|
-
stackParserProcessor,
|
|
28
|
-
tagsProcessor,
|
|
37
|
+
contextMiddleware, enrichMiddleware, tagsMiddleware, traceContextMiddleware, typeMiddleware,
|
|
38
|
+
breadcrumbBufferProcessor, dedupeProcessor, dynamicSamplerProcessor, enrichProcessor,
|
|
39
|
+
filterProcessor, fingerprintProcessor, fingersCrossedProcessor, levelOverrideProcessor,
|
|
40
|
+
normalizeErrorProcessor, privacyGuardProcessor, rateLimitProcessor, redactProcessor,
|
|
41
|
+
routeProcessor, sampleProcessor, schemaDevCheckProcessor, stackParserProcessor, tagsProcessor,
|
|
29
42
|
} from "@loggerjs/processors";
|
|
30
43
|
|
|
31
44
|
const middleware = [
|
|
@@ -37,7 +50,7 @@ const middleware = [
|
|
|
37
50
|
];
|
|
38
51
|
|
|
39
52
|
const processors = [
|
|
40
|
-
redactProcessor({ keys: ["password", "token", /secret/i] }),
|
|
53
|
+
redactProcessor({ keys: ["password", "token", /secret/i], censor: "[hidden]" }),
|
|
41
54
|
privacyGuardProcessor({ maxStringLength: 8192, allowKeys: ["publicToken"] }),
|
|
42
55
|
schemaDevCheckProcessor({
|
|
43
56
|
validators: { "order.created": (data) => (typeof data === "object" && data ? true : "bad payload") },
|
|
@@ -59,9 +72,34 @@ const processors = [
|
|
|
59
72
|
];
|
|
60
73
|
```
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
</details>
|
|
76
|
+
|
|
77
|
+
## The toolbox
|
|
78
|
+
|
|
79
|
+
| Group | Steps |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| **Redaction & privacy** | `redactProcessor`, `privacyGuardProcessor` |
|
|
82
|
+
| **Sampling & volume** | `sampleProcessor`, `dynamicSamplerProcessor`, `rateLimitProcessor`, `dedupeProcessor`, `coalesceProcessor` |
|
|
83
|
+
| **Enrichment & tagging** | `enrichProcessor` / `enrichMiddleware`, `tagsProcessor` / `tagsMiddleware`, `typeProcessor` / `typeMiddleware`, `contextProcessor` / `contextMiddleware`, `traceContextProcessor` / `traceContextMiddleware` |
|
|
84
|
+
| **Errors** | `normalizeErrorProcessor`, `fingerprintProcessor`, `stackParserProcessor`, `symbolicateStackProcessor` |
|
|
85
|
+
| **Routing & control** | `routeProcessor`, `filterProcessor`, `levelOverrideProcessor` |
|
|
86
|
+
| **Buffering** | `fingersCrossedProcessor`, `breadcrumbBufferProcessor` |
|
|
87
|
+
| **Development** | `schemaDevCheckProcessor` |
|
|
88
|
+
|
|
89
|
+
## Ordering & cost
|
|
90
|
+
|
|
91
|
+
- **Prefer middleware** for metadata and enrichment that can run before event projection — it's the cheapest place to drop or annotate a record.
|
|
92
|
+
- **Use processors** for event-only behavior: routing, schema checks, sampling, buffering, and filtering on the resolved event shape.
|
|
93
|
+
- **Configuring any processor disables the record fast path** for that logger, because every log must then be projected to an event. That is the correct trade when you need event-level behavior — see [PROCESSORS.md](../../docs/PROCESSORS.md) and [PERFORMANCE.md](../../docs/PERFORMANCE.md).
|
|
94
|
+
- `redactProcessor()` supports exact `keys`/`paths`, regex/custom matchers, the Pino-compatible `censor` alias, and `remove: true` for omitting matched object fields. It does not compile user paths with `eval` or `new Function`.
|
|
95
|
+
- `fingersCrossedProcessor` accepts a `flushTo` transport or sink to replay buffered pre-trigger events. `routeProcessor` targets **named** transports, so name the transports you route to. Expensive I/O and serialization belong in transports, not processors.
|
|
96
|
+
|
|
97
|
+
## Documentation
|
|
98
|
+
|
|
99
|
+
- [Processors](../../docs/PROCESSORS.md) — the full reference and ordering guidance
|
|
100
|
+
- [Operations](../../docs/OPERATIONS.md) — privacy defaults and what to redact
|
|
101
|
+
- [Concepts](../../docs/CONCEPTS.md) · [LoggerJS root README](../../README.md)
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
[MIT](../../LICENSE) © JS Kits
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type LogEvent, type Processor } from "@loggerjs/core";
|
|
2
|
+
export interface CoalesceOptions {
|
|
3
|
+
windowMs?: number;
|
|
4
|
+
maxEntries?: number;
|
|
5
|
+
key?: (event: LogEvent) => string;
|
|
6
|
+
field?: string;
|
|
7
|
+
updateMessage?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function coalesceProcessor(options?: CoalesceOptions): Processor;
|
|
10
|
+
//# sourceMappingURL=coalesce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coalesce.d.ts","sourceRoot":"","sources":["../src/coalesce.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,QAAQ,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3F,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,MAAM,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAgCD,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,eAAoB,GAAG,SAAS,CAqC1E"}
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,12 @@ let _loggerjs_core = require("@loggerjs/core");
|
|
|
4
4
|
function matchesKey(key, path, value, matchers) {
|
|
5
5
|
return matchers.some((matcher) => {
|
|
6
6
|
if (typeof matcher === "string") return matcher.toLowerCase() === key.toLowerCase();
|
|
7
|
-
if (matcher instanceof RegExp)
|
|
7
|
+
if (matcher instanceof RegExp) {
|
|
8
|
+
matcher.lastIndex = 0;
|
|
9
|
+
const keyMatches = matcher.test(key);
|
|
10
|
+
matcher.lastIndex = 0;
|
|
11
|
+
return keyMatches || matcher.test(path);
|
|
12
|
+
}
|
|
8
13
|
return matcher(key, path, value);
|
|
9
14
|
});
|
|
10
15
|
}
|
|
@@ -29,8 +34,10 @@ function redactValue(value, options, path = "", depth = 0, seen = /* @__PURE__ *
|
|
|
29
34
|
seen.set(value, out);
|
|
30
35
|
for (const [key, child] of Object.entries(input)) {
|
|
31
36
|
const childPath = path ? `${path}.${key}` : key;
|
|
32
|
-
if (matchesKey(key, childPath, child, options.keys ?? []) || pathMatches(childPath, options.paths ?? []))
|
|
33
|
-
|
|
37
|
+
if (matchesKey(key, childPath, child, options.keys ?? []) || pathMatches(childPath, options.paths ?? [])) {
|
|
38
|
+
if (options.remove) continue;
|
|
39
|
+
out[key] = options.replacement;
|
|
40
|
+
} else out[key] = redactValue(child, options, childPath, depth + 1, seen);
|
|
34
41
|
}
|
|
35
42
|
return out;
|
|
36
43
|
}
|
|
@@ -48,7 +55,8 @@ function redactProcessor(options = {}) {
|
|
|
48
55
|
"api_key"
|
|
49
56
|
],
|
|
50
57
|
paths: options.paths ?? [],
|
|
51
|
-
replacement: options.replacement ?? "[REDACTED]",
|
|
58
|
+
replacement: options.replacement ?? options.censor ?? "[REDACTED]",
|
|
59
|
+
remove: options.remove ?? false,
|
|
52
60
|
maxDepth: options.maxDepth ?? 8
|
|
53
61
|
};
|
|
54
62
|
return (event) => ({
|
|
@@ -127,6 +135,58 @@ function contextProcessor(context) {
|
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
//#endregion
|
|
138
|
+
//#region src/coalesce.ts
|
|
139
|
+
function attachCoalesced(event, field, key, state, updateMessage) {
|
|
140
|
+
if (state.count <= 1) return event;
|
|
141
|
+
const payload = {
|
|
142
|
+
key,
|
|
143
|
+
count: state.count,
|
|
144
|
+
firstSeen: state.firstSeen,
|
|
145
|
+
lastSeen: state.lastSeen
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
...event,
|
|
149
|
+
message: updateMessage ? `${event.message} (x${state.count})` : event.message,
|
|
150
|
+
data: event.data && typeof event.data === "object" && !Array.isArray(event.data) ? {
|
|
151
|
+
...event.data,
|
|
152
|
+
[field]: payload
|
|
153
|
+
} : {
|
|
154
|
+
value: event.data,
|
|
155
|
+
[field]: payload
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function coalesceProcessor(options = {}) {
|
|
160
|
+
const windowMs = options.windowMs ?? 1e3;
|
|
161
|
+
const maxEntries = options.maxEntries ?? 1e3;
|
|
162
|
+
const field = options.field ?? "coalesced";
|
|
163
|
+
const updateMessage = options.updateMessage ?? true;
|
|
164
|
+
const keyFor = options.key ?? ((event) => `${event.levelName}:${event.message}:${event.error?.message ?? ""}`);
|
|
165
|
+
const seen = /* @__PURE__ */ new Map();
|
|
166
|
+
return (event, context) => {
|
|
167
|
+
const now = context.now();
|
|
168
|
+
const key = keyFor(event);
|
|
169
|
+
const previous = seen.get(key);
|
|
170
|
+
if (previous && now - previous.lastSeen < windowMs) {
|
|
171
|
+
previous.count += 1;
|
|
172
|
+
previous.lastSeen = now;
|
|
173
|
+
(0, _loggerjs_core.incrementLoggerMetaCounter)("processor.coalesce.suppressed");
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const next = {
|
|
177
|
+
firstSeen: now,
|
|
178
|
+
lastSeen: now,
|
|
179
|
+
count: 1
|
|
180
|
+
};
|
|
181
|
+
seen.set(key, next);
|
|
182
|
+
if (seen.size > maxEntries) {
|
|
183
|
+
const cutoff = now - windowMs;
|
|
184
|
+
for (const [entryKey, state] of seen) if (state.lastSeen < cutoff || seen.size > maxEntries) seen.delete(entryKey);
|
|
185
|
+
}
|
|
186
|
+
return previous ? attachCoalesced(event, field, key, previous, updateMessage) : event;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
130
190
|
//#region src/dedupe.ts
|
|
131
191
|
function dedupeProcessor(options = {}) {
|
|
132
192
|
const windowMs = options.windowMs ?? 1e3;
|
|
@@ -825,7 +885,7 @@ function normalizeFrames(frames, options) {
|
|
|
825
885
|
}
|
|
826
886
|
return out;
|
|
827
887
|
}
|
|
828
|
-
function writeFrames(event, options, frames) {
|
|
888
|
+
function writeFrames$1(event, options, frames) {
|
|
829
889
|
if (options.target === "context") return {
|
|
830
890
|
...event,
|
|
831
891
|
context: {
|
|
@@ -858,7 +918,60 @@ function stackParserProcessor(options = {}) {
|
|
|
858
918
|
const stack = event.error?.stack;
|
|
859
919
|
if (!stack || normalized.maxFrames === 0) return event;
|
|
860
920
|
const frames = normalizeFrames(normalized.parser(stack), normalized);
|
|
861
|
-
return frames.length > 0 ? writeFrames(event, normalized, frames) : event;
|
|
921
|
+
return frames.length > 0 ? writeFrames$1(event, normalized, frames) : event;
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/symbolicate-stack.ts
|
|
926
|
+
function frameListFromEvent(event, sourceKey) {
|
|
927
|
+
const existing = event.error?.[sourceKey];
|
|
928
|
+
if (Array.isArray(existing)) return existing;
|
|
929
|
+
const stack = event.error?.stack;
|
|
930
|
+
return stack ? parseStack(stack) : [];
|
|
931
|
+
}
|
|
932
|
+
function applySymbolication(frame, event, options) {
|
|
933
|
+
const original = options.symbolicate(frame, event);
|
|
934
|
+
if (!original) return frame;
|
|
935
|
+
if (options.mode === "replace") return {
|
|
936
|
+
...original,
|
|
937
|
+
raw: frame.raw
|
|
938
|
+
};
|
|
939
|
+
return {
|
|
940
|
+
...frame,
|
|
941
|
+
original
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function writeFrames(event, target, key, frames) {
|
|
945
|
+
if (target === "context") return {
|
|
946
|
+
...event,
|
|
947
|
+
context: {
|
|
948
|
+
...event.context,
|
|
949
|
+
[key]: frames
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
return {
|
|
953
|
+
...event,
|
|
954
|
+
error: {
|
|
955
|
+
message: event.error?.message ?? event.message,
|
|
956
|
+
...event.error,
|
|
957
|
+
[key]: frames
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function symbolicateStackProcessor(options) {
|
|
962
|
+
const maxFrames = Math.max(0, Math.floor(options.maxFrames ?? 20));
|
|
963
|
+
const sourceKey = options.sourceKey ?? "frames";
|
|
964
|
+
const target = options.target ?? "error";
|
|
965
|
+
const key = options.key ?? "symbolicatedFrames";
|
|
966
|
+
const mode = options.mode ?? "annotate";
|
|
967
|
+
return (event) => {
|
|
968
|
+
if (maxFrames === 0) return event;
|
|
969
|
+
const frames = frameListFromEvent(event, sourceKey).slice(0, maxFrames);
|
|
970
|
+
if (frames.length === 0) return event;
|
|
971
|
+
return writeFrames(event, target, key, frames.map((frame) => applySymbolication(frame, event, {
|
|
972
|
+
mode,
|
|
973
|
+
symbolicate: options.symbolicate
|
|
974
|
+
})));
|
|
862
975
|
};
|
|
863
976
|
}
|
|
864
977
|
//#endregion
|
|
@@ -1326,6 +1439,8 @@ function breadcrumbBufferProcessor(options = {}) {
|
|
|
1326
1439
|
//#endregion
|
|
1327
1440
|
exports.breadcrumbBuffer = breadcrumbBufferProcessor;
|
|
1328
1441
|
exports.breadcrumbBufferProcessor = breadcrumbBufferProcessor;
|
|
1442
|
+
exports.coalesce = coalesceProcessor;
|
|
1443
|
+
exports.coalesceProcessor = coalesceProcessor;
|
|
1329
1444
|
exports.context = contextProcessor;
|
|
1330
1445
|
exports.contextMiddleware = contextMiddleware;
|
|
1331
1446
|
exports.contextMw = contextMiddleware;
|
|
@@ -1365,6 +1480,8 @@ exports.schemaDevCheck = schemaDevCheckProcessor;
|
|
|
1365
1480
|
exports.schemaDevCheckProcessor = schemaDevCheckProcessor;
|
|
1366
1481
|
exports.stackParser = stackParserProcessor;
|
|
1367
1482
|
exports.stackParserProcessor = stackParserProcessor;
|
|
1483
|
+
exports.symbolicateStack = symbolicateStackProcessor;
|
|
1484
|
+
exports.symbolicateStackProcessor = symbolicateStackProcessor;
|
|
1368
1485
|
exports.tags = tagsProcessor;
|
|
1369
1486
|
exports.tagsMiddleware = tagsMiddleware;
|
|
1370
1487
|
exports.tagsMw = tagsMiddleware;
|