@zudojs/observability 0.1.0 → 1.0.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/LICENSE +21 -0
- package/README.md +262 -13
- package/dist/errors/index.d.ts +5 -0
- package/dist/errors/index.js +5 -0
- package/dist/errors/observabilityError.core.d.ts +32 -0
- package/dist/errors/observabilityError.core.js +56 -0
- package/dist/exporter/exporter.console.d.ts +51 -3
- package/dist/exporter/exporter.console.js +110 -23
- package/dist/exporter/index.d.ts +2 -2
- package/dist/exporter/index.js +2 -2
- package/dist/index.d.ts +17 -11
- package/dist/index.js +20 -12
- package/dist/internal/ids.core.d.ts +26 -0
- package/dist/internal/ids.core.js +51 -0
- package/dist/internal/index.d.ts +5 -0
- package/dist/internal/index.js +5 -0
- package/dist/logLevel/index.d.ts +1 -1
- package/dist/logLevel/index.js +1 -1
- package/dist/logLevel/logLevel.type.d.ts +8 -2
- package/dist/logLevel/logLevel.type.js +27 -20
- package/dist/logRecord/index.d.ts +1 -1
- package/dist/logRecord/index.js +1 -1
- package/dist/logRecord/logRecord.core.d.ts +15 -3
- package/dist/logRecord/logRecord.core.js +48 -26
- package/dist/logger/logger.core.d.ts +16 -8
- package/dist/logger/logger.core.js +54 -23
- package/dist/metrics/counter/counter.core.d.ts +4 -1
- package/dist/metrics/counter/counter.core.js +11 -3
- package/dist/metrics/gauge/gauge.core.d.ts +1 -0
- package/dist/metrics/gauge/gauge.core.js +9 -0
- package/dist/metrics/histogram/histogram.core.d.ts +22 -11
- package/dist/metrics/histogram/histogram.core.js +102 -7
- package/dist/metrics/histogram/index.d.ts +1 -1
- package/dist/metrics/histogram/index.js +1 -1
- package/dist/metrics/index.d.ts +4 -3
- package/dist/metrics/index.js +4 -3
- package/dist/metrics/metrics.reader.d.ts +39 -0
- package/dist/metrics/metrics.reader.js +81 -0
- package/dist/metrics/metrics.registry.d.ts +55 -4
- package/dist/metrics/metrics.registry.js +136 -49
- package/dist/noop/index.d.ts +1 -1
- package/dist/noop/index.js +1 -1
- package/dist/noop/noopObservability.core.d.ts +14 -2
- package/dist/noop/noopObservability.core.js +48 -11
- package/dist/observability/observability.core.d.ts +57 -3
- package/dist/observability/observability.core.js +206 -50
- package/dist/processor/index.d.ts +4 -3
- package/dist/processor/index.js +4 -3
- package/dist/processor/processor.batch.d.ts +66 -11
- package/dist/processor/processor.batch.js +130 -29
- package/dist/processor/processor.log.d.ts +56 -0
- package/dist/processor/processor.log.js +119 -0
- package/dist/propagation/index.d.ts +1 -1
- package/dist/propagation/index.js +1 -1
- package/dist/propagation/propagation.core.d.ts +26 -6
- package/dist/propagation/propagation.core.js +35 -19
- package/dist/redaction/index.d.ts +1 -1
- package/dist/redaction/index.js +1 -1
- package/dist/redaction/redaction.core.d.ts +40 -5
- package/dist/redaction/redaction.core.js +221 -36
- package/dist/sampling/index.d.ts +1 -1
- package/dist/sampling/index.js +1 -1
- package/dist/sampling/sampler.type.d.ts +38 -6
- package/dist/sampling/sampler.type.js +64 -23
- package/dist/tracing/index.d.ts +2 -2
- package/dist/tracing/index.js +2 -2
- package/dist/tracing/span/index.d.ts +1 -1
- package/dist/tracing/span/index.js +1 -1
- package/dist/tracing/span/span.core.d.ts +51 -8
- package/dist/tracing/span/span.core.js +90 -10
- package/dist/tracing/span/spanContext.type.d.ts +12 -3
- package/dist/tracing/span/spanContext.type.js +19 -16
- package/dist/tracing/tracer/index.d.ts +2 -2
- package/dist/tracing/tracer/index.js +2 -2
- package/dist/tracing/tracer/tracer.core.d.ts +44 -14
- package/dist/tracing/tracer/tracer.core.js +81 -25
- package/dist/types/config.types.d.ts +138 -0
- package/dist/types/config.types.js +5 -0
- package/dist/types/logging.types.d.ts +94 -0
- package/dist/types/logging.types.js +20 -0
- package/dist/types/metrics.types.d.ts +93 -0
- package/dist/types/metrics.types.js +5 -0
- package/dist/types/tracing.types.d.ts +115 -0
- package/dist/types/tracing.types.js +28 -0
- package/package.json +27 -14
- package/dist/exporter/exporter.console.d.ts.map +0 -1
- package/dist/exporter/exporter.console.js.map +0 -1
- package/dist/exporter/index.d.ts.map +0 -1
- package/dist/exporter/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logLevel/index.d.ts.map +0 -1
- package/dist/logLevel/index.js.map +0 -1
- package/dist/logLevel/logLevel.type.d.ts.map +0 -1
- package/dist/logLevel/logLevel.type.js.map +0 -1
- package/dist/logRecord/index.d.ts.map +0 -1
- package/dist/logRecord/index.js.map +0 -1
- package/dist/logRecord/logRecord.core.d.ts.map +0 -1
- package/dist/logRecord/logRecord.core.js.map +0 -1
- package/dist/logger/index.d.ts.map +0 -1
- package/dist/logger/index.js.map +0 -1
- package/dist/logger/logger.core.d.ts.map +0 -1
- package/dist/logger/logger.core.js.map +0 -1
- package/dist/metrics/counter/counter.core.d.ts.map +0 -1
- package/dist/metrics/counter/counter.core.js.map +0 -1
- package/dist/metrics/counter/index.d.ts.map +0 -1
- package/dist/metrics/counter/index.js.map +0 -1
- package/dist/metrics/gauge/gauge.core.d.ts.map +0 -1
- package/dist/metrics/gauge/gauge.core.js.map +0 -1
- package/dist/metrics/gauge/index.d.ts.map +0 -1
- package/dist/metrics/gauge/index.js.map +0 -1
- package/dist/metrics/histogram/histogram.core.d.ts.map +0 -1
- package/dist/metrics/histogram/histogram.core.js.map +0 -1
- package/dist/metrics/histogram/index.d.ts.map +0 -1
- package/dist/metrics/histogram/index.js.map +0 -1
- package/dist/metrics/index.d.ts.map +0 -1
- package/dist/metrics/index.js.map +0 -1
- package/dist/metrics/metrics.registry.d.ts.map +0 -1
- package/dist/metrics/metrics.registry.js.map +0 -1
- package/dist/noop/index.d.ts.map +0 -1
- package/dist/noop/index.js.map +0 -1
- package/dist/noop/noopObservability.core.d.ts.map +0 -1
- package/dist/noop/noopObservability.core.js.map +0 -1
- package/dist/observability/index.d.ts.map +0 -1
- package/dist/observability/index.js.map +0 -1
- package/dist/observability/observability.core.d.ts.map +0 -1
- package/dist/observability/observability.core.js.map +0 -1
- package/dist/processor/index.d.ts.map +0 -1
- package/dist/processor/index.js.map +0 -1
- package/dist/processor/processor.batch.d.ts.map +0 -1
- package/dist/processor/processor.batch.js.map +0 -1
- package/dist/propagation/index.d.ts.map +0 -1
- package/dist/propagation/index.js.map +0 -1
- package/dist/propagation/propagation.core.d.ts.map +0 -1
- package/dist/propagation/propagation.core.js.map +0 -1
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/redaction.core.d.ts.map +0 -1
- package/dist/redaction/redaction.core.js.map +0 -1
- package/dist/sampling/index.d.ts.map +0 -1
- package/dist/sampling/index.js.map +0 -1
- package/dist/sampling/sampler.type.d.ts.map +0 -1
- package/dist/sampling/sampler.type.js.map +0 -1
- package/dist/tracing/index.d.ts.map +0 -1
- package/dist/tracing/index.js.map +0 -1
- package/dist/tracing/span/index.d.ts.map +0 -1
- package/dist/tracing/span/index.js.map +0 -1
- package/dist/tracing/span/span.core.d.ts.map +0 -1
- package/dist/tracing/span/span.core.js.map +0 -1
- package/dist/tracing/span/spanContext.type.d.ts.map +0 -1
- package/dist/tracing/span/spanContext.type.js.map +0 -1
- package/dist/tracing/tracer/index.d.ts.map +0 -1
- package/dist/tracing/tracer/index.js.map +0 -1
- package/dist/tracing/tracer/tracer.core.d.ts.map +0 -1
- package/dist/tracing/tracer/tracer.core.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/observability — Log Processor
|
|
3
|
+
*
|
|
4
|
+
* Buffers log records and hands them to an exporter in batches.
|
|
5
|
+
*
|
|
6
|
+
* A logger writes synchronously and cannot await anything, so the naive
|
|
7
|
+
* bridge — `void exporter.export([record])` — exports one record per line and
|
|
8
|
+
* turns any exporter rejection into an unhandled rejection, which ends the
|
|
9
|
+
* process under Node's default. This transport is that bridge done properly:
|
|
10
|
+
* bounded queue, batched export, errors reported, and a `flush()` that
|
|
11
|
+
* shutdown can await so the last records are not lost.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_BATCH_SIZE = 256;
|
|
14
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 1_000;
|
|
15
|
+
const DEFAULT_MAX_QUEUE_SIZE = 4_096;
|
|
16
|
+
/** A {@link LogTransport} that batches records into a {@link LogExporter}. */
|
|
17
|
+
export class BatchLogProcessor {
|
|
18
|
+
name = "batch";
|
|
19
|
+
exporter;
|
|
20
|
+
batchSize;
|
|
21
|
+
flushIntervalMs;
|
|
22
|
+
maxQueueSize;
|
|
23
|
+
onError;
|
|
24
|
+
onDrop;
|
|
25
|
+
buffer = [];
|
|
26
|
+
timer;
|
|
27
|
+
shuttingDown = false;
|
|
28
|
+
inFlight;
|
|
29
|
+
droppedRecords = 0;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.exporter = options.exporter;
|
|
32
|
+
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
33
|
+
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
34
|
+
this.maxQueueSize = Math.max(options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE, this.batchSize);
|
|
35
|
+
this.onError = options.onError;
|
|
36
|
+
this.onDrop = options.onDrop;
|
|
37
|
+
}
|
|
38
|
+
write(record) {
|
|
39
|
+
if (this.shuttingDown)
|
|
40
|
+
return;
|
|
41
|
+
if (this.buffer.length >= this.maxQueueSize) {
|
|
42
|
+
this.droppedRecords++;
|
|
43
|
+
this.onDrop?.(this.droppedRecords);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.buffer.push(record);
|
|
47
|
+
if (this.buffer.length >= this.batchSize) {
|
|
48
|
+
void this.flush();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.startTimer();
|
|
52
|
+
}
|
|
53
|
+
startTimer() {
|
|
54
|
+
if (this.timer !== undefined || this.shuttingDown)
|
|
55
|
+
return;
|
|
56
|
+
this.timer = setInterval(() => {
|
|
57
|
+
if (this.buffer.length === 0) {
|
|
58
|
+
this.stopTimer();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
void this.flush();
|
|
62
|
+
}, this.flushIntervalMs);
|
|
63
|
+
if (typeof this.timer === "object" && "unref" in this.timer) {
|
|
64
|
+
this.timer.unref();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
stopTimer() {
|
|
68
|
+
if (this.timer !== undefined) {
|
|
69
|
+
clearInterval(this.timer);
|
|
70
|
+
this.timer = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Records dropped because the queue was full. */
|
|
74
|
+
getDroppedCount() {
|
|
75
|
+
return this.droppedRecords;
|
|
76
|
+
}
|
|
77
|
+
/** Records currently queued. */
|
|
78
|
+
getQueueSize() {
|
|
79
|
+
return this.buffer.length;
|
|
80
|
+
}
|
|
81
|
+
async flush() {
|
|
82
|
+
if (this.inFlight) {
|
|
83
|
+
await this.inFlight;
|
|
84
|
+
if (this.buffer.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const run = this.drain();
|
|
88
|
+
this.inFlight = run;
|
|
89
|
+
try {
|
|
90
|
+
await run;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
if (this.inFlight === run)
|
|
94
|
+
this.inFlight = undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async drain() {
|
|
98
|
+
while (this.buffer.length > 0) {
|
|
99
|
+
const batch = this.buffer.splice(0, this.batchSize);
|
|
100
|
+
try {
|
|
101
|
+
await this.exporter.export(batch);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.onError?.(error, "LogExporter.export");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async shutdown() {
|
|
109
|
+
this.shuttingDown = true;
|
|
110
|
+
this.stopTimer();
|
|
111
|
+
await this.flush();
|
|
112
|
+
await this.exporter.shutdown();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Creates a batching log transport. */
|
|
116
|
+
export function createBatchLogProcessor(options) {
|
|
117
|
+
return new BatchLogProcessor(options);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=processor.log.js.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Context propagation with AsyncLocalStorage for request-scoped IDs.
|
|
5
5
|
*/
|
|
6
|
-
export { createPropagationContext, derivePropagationContext, getCurrentContext, AsyncPropagationManager, createPropagationManager, } from "./propagation.core.js";
|
|
6
|
+
export { createPropagationContext, derivePropagationContext, getCurrentContext, requireCurrentContext, AsyncPropagationManager, createPropagationManager, } from "./propagation.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Context propagation with AsyncLocalStorage for request-scoped IDs.
|
|
5
5
|
*/
|
|
6
|
-
export { createPropagationContext, derivePropagationContext, getCurrentContext, AsyncPropagationManager, createPropagationManager, } from "./propagation.core.js";
|
|
6
|
+
export { createPropagationContext, derivePropagationContext, getCurrentContext, requireCurrentContext, AsyncPropagationManager, createPropagationManager, } from "./propagation.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -7,17 +7,37 @@
|
|
|
7
7
|
import type { PropagationContext, PropagationContextOptions, PropagationManager } from "../types.js";
|
|
8
8
|
/** Creates a new propagation context. */
|
|
9
9
|
export declare function createPropagationContext(options?: PropagationContextOptions): PropagationContext;
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Derives a child context from a parent.
|
|
12
|
+
*
|
|
13
|
+
* The trace ID, the sampling flags and the baggage all carry over: a child
|
|
14
|
+
* that dropped the flags would break the sampling decision for everything
|
|
15
|
+
* below it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function derivePropagationContext(parent: PropagationContext, overrides?: PropagationContextOptions): PropagationContext;
|
|
18
|
+
/**
|
|
19
|
+
* The active propagation context, or `undefined` when there is none.
|
|
20
|
+
*
|
|
21
|
+
* Returning `undefined` rather than inventing a context keeps "no active
|
|
22
|
+
* trace" distinguishable from a real one — two calls outside a `run()` scope
|
|
23
|
+
* used to hand back two unrelated trace IDs.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getCurrentContext(): PropagationContext | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The active propagation context, creating a fresh one when there is none.
|
|
28
|
+
*
|
|
29
|
+
* Use this only where a context is genuinely required and a new trace is an
|
|
30
|
+
* acceptable answer.
|
|
31
|
+
*/
|
|
32
|
+
export declare function requireCurrentContext(): PropagationContext;
|
|
14
33
|
/**
|
|
15
34
|
* PropagationManager implementation using AsyncLocalStorage.
|
|
16
35
|
*/
|
|
17
36
|
export declare class AsyncPropagationManager implements PropagationManager {
|
|
18
|
-
current(): PropagationContext;
|
|
37
|
+
current(): PropagationContext | undefined;
|
|
19
38
|
run<T>(context: PropagationContext, fn: () => T | Promise<T>): Promise<T>;
|
|
20
|
-
|
|
39
|
+
runSync<T>(context: PropagationContext, fn: () => T): T;
|
|
40
|
+
derive(overrides?: PropagationContextOptions): PropagationContext;
|
|
21
41
|
}
|
|
22
42
|
/** Creates a propagation manager. */
|
|
23
43
|
export declare function createPropagationManager(): AsyncPropagationManager;
|
|
@@ -5,50 +5,61 @@
|
|
|
5
5
|
* trace, span, request, and correlation IDs.
|
|
6
6
|
*/
|
|
7
7
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
8
|
-
import {
|
|
8
|
+
import { generateSpanId, generateTraceId } from "../internal/index.js";
|
|
9
9
|
const storage = new AsyncLocalStorage();
|
|
10
|
-
/** Generates a random hex ID. */
|
|
11
|
-
function generateId(byteLength = 16) {
|
|
12
|
-
const bytes = new Uint8Array(byteLength);
|
|
13
|
-
for (let i = 0; i < byteLength; i++) {
|
|
14
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
15
|
-
}
|
|
16
|
-
return Array.from(bytes)
|
|
17
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
18
|
-
.join("");
|
|
19
|
-
}
|
|
20
10
|
/** Creates a new propagation context. */
|
|
21
11
|
export function createPropagationContext(options) {
|
|
22
|
-
const traceId = options?.traceId ?? generateId(16);
|
|
23
|
-
const spanId = options?.spanId ?? generateId(8);
|
|
24
12
|
return {
|
|
25
|
-
traceId,
|
|
26
|
-
spanId,
|
|
13
|
+
traceId: options?.traceId ?? generateTraceId(),
|
|
14
|
+
spanId: options?.spanId ?? generateSpanId(),
|
|
27
15
|
parentSpanId: options?.parentSpanId,
|
|
28
16
|
requestId: options?.requestId,
|
|
29
17
|
correlationId: options?.correlationId,
|
|
30
18
|
userId: options?.userId,
|
|
31
19
|
service: options?.service,
|
|
20
|
+
traceFlags: options?.traceFlags,
|
|
32
21
|
baggage: options?.baggage
|
|
33
22
|
? Object.freeze({ ...options.baggage })
|
|
34
23
|
: undefined,
|
|
35
24
|
};
|
|
36
25
|
}
|
|
37
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Derives a child context from a parent.
|
|
28
|
+
*
|
|
29
|
+
* The trace ID, the sampling flags and the baggage all carry over: a child
|
|
30
|
+
* that dropped the flags would break the sampling decision for everything
|
|
31
|
+
* below it.
|
|
32
|
+
*/
|
|
38
33
|
export function derivePropagationContext(parent, overrides) {
|
|
39
34
|
return createPropagationContext({
|
|
40
35
|
traceId: overrides?.traceId ?? parent.traceId,
|
|
41
|
-
spanId: overrides?.spanId ??
|
|
36
|
+
spanId: overrides?.spanId ?? generateSpanId(),
|
|
42
37
|
parentSpanId: parent.spanId,
|
|
43
38
|
requestId: overrides?.requestId ?? parent.requestId,
|
|
44
39
|
correlationId: overrides?.correlationId ?? parent.correlationId,
|
|
45
40
|
userId: overrides?.userId ?? parent.userId,
|
|
46
41
|
service: overrides?.service ?? parent.service,
|
|
42
|
+
traceFlags: overrides?.traceFlags ?? parent.traceFlags,
|
|
47
43
|
baggage: overrides?.baggage ?? parent.baggage,
|
|
48
44
|
});
|
|
49
45
|
}
|
|
50
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* The active propagation context, or `undefined` when there is none.
|
|
48
|
+
*
|
|
49
|
+
* Returning `undefined` rather than inventing a context keeps "no active
|
|
50
|
+
* trace" distinguishable from a real one — two calls outside a `run()` scope
|
|
51
|
+
* used to hand back two unrelated trace IDs.
|
|
52
|
+
*/
|
|
51
53
|
export function getCurrentContext() {
|
|
54
|
+
return storage.getStore();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The active propagation context, creating a fresh one when there is none.
|
|
58
|
+
*
|
|
59
|
+
* Use this only where a context is genuinely required and a new trace is an
|
|
60
|
+
* acceptable answer.
|
|
61
|
+
*/
|
|
62
|
+
export function requireCurrentContext() {
|
|
52
63
|
return storage.getStore() ?? createPropagationContext();
|
|
53
64
|
}
|
|
54
65
|
/**
|
|
@@ -61,9 +72,14 @@ export class AsyncPropagationManager {
|
|
|
61
72
|
async run(context, fn) {
|
|
62
73
|
return storage.run(context, fn);
|
|
63
74
|
}
|
|
75
|
+
runSync(context, fn) {
|
|
76
|
+
return storage.run(context, fn);
|
|
77
|
+
}
|
|
64
78
|
derive(overrides) {
|
|
65
79
|
const current = this.current();
|
|
66
|
-
return
|
|
80
|
+
return current
|
|
81
|
+
? derivePropagationContext(current, overrides)
|
|
82
|
+
: createPropagationContext(overrides);
|
|
67
83
|
}
|
|
68
84
|
}
|
|
69
85
|
/** Creates a propagation manager. */
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sensitive field redaction for logs and traces.
|
|
5
5
|
*/
|
|
6
|
-
export { createRedactor, redactObject, isSensitiveField, } from "./redaction.core.js";
|
|
6
|
+
export { createRedactor, createStructureRedactor, redactObject, redactValue, isSensitiveField, DEFAULT_SENSITIVE_FIELDS, CIRCULAR_MARKER, MAX_DEPTH_MARKER, } from "./redaction.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/redaction/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sensitive field redaction for logs and traces.
|
|
5
5
|
*/
|
|
6
|
-
export { createRedactor, redactObject, isSensitiveField, } from "./redaction.core.js";
|
|
6
|
+
export { createRedactor, createStructureRedactor, redactObject, redactValue, isSensitiveField, DEFAULT_SENSITIVE_FIELDS, CIRCULAR_MARKER, MAX_DEPTH_MARKER, } from "./redaction.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,18 +2,53 @@
|
|
|
2
2
|
* @zudojs/observability — Redaction
|
|
3
3
|
*
|
|
4
4
|
* Redacts sensitive fields from log contexts and trace attributes.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* The rules that make this safe rather than decorative:
|
|
7
|
+
* - arrays are traversed, because secrets usually arrive inside one
|
|
8
|
+
* (`headers: [{ authorization: "Bearer …" }]`);
|
|
9
|
+
* - traversal is cycle-aware and depth-capped, because a request object in
|
|
10
|
+
* a log context is a graph, not a tree;
|
|
11
|
+
* - built-ins such as `Error`, `Date`, `Map` and `Set` are left intact
|
|
12
|
+
* instead of collapsing to `{}`, while an instance of a user-defined
|
|
13
|
+
* class is walked like a plain object — its own fields are what an
|
|
14
|
+
* exporter serializes, so a DTO carrying a `password` must not slip
|
|
15
|
+
* past the field list;
|
|
16
|
+
* - matching is substring-based by default, so `userPassword` and
|
|
17
|
+
* `x-api-key` are caught, not just the exact names in the list.
|
|
6
18
|
*/
|
|
7
19
|
import type { RedactionConfig } from "../types.js";
|
|
20
|
+
/** Default sensitive field names, matched case-insensitively. */
|
|
21
|
+
export declare const DEFAULT_SENSITIVE_FIELDS: readonly string[];
|
|
22
|
+
/** Marker used in place of a structure that was too deep or already seen. */
|
|
23
|
+
export declare const CIRCULAR_MARKER = "[CIRCULAR]";
|
|
24
|
+
export declare const MAX_DEPTH_MARKER = "[MAX_DEPTH]";
|
|
8
25
|
/**
|
|
9
|
-
* Creates a redactor that replaces sensitive values
|
|
26
|
+
* Creates a redactor that replaces sensitive values for a single field.
|
|
27
|
+
*
|
|
28
|
+
* This is the leaf-level decision. Use {@link redactObject} to walk a
|
|
29
|
+
* structure — it applies this to every field it reaches.
|
|
10
30
|
*/
|
|
11
31
|
export declare function createRedactor(config?: RedactionConfig): (key: string, value: unknown) => unknown;
|
|
12
32
|
/**
|
|
13
|
-
* Redacts sensitive fields from a
|
|
14
|
-
*
|
|
33
|
+
* Redacts sensitive fields from a structure.
|
|
34
|
+
*
|
|
35
|
+
* Returns a new value; the input is never mutated. Arrays are traversed,
|
|
36
|
+
* cycles become {@link CIRCULAR_MARKER}, and anything deeper than
|
|
37
|
+
* `maxDepth` becomes {@link MAX_DEPTH_MARKER}.
|
|
15
38
|
*/
|
|
16
39
|
export declare function redactObject<T extends Record<string, unknown>>(obj: T, config?: RedactionConfig): T;
|
|
17
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Redacts any value, not just a plain object — an array of headers, a scalar,
|
|
42
|
+
* a nested mix.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactValue(value: unknown, config?: RedactionConfig): unknown;
|
|
45
|
+
/** Checks if a field name is sensitive under the given configuration. */
|
|
18
46
|
export declare function isSensitiveField(fieldName: string, config?: RedactionConfig): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Compiles a configuration once into a reusable structure redactor.
|
|
49
|
+
*
|
|
50
|
+
* Prefer this on a hot path: {@link redactObject} recompiles the field
|
|
51
|
+
* matcher on every call.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createStructureRedactor(config?: RedactionConfig): (value: unknown) => unknown;
|
|
19
54
|
//# sourceMappingURL=redaction.core.d.ts.map
|
|
@@ -2,66 +2,251 @@
|
|
|
2
2
|
* @zudojs/observability — Redaction
|
|
3
3
|
*
|
|
4
4
|
* Redacts sensitive fields from log contexts and trace attributes.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* The rules that make this safe rather than decorative:
|
|
7
|
+
* - arrays are traversed, because secrets usually arrive inside one
|
|
8
|
+
* (`headers: [{ authorization: "Bearer …" }]`);
|
|
9
|
+
* - traversal is cycle-aware and depth-capped, because a request object in
|
|
10
|
+
* a log context is a graph, not a tree;
|
|
11
|
+
* - built-ins such as `Error`, `Date`, `Map` and `Set` are left intact
|
|
12
|
+
* instead of collapsing to `{}`, while an instance of a user-defined
|
|
13
|
+
* class is walked like a plain object — its own fields are what an
|
|
14
|
+
* exporter serializes, so a DTO carrying a `password` must not slip
|
|
15
|
+
* past the field list;
|
|
16
|
+
* - matching is substring-based by default, so `userPassword` and
|
|
17
|
+
* `x-api-key` are caught, not just the exact names in the list.
|
|
6
18
|
*/
|
|
7
|
-
/** Default sensitive field names. */
|
|
8
|
-
const DEFAULT_SENSITIVE_FIELDS = [
|
|
19
|
+
/** Default sensitive field names, matched case-insensitively. */
|
|
20
|
+
export const DEFAULT_SENSITIVE_FIELDS = [
|
|
9
21
|
"password",
|
|
22
|
+
"passwd",
|
|
10
23
|
"secret",
|
|
11
24
|
"token",
|
|
12
25
|
"authorization",
|
|
26
|
+
"auth",
|
|
13
27
|
"cookie",
|
|
28
|
+
"session",
|
|
29
|
+
"credential",
|
|
30
|
+
"api_key",
|
|
31
|
+
"apikey",
|
|
14
32
|
"access_token",
|
|
15
33
|
"refresh_token",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"accessToken",
|
|
19
|
-
"refreshToken",
|
|
34
|
+
"private_key",
|
|
35
|
+
"client_secret",
|
|
20
36
|
"credit_card",
|
|
21
|
-
"
|
|
37
|
+
"creditcard",
|
|
38
|
+
"card_number",
|
|
39
|
+
"cardnumber",
|
|
40
|
+
"cvv",
|
|
22
41
|
"ssn",
|
|
23
42
|
"social_security",
|
|
43
|
+
"pin",
|
|
44
|
+
"otp",
|
|
24
45
|
];
|
|
25
46
|
const DEFAULT_REPLACEMENT = "[REDACTED]";
|
|
47
|
+
const DEFAULT_MAX_DEPTH = 8;
|
|
48
|
+
/** Marker used in place of a structure that was too deep or already seen. */
|
|
49
|
+
export const CIRCULAR_MARKER = "[CIRCULAR]";
|
|
50
|
+
export const MAX_DEPTH_MARKER = "[MAX_DEPTH]";
|
|
26
51
|
/**
|
|
27
|
-
*
|
|
52
|
+
* Splits a field name into its lowercase words.
|
|
53
|
+
*
|
|
54
|
+
* `x-api-key`, `api_key` and `apiKey` all reduce to `["api", "key"]`, and a
|
|
55
|
+
* digit run is its own word so `token2` yields `["token", "2"]`.
|
|
28
56
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
function words(key) {
|
|
58
|
+
return key
|
|
59
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
60
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
61
|
+
.replace(/([a-zA-Z])([0-9])/g, "$1 $2")
|
|
62
|
+
.replace(/([0-9])([a-zA-Z])/g, "$1 $2")
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.split(/[^a-z0-9]+/)
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Every run of consecutive words in `key`, joined.
|
|
69
|
+
*
|
|
70
|
+
* Matching on these rather than on a raw substring is what keeps
|
|
71
|
+
* `shippingAddress` out of the `pin` rule and `authorId` out of the `auth`
|
|
72
|
+
* rule, while still catching `userPassword`, `x-api-key` and `accessToken`.
|
|
73
|
+
* A plain `normalized.includes(field)` test redacts every one of those — a
|
|
74
|
+
* silent, permanent loss of legitimate log data that looks exactly like the
|
|
75
|
+
* field never being logged.
|
|
76
|
+
*/
|
|
77
|
+
function wordJoins(key) {
|
|
78
|
+
const parts = words(key);
|
|
79
|
+
const joins = [];
|
|
80
|
+
for (let start = 0; start < parts.length; start++) {
|
|
81
|
+
let joined = "";
|
|
82
|
+
for (let end = start; end < parts.length; end++) {
|
|
83
|
+
joined += parts[end] ?? "";
|
|
84
|
+
joins.push(joined);
|
|
38
85
|
}
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
}
|
|
87
|
+
return joins;
|
|
88
|
+
}
|
|
89
|
+
function compile(config) {
|
|
90
|
+
const fields = (config?.fields ?? DEFAULT_SENSITIVE_FIELDS).map((field) => field.toLowerCase());
|
|
91
|
+
const patterns = config?.patterns ?? [];
|
|
92
|
+
const matchMode = config?.matchMode ?? "contains";
|
|
93
|
+
const exact = new Set(fields);
|
|
94
|
+
const normalizedFields = new Set(fields.map((field) => field.replace(/[^a-z0-9]/g, "")).filter(Boolean));
|
|
95
|
+
const isSensitive = (key) => {
|
|
96
|
+
const lower = key.toLowerCase();
|
|
97
|
+
if (exact.has(lower))
|
|
98
|
+
return true;
|
|
99
|
+
if (matchMode === "contains") {
|
|
100
|
+
for (const candidate of wordJoins(key)) {
|
|
101
|
+
if (normalizedFields.has(candidate))
|
|
102
|
+
return true;
|
|
103
|
+
// Tolerate a plural: `passwords` is the `password` field.
|
|
104
|
+
if (candidate.endsWith("s") &&
|
|
105
|
+
normalizedFields.has(candidate.slice(0, -1))) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
41
109
|
}
|
|
42
|
-
|
|
110
|
+
for (const pattern of patterns) {
|
|
111
|
+
pattern.lastIndex = 0;
|
|
112
|
+
if (pattern.test(key))
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
isSensitive,
|
|
119
|
+
replacement: config?.replacement ?? DEFAULT_REPLACEMENT,
|
|
120
|
+
maxDepth: config?.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
121
|
+
customRedactor: config?.customRedactor,
|
|
43
122
|
};
|
|
44
123
|
}
|
|
45
124
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
125
|
+
* Creates a redactor that replaces sensitive values for a single field.
|
|
126
|
+
*
|
|
127
|
+
* This is the leaf-level decision. Use {@link redactObject} to walk a
|
|
128
|
+
* structure — it applies this to every field it reaches.
|
|
48
129
|
*/
|
|
49
|
-
export function
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
result
|
|
130
|
+
export function createRedactor(config) {
|
|
131
|
+
const compiled = compile(config);
|
|
132
|
+
return (key, value) => redactField(key, value, compiled);
|
|
133
|
+
}
|
|
134
|
+
function redactField(key, value, compiled) {
|
|
135
|
+
if (compiled.customRedactor) {
|
|
136
|
+
const result = compiled.customRedactor(key, value);
|
|
137
|
+
if (result !== value)
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
if (compiled.isSensitive(key))
|
|
141
|
+
return compiled.replacement;
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
/** True when a value should be walked rather than treated as a leaf. */
|
|
145
|
+
function isPlainContainer(value) {
|
|
146
|
+
if (value === null || typeof value !== "object")
|
|
147
|
+
return false;
|
|
148
|
+
if (Array.isArray(value))
|
|
149
|
+
return false;
|
|
150
|
+
const prototype = Object.getPrototypeOf(value);
|
|
151
|
+
return prototype === Object.prototype || prototype === null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Built-ins whose contents are not key/value fields: they are left intact
|
|
155
|
+
* rather than walked, because rebuilding them field by field would collapse
|
|
156
|
+
* a `Date` or a `Map` to `{}`.
|
|
157
|
+
*/
|
|
158
|
+
function isOpaqueBuiltin(value) {
|
|
159
|
+
return (value instanceof Date ||
|
|
160
|
+
value instanceof RegExp ||
|
|
161
|
+
value instanceof Error ||
|
|
162
|
+
value instanceof Map ||
|
|
163
|
+
value instanceof Set ||
|
|
164
|
+
value instanceof WeakMap ||
|
|
165
|
+
value instanceof WeakSet ||
|
|
166
|
+
value instanceof Promise ||
|
|
167
|
+
value instanceof ArrayBuffer ||
|
|
168
|
+
ArrayBuffer.isView(value) ||
|
|
169
|
+
value instanceof Number ||
|
|
170
|
+
value instanceof String ||
|
|
171
|
+
value instanceof Boolean ||
|
|
172
|
+
(typeof URL !== "undefined" && value instanceof URL) ||
|
|
173
|
+
(typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* True for an instance of a user-defined class (a DTO, a request model).
|
|
177
|
+
*
|
|
178
|
+
* Its own enumerable fields are exactly what `JSON.stringify` — and so every
|
|
179
|
+
* exporter — writes out, so a `password` field on a class instance leaked
|
|
180
|
+
* through a redactor that only walked plain objects. Such an instance is
|
|
181
|
+
* walked like a plain object and rebuilt on the same prototype, so
|
|
182
|
+
* `instanceof` and any `toJSON` it defines survive.
|
|
183
|
+
*/
|
|
184
|
+
function isWalkableInstance(value) {
|
|
185
|
+
if (value === null || typeof value !== "object")
|
|
186
|
+
return false;
|
|
187
|
+
if (Array.isArray(value) || isPlainContainer(value))
|
|
188
|
+
return false;
|
|
189
|
+
if (isOpaqueBuiltin(value))
|
|
190
|
+
return false;
|
|
191
|
+
return Object.keys(value).length > 0;
|
|
192
|
+
}
|
|
193
|
+
function walk(value, compiled, depth, seen) {
|
|
194
|
+
if (depth > compiled.maxDepth)
|
|
195
|
+
return MAX_DEPTH_MARKER;
|
|
196
|
+
if (Array.isArray(value)) {
|
|
197
|
+
if (seen.has(value))
|
|
198
|
+
return CIRCULAR_MARKER;
|
|
199
|
+
seen.add(value);
|
|
200
|
+
const result = value.map((entry) => walk(entry, compiled, depth + 1, seen));
|
|
201
|
+
seen.delete(value);
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
if (isPlainContainer(value) || isWalkableInstance(value)) {
|
|
205
|
+
if (seen.has(value))
|
|
206
|
+
return CIRCULAR_MARKER;
|
|
207
|
+
seen.add(value);
|
|
208
|
+
const result = Object.create(Object.getPrototypeOf(value));
|
|
209
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
210
|
+
const redacted = redactField(key, entry, compiled);
|
|
211
|
+
// A field the matcher replaced is done — never walk into it, or a
|
|
212
|
+
// nested object under a sensitive key would leak through its children.
|
|
213
|
+
result[key] =
|
|
214
|
+
redacted === entry ? walk(entry, compiled, depth + 1, seen) : redacted;
|
|
58
215
|
}
|
|
216
|
+
seen.delete(value);
|
|
217
|
+
return result;
|
|
59
218
|
}
|
|
60
|
-
return
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Redacts sensitive fields from a structure.
|
|
223
|
+
*
|
|
224
|
+
* Returns a new value; the input is never mutated. Arrays are traversed,
|
|
225
|
+
* cycles become {@link CIRCULAR_MARKER}, and anything deeper than
|
|
226
|
+
* `maxDepth` becomes {@link MAX_DEPTH_MARKER}.
|
|
227
|
+
*/
|
|
228
|
+
export function redactObject(obj, config) {
|
|
229
|
+
return walk(obj, compile(config), 0, new WeakSet());
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Redacts any value, not just a plain object — an array of headers, a scalar,
|
|
233
|
+
* a nested mix.
|
|
234
|
+
*/
|
|
235
|
+
export function redactValue(value, config) {
|
|
236
|
+
return walk(value, compile(config), 0, new WeakSet());
|
|
61
237
|
}
|
|
62
|
-
/** Checks if a field name is sensitive. */
|
|
238
|
+
/** Checks if a field name is sensitive under the given configuration. */
|
|
63
239
|
export function isSensitiveField(fieldName, config) {
|
|
64
|
-
|
|
65
|
-
|
|
240
|
+
return compile(config).isSensitive(fieldName);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Compiles a configuration once into a reusable structure redactor.
|
|
244
|
+
*
|
|
245
|
+
* Prefer this on a hot path: {@link redactObject} recompiles the field
|
|
246
|
+
* matcher on every call.
|
|
247
|
+
*/
|
|
248
|
+
export function createStructureRedactor(config) {
|
|
249
|
+
const compiled = compile(config);
|
|
250
|
+
return (value) => walk(value, compiled, 0, new WeakSet());
|
|
66
251
|
}
|
|
67
252
|
//# sourceMappingURL=redaction.core.js.map
|
package/dist/sampling/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sampling strategies: AlwaysOn, AlwaysOff, Probability, ParentBased.
|
|
5
5
|
*/
|
|
6
|
-
export { AlwaysOnSampler, AlwaysOffSampler, ProbabilitySampler, ParentBasedSampler, createAlwaysOnSampler, createAlwaysOffSampler, createProbabilitySampler, createParentBasedSampler, } from "./sampler.type.js";
|
|
6
|
+
export { AlwaysOnSampler, AlwaysOffSampler, ProbabilitySampler, ParentBasedSampler, createAlwaysOnSampler, createAlwaysOffSampler, createProbabilitySampler, createParentBasedSampler, isSampled, isRecording, type ParentBasedSamplerOptions, } from "./sampler.type.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/sampling/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sampling strategies: AlwaysOn, AlwaysOff, Probability, ParentBased.
|
|
5
5
|
*/
|
|
6
|
-
export { AlwaysOnSampler, AlwaysOffSampler, ProbabilitySampler, ParentBasedSampler, createAlwaysOnSampler, createAlwaysOffSampler, createProbabilitySampler, createParentBasedSampler, } from "./sampler.type.js";
|
|
6
|
+
export { AlwaysOnSampler, AlwaysOffSampler, ProbabilitySampler, ParentBasedSampler, createAlwaysOnSampler, createAlwaysOffSampler, createProbabilitySampler, createParentBasedSampler, isSampled, isRecording, } from "./sampler.type.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|