@untangle-code/observability 0.0.0-stage → 0.1.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 +661 -0
- package/NOTICE +90 -0
- package/README.md +114 -2
- package/THIRD_PARTY_LICENSES.md +24 -0
- package/dist/correlation.d.ts +36 -0
- package/dist/correlation.d.ts.map +1 -0
- package/dist/correlation.js +12 -0
- package/dist/correlation.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +100 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +52 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +108 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +62 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +121 -0
- package/dist/metrics.js.map +1 -0
- package/dist/profiling.d.ts +75 -0
- package/dist/profiling.d.ts.map +1 -0
- package/dist/profiling.js +107 -0
- package/dist/profiling.js.map +1 -0
- package/dist/redact.d.ts +29 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +123 -0
- package/dist/redact.js.map +1 -0
- package/package.json +59 -3
- package/src/correlation.ts +44 -0
- package/src/errors.ts +133 -0
- package/src/index.ts +26 -0
- package/src/logger.ts +192 -0
- package/src/metrics.ts +157 -0
- package/src/profiling.ts +171 -0
- package/src/redact.ts +144 -0
package/dist/logger.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { redact } from "./redact.js";
|
|
2
|
+
const LEVEL_ORDER = {
|
|
3
|
+
trace: 10,
|
|
4
|
+
debug: 20,
|
|
5
|
+
info: 30,
|
|
6
|
+
warn: 40,
|
|
7
|
+
error: 50,
|
|
8
|
+
};
|
|
9
|
+
function defaultSink(line) {
|
|
10
|
+
process.stderr.write(`${line}\n`);
|
|
11
|
+
}
|
|
12
|
+
function mergeBindings(base, next) {
|
|
13
|
+
const merged = { ...base, ...next };
|
|
14
|
+
const context = { ...base.context, ...next.context };
|
|
15
|
+
if (Object.keys(context).length > 0) {
|
|
16
|
+
merged.context = context;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
delete merged.context;
|
|
20
|
+
}
|
|
21
|
+
return merged;
|
|
22
|
+
}
|
|
23
|
+
/** Correlation keys promoted from {@link LoggerBindings} into `context` (see module doc). */
|
|
24
|
+
function correlationContext(bindings) {
|
|
25
|
+
const extra = {};
|
|
26
|
+
if (bindings.generationId !== undefined)
|
|
27
|
+
extra["generationId"] = bindings.generationId;
|
|
28
|
+
if (bindings.workspaceId !== undefined)
|
|
29
|
+
extra["workspaceId"] = bindings.workspaceId;
|
|
30
|
+
if (bindings.repoId !== undefined)
|
|
31
|
+
extra["repoId"] = bindings.repoId;
|
|
32
|
+
if (bindings.refId !== undefined)
|
|
33
|
+
extra["refId"] = bindings.refId;
|
|
34
|
+
return extra;
|
|
35
|
+
}
|
|
36
|
+
class JsonLinesLogger {
|
|
37
|
+
clock;
|
|
38
|
+
minLevel;
|
|
39
|
+
sink;
|
|
40
|
+
includeSourceText;
|
|
41
|
+
bindings;
|
|
42
|
+
constructor(clock, minLevel, sink, includeSourceText, bindings) {
|
|
43
|
+
this.clock = clock;
|
|
44
|
+
this.minLevel = minLevel;
|
|
45
|
+
this.sink = sink;
|
|
46
|
+
this.includeSourceText = includeSourceText;
|
|
47
|
+
this.bindings = bindings;
|
|
48
|
+
}
|
|
49
|
+
log(event) {
|
|
50
|
+
if (LEVEL_ORDER[event.level] < LEVEL_ORDER[this.minLevel]) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const component = event.component || this.bindings.component || "untangle";
|
|
54
|
+
const requestId = event.requestId ?? this.bindings.requestId;
|
|
55
|
+
const jobId = event.jobId ?? this.bindings.jobId;
|
|
56
|
+
const context = {
|
|
57
|
+
...correlationContext(this.bindings),
|
|
58
|
+
...this.bindings.context,
|
|
59
|
+
...event.context,
|
|
60
|
+
};
|
|
61
|
+
const line = {
|
|
62
|
+
timestamp: event.timestamp,
|
|
63
|
+
level: event.level,
|
|
64
|
+
component,
|
|
65
|
+
event: event.event,
|
|
66
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
67
|
+
...(jobId !== undefined ? { jobId } : {}),
|
|
68
|
+
...(Object.keys(context).length > 0
|
|
69
|
+
? {
|
|
70
|
+
context: redact(context, { includeSourceText: this.includeSourceText }),
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
};
|
|
74
|
+
this.sink(JSON.stringify(line));
|
|
75
|
+
}
|
|
76
|
+
child(bindings) {
|
|
77
|
+
return new JsonLinesLogger(this.clock, this.minLevel, this.sink, this.includeSourceText, mergeBindings(this.bindings, bindings));
|
|
78
|
+
}
|
|
79
|
+
error(event, context) {
|
|
80
|
+
this.emit("error", event, context);
|
|
81
|
+
}
|
|
82
|
+
warn(event, context) {
|
|
83
|
+
this.emit("warn", event, context);
|
|
84
|
+
}
|
|
85
|
+
info(event, context) {
|
|
86
|
+
this.emit("info", event, context);
|
|
87
|
+
}
|
|
88
|
+
debug(event, context) {
|
|
89
|
+
this.emit("debug", event, context);
|
|
90
|
+
}
|
|
91
|
+
trace(event, context) {
|
|
92
|
+
this.emit("trace", event, context);
|
|
93
|
+
}
|
|
94
|
+
emit(level, event, context) {
|
|
95
|
+
this.log({
|
|
96
|
+
timestamp: this.clock.nowIso(),
|
|
97
|
+
level,
|
|
98
|
+
component: this.bindings.component ?? "untangle",
|
|
99
|
+
event,
|
|
100
|
+
...(context !== undefined ? { context } : {}),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Creates a structured, JSON-lines-on-stderr {@link Logger} (architecture.md §24.2). */
|
|
105
|
+
export function createLogger(options) {
|
|
106
|
+
return new JsonLinesLogger(options.clock, options.level ?? "info", options.sink ?? defaultSink, options.debugIncludeSourceText ?? false, options.bindings ?? {});
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics registry (spec.md §23.3): exactly the internal counters the core
|
|
3
|
+
* engine SHOULD expose. No external exporter — `snapshot()` returns a plain
|
|
4
|
+
* object, which callers may print, log, or serve however they like.
|
|
5
|
+
*
|
|
6
|
+
* spec.md §23.3 calls all eleven names "Zähler" (counters), but three of
|
|
7
|
+
* them are not cumulative counts by nature:
|
|
8
|
+
* - `search_latency_ms` is a distribution of per-search durations, not a
|
|
9
|
+
* single number — modeled as a histogram.
|
|
10
|
+
* - `resolved_import_ratio`/`resolved_call_ratio` are point-in-time ratios
|
|
11
|
+
* (resolved / total at last computation), which a monotonic counter
|
|
12
|
+
* cannot represent without lying about "cumulative" — modeled as gauges
|
|
13
|
+
* the caller sets to the latest computed value.
|
|
14
|
+
*
|
|
15
|
+
* The other eight are genuinely cumulative and are modeled as counters.
|
|
16
|
+
* This is a WP-04-local decision (README "Entscheidungen") — the set of
|
|
17
|
+
* names is exactly spec.md §23.3's eleven, not open for extension by name.
|
|
18
|
+
*/
|
|
19
|
+
/** Cumulative counters (spec.md §23.3). */
|
|
20
|
+
export declare const COUNTER_NAMES: readonly ["files_scanned", "files_changed", "chunks_created", "embeddings_requested", "embedding_cache_hits", "qdrant_upsert_bytes", "parse_failures", "watcher_reconciliations"];
|
|
21
|
+
export type CounterName = (typeof COUNTER_NAMES)[number];
|
|
22
|
+
/** Latency distributions (spec.md §23.3). */
|
|
23
|
+
export declare const HISTOGRAM_NAMES: readonly ["search_latency_ms"];
|
|
24
|
+
export type HistogramName = (typeof HISTOGRAM_NAMES)[number];
|
|
25
|
+
/** Point-in-time ratios (spec.md §23.3). */
|
|
26
|
+
export declare const RATIO_NAMES: readonly ["resolved_import_ratio", "resolved_call_ratio"];
|
|
27
|
+
export type RatioName = (typeof RATIO_NAMES)[number];
|
|
28
|
+
export interface HistogramSnapshot {
|
|
29
|
+
count: number;
|
|
30
|
+
sum: number;
|
|
31
|
+
min: number;
|
|
32
|
+
max: number;
|
|
33
|
+
avg: number;
|
|
34
|
+
p50: number;
|
|
35
|
+
p95: number;
|
|
36
|
+
p99: number;
|
|
37
|
+
}
|
|
38
|
+
export interface MetricsSnapshotV1 {
|
|
39
|
+
counters: Record<CounterName, number>;
|
|
40
|
+
histograms: Record<HistogramName, HistogramSnapshot>;
|
|
41
|
+
ratios: Record<RatioName, number | null>;
|
|
42
|
+
}
|
|
43
|
+
/** Summarizes raw samples into a {@link HistogramSnapshot}. Pure, no state. */
|
|
44
|
+
export declare function summarizeHistogram(samples: readonly number[]): HistogramSnapshot;
|
|
45
|
+
/** In-process registry for exactly the eleven counters of spec.md §23.3. */
|
|
46
|
+
export declare class MetricsRegistry {
|
|
47
|
+
private readonly counters;
|
|
48
|
+
private readonly samples;
|
|
49
|
+
private readonly ratios;
|
|
50
|
+
constructor();
|
|
51
|
+
/** Increments a counter by `delta` (default 1). `delta` must be finite and >= 0. */
|
|
52
|
+
incrementCounter(name: CounterName, delta?: number): void;
|
|
53
|
+
/** Records one observed latency (milliseconds) for a histogram. */
|
|
54
|
+
observeHistogram(name: HistogramName, valueMs: number): void;
|
|
55
|
+
/** Sets the latest value of a ratio gauge. `null` clears it back to "unknown". */
|
|
56
|
+
setRatio(name: RatioName, value: number | null): void;
|
|
57
|
+
/** Point-in-time snapshot as a plain object — no external exporter. */
|
|
58
|
+
snapshot(): MetricsSnapshotV1;
|
|
59
|
+
/** Resets every counter, histogram and ratio back to its initial state. */
|
|
60
|
+
reset(): void;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,2CAA2C;AAC3C,eAAO,MAAM,aAAa,mLAShB,CAAC;AACX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,6CAA6C;AAC7C,eAAO,MAAM,eAAe,gCAAiC,CAAC;AAC9D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,4CAA4C;AAC5C,eAAO,MAAM,WAAW,2DAA4D,CAAC;AACrF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1C;AAaD,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,CAgBhF;AAOD,4EAA4E;AAC5E,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8B;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;;IAQ1D,oFAAoF;IAC7E,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,SAAI,GAAG,IAAI;IAO3D,mEAAmE;IAC5D,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAOnE,kFAAkF;IAC3E,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAO5D,uEAAuE;IAChE,QAAQ,IAAI,iBAAiB;IAQpC,2EAA2E;IACpE,KAAK,IAAI,IAAI;CAWrB"}
|
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics registry (spec.md §23.3): exactly the internal counters the core
|
|
3
|
+
* engine SHOULD expose. No external exporter — `snapshot()` returns a plain
|
|
4
|
+
* object, which callers may print, log, or serve however they like.
|
|
5
|
+
*
|
|
6
|
+
* spec.md §23.3 calls all eleven names "Zähler" (counters), but three of
|
|
7
|
+
* them are not cumulative counts by nature:
|
|
8
|
+
* - `search_latency_ms` is a distribution of per-search durations, not a
|
|
9
|
+
* single number — modeled as a histogram.
|
|
10
|
+
* - `resolved_import_ratio`/`resolved_call_ratio` are point-in-time ratios
|
|
11
|
+
* (resolved / total at last computation), which a monotonic counter
|
|
12
|
+
* cannot represent without lying about "cumulative" — modeled as gauges
|
|
13
|
+
* the caller sets to the latest computed value.
|
|
14
|
+
*
|
|
15
|
+
* The other eight are genuinely cumulative and are modeled as counters.
|
|
16
|
+
* This is a WP-04-local decision (README "Entscheidungen") — the set of
|
|
17
|
+
* names is exactly spec.md §23.3's eleven, not open for extension by name.
|
|
18
|
+
*/
|
|
19
|
+
/** Cumulative counters (spec.md §23.3). */
|
|
20
|
+
export const COUNTER_NAMES = [
|
|
21
|
+
"files_scanned",
|
|
22
|
+
"files_changed",
|
|
23
|
+
"chunks_created",
|
|
24
|
+
"embeddings_requested",
|
|
25
|
+
"embedding_cache_hits",
|
|
26
|
+
"qdrant_upsert_bytes",
|
|
27
|
+
"parse_failures",
|
|
28
|
+
"watcher_reconciliations",
|
|
29
|
+
];
|
|
30
|
+
/** Latency distributions (spec.md §23.3). */
|
|
31
|
+
export const HISTOGRAM_NAMES = ["search_latency_ms"];
|
|
32
|
+
/** Point-in-time ratios (spec.md §23.3). */
|
|
33
|
+
export const RATIO_NAMES = ["resolved_import_ratio", "resolved_call_ratio"];
|
|
34
|
+
const EMPTY_HISTOGRAM = {
|
|
35
|
+
count: 0,
|
|
36
|
+
sum: 0,
|
|
37
|
+
min: 0,
|
|
38
|
+
max: 0,
|
|
39
|
+
avg: 0,
|
|
40
|
+
p50: 0,
|
|
41
|
+
p95: 0,
|
|
42
|
+
p99: 0,
|
|
43
|
+
};
|
|
44
|
+
/** Summarizes raw samples into a {@link HistogramSnapshot}. Pure, no state. */
|
|
45
|
+
export function summarizeHistogram(samples) {
|
|
46
|
+
if (samples.length === 0) {
|
|
47
|
+
return EMPTY_HISTOGRAM;
|
|
48
|
+
}
|
|
49
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
50
|
+
const sum = sorted.reduce((total, value) => total + value, 0);
|
|
51
|
+
return {
|
|
52
|
+
count: sorted.length,
|
|
53
|
+
sum,
|
|
54
|
+
min: sorted[0],
|
|
55
|
+
max: sorted[sorted.length - 1],
|
|
56
|
+
avg: sum / sorted.length,
|
|
57
|
+
p50: percentile(sorted, 0.5),
|
|
58
|
+
p95: percentile(sorted, 0.95),
|
|
59
|
+
p99: percentile(sorted, 0.99),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function percentile(sorted, fraction) {
|
|
63
|
+
const index = Math.min(sorted.length - 1, Math.floor(fraction * sorted.length));
|
|
64
|
+
return sorted[index];
|
|
65
|
+
}
|
|
66
|
+
/** In-process registry for exactly the eleven counters of spec.md §23.3. */
|
|
67
|
+
export class MetricsRegistry {
|
|
68
|
+
counters;
|
|
69
|
+
samples;
|
|
70
|
+
ratios;
|
|
71
|
+
constructor() {
|
|
72
|
+
this.counters = fromNames(COUNTER_NAMES, () => 0);
|
|
73
|
+
this.samples = fromNames(HISTOGRAM_NAMES, () => []);
|
|
74
|
+
this.ratios = fromNames(RATIO_NAMES, () => null);
|
|
75
|
+
}
|
|
76
|
+
/** Increments a counter by `delta` (default 1). `delta` must be finite and >= 0. */
|
|
77
|
+
incrementCounter(name, delta = 1) {
|
|
78
|
+
if (!Number.isFinite(delta) || delta < 0) {
|
|
79
|
+
throw new RangeError(`counter delta must be a finite, non-negative number, got ${delta}`);
|
|
80
|
+
}
|
|
81
|
+
this.counters[name] += delta;
|
|
82
|
+
}
|
|
83
|
+
/** Records one observed latency (milliseconds) for a histogram. */
|
|
84
|
+
observeHistogram(name, valueMs) {
|
|
85
|
+
if (!Number.isFinite(valueMs) || valueMs < 0) {
|
|
86
|
+
throw new RangeError(`histogram value must be a finite, non-negative number, got ${valueMs}`);
|
|
87
|
+
}
|
|
88
|
+
this.samples[name].push(valueMs);
|
|
89
|
+
}
|
|
90
|
+
/** Sets the latest value of a ratio gauge. `null` clears it back to "unknown". */
|
|
91
|
+
setRatio(name, value) {
|
|
92
|
+
if (value !== null && (!Number.isFinite(value) || value < 0 || value > 1)) {
|
|
93
|
+
throw new RangeError(`ratio must be null or within [0, 1], got ${value}`);
|
|
94
|
+
}
|
|
95
|
+
this.ratios[name] = value;
|
|
96
|
+
}
|
|
97
|
+
/** Point-in-time snapshot as a plain object — no external exporter. */
|
|
98
|
+
snapshot() {
|
|
99
|
+
return {
|
|
100
|
+
counters: { ...this.counters },
|
|
101
|
+
histograms: fromNames(HISTOGRAM_NAMES, (name) => summarizeHistogram(this.samples[name])),
|
|
102
|
+
ratios: { ...this.ratios },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Resets every counter, histogram and ratio back to its initial state. */
|
|
106
|
+
reset() {
|
|
107
|
+
for (const name of COUNTER_NAMES) {
|
|
108
|
+
this.counters[name] = 0;
|
|
109
|
+
}
|
|
110
|
+
for (const name of HISTOGRAM_NAMES) {
|
|
111
|
+
this.samples[name] = [];
|
|
112
|
+
}
|
|
113
|
+
for (const name of RATIO_NAMES) {
|
|
114
|
+
this.ratios[name] = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function fromNames(names, init) {
|
|
119
|
+
return Object.fromEntries(names.map((name) => [name, init(name)]));
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profiling collector for `--profile` (architecture.md §24.3): stage
|
|
3
|
+
* timings via the injected `Clock`, file counts, cache hit rates, embedding
|
|
4
|
+
* and Qdrant latency/bytes, a parser/language breakdown, and the process
|
|
5
|
+
* memory high water mark.
|
|
6
|
+
*
|
|
7
|
+
* Stage timing uses `Clock` exclusively (BUILD_PLAN determinism rule).
|
|
8
|
+
* The memory high water mark is the one place this module reads real
|
|
9
|
+
* process state (`process.memoryUsage()`), because that is exactly what
|
|
10
|
+
* architecture.md §24.3 asks for — it is not a timing or randomness
|
|
11
|
+
* decision, just a resource-usage sample.
|
|
12
|
+
*/
|
|
13
|
+
import type { Clock } from "@untangle-code/domain";
|
|
14
|
+
import { type HistogramSnapshot } from "./metrics.js";
|
|
15
|
+
export interface StageTimingV1 {
|
|
16
|
+
stage: string;
|
|
17
|
+
startedAtIso: string;
|
|
18
|
+
endedAtIso: string;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CacheHitRateV1 {
|
|
22
|
+
hits: number;
|
|
23
|
+
misses: number;
|
|
24
|
+
/** `hits / (hits + misses)`, or `null` when there were no accesses at all. */
|
|
25
|
+
rate: number | null;
|
|
26
|
+
}
|
|
27
|
+
export interface ParserBreakdownEntryV1 {
|
|
28
|
+
files: number;
|
|
29
|
+
failures: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ProfileReportV1 {
|
|
32
|
+
stages: StageTimingV1[];
|
|
33
|
+
fileCounts: Record<string, number>;
|
|
34
|
+
cacheHitRates: Record<string, CacheHitRateV1>;
|
|
35
|
+
embeddingLatencyMs: HistogramSnapshot;
|
|
36
|
+
qdrantLatencyMs: HistogramSnapshot;
|
|
37
|
+
qdrantBytes: number;
|
|
38
|
+
parserBreakdown: Record<string, ParserBreakdownEntryV1>;
|
|
39
|
+
memoryHighWaterMarkBytes: number;
|
|
40
|
+
}
|
|
41
|
+
export interface StageHandle {
|
|
42
|
+
/** Ends the stage. Idempotent — a second call is a no-op. */
|
|
43
|
+
end(): void;
|
|
44
|
+
}
|
|
45
|
+
/** Reads the process's current resident set size. Isolated for testability. */
|
|
46
|
+
export declare function sampleMemoryBytes(): number;
|
|
47
|
+
/** Collects one `--profile` run's worth of measurements. */
|
|
48
|
+
export declare class ProfileCollector {
|
|
49
|
+
private readonly clock;
|
|
50
|
+
private readonly readMemoryBytes;
|
|
51
|
+
private readonly stages;
|
|
52
|
+
private readonly fileCounts;
|
|
53
|
+
private readonly cacheCounters;
|
|
54
|
+
private readonly embeddingLatencies;
|
|
55
|
+
private readonly qdrantLatencies;
|
|
56
|
+
private readonly parserBreakdown;
|
|
57
|
+
private qdrantBytesTotal;
|
|
58
|
+
private memoryHighWaterMarkBytes;
|
|
59
|
+
constructor(deps: {
|
|
60
|
+
clock: Clock;
|
|
61
|
+
readMemoryBytes?: () => number;
|
|
62
|
+
});
|
|
63
|
+
/** Starts timing `stage`. Returns a handle whose `end()` records the duration. */
|
|
64
|
+
beginStage(stage: string): StageHandle;
|
|
65
|
+
recordFileCount(category: string, delta?: number): void;
|
|
66
|
+
recordCacheAccess(name: string, hit: boolean): void;
|
|
67
|
+
recordEmbeddingLatency(ms: number): void;
|
|
68
|
+
recordQdrantLatency(ms: number): void;
|
|
69
|
+
addQdrantBytes(bytes: number): void;
|
|
70
|
+
recordParserOutcome(language: string, outcome: "success" | "failure"): void;
|
|
71
|
+
/** Re-samples the memory high water mark on demand (also sampled automatically around stages/latencies). */
|
|
72
|
+
sampleMemory(): void;
|
|
73
|
+
snapshot(): ProfileReportV1;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=profiling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiling.d.ts","sourceRoot":"","sources":["../src/profiling.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,KAAK,iBAAiB,EAAsB,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9C,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,eAAe,EAAE,iBAAiB,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IACxD,wBAAwB,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,6DAA6D;IAC7D,GAAG,IAAI,IAAI,CAAC;CACb;AAOD,+EAA+E;AAC/E,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,4DAA4D;AAC5D,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAe;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqC;IACnE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAgB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA8C;IAC9E,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,wBAAwB,CAAS;gBAEtB,IAAI,EAAE;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE;IAMzE,kFAAkF;IAC3E,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW;IAuBtC,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,IAAI;IAIlD,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI;IAUnD,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKxC,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKrC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAInC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI;IASlF,4GAA4G;IACrG,YAAY,IAAI,IAAI;IAOpB,QAAQ,IAAI,eAAe;CAqBnC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { summarizeHistogram } from "./metrics.js";
|
|
2
|
+
/** Reads the process's current resident set size. Isolated for testability. */
|
|
3
|
+
export function sampleMemoryBytes() {
|
|
4
|
+
return process.memoryUsage().rss;
|
|
5
|
+
}
|
|
6
|
+
/** Collects one `--profile` run's worth of measurements. */
|
|
7
|
+
export class ProfileCollector {
|
|
8
|
+
clock;
|
|
9
|
+
readMemoryBytes;
|
|
10
|
+
stages = [];
|
|
11
|
+
fileCounts = {};
|
|
12
|
+
cacheCounters = {};
|
|
13
|
+
embeddingLatencies = [];
|
|
14
|
+
qdrantLatencies = [];
|
|
15
|
+
parserBreakdown = {};
|
|
16
|
+
qdrantBytesTotal = 0;
|
|
17
|
+
memoryHighWaterMarkBytes;
|
|
18
|
+
constructor(deps) {
|
|
19
|
+
this.clock = deps.clock;
|
|
20
|
+
this.readMemoryBytes = deps.readMemoryBytes ?? sampleMemoryBytes;
|
|
21
|
+
this.memoryHighWaterMarkBytes = this.readMemoryBytes();
|
|
22
|
+
}
|
|
23
|
+
/** Starts timing `stage`. Returns a handle whose `end()` records the duration. */
|
|
24
|
+
beginStage(stage) {
|
|
25
|
+
const startedAtIso = this.clock.nowIso();
|
|
26
|
+
const startedAtMs = this.clock.nowEpochMs();
|
|
27
|
+
this.sampleMemory();
|
|
28
|
+
let ended = false;
|
|
29
|
+
return {
|
|
30
|
+
end: () => {
|
|
31
|
+
if (ended) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
ended = true;
|
|
35
|
+
const endedAtMs = this.clock.nowEpochMs();
|
|
36
|
+
this.stages.push({
|
|
37
|
+
stage,
|
|
38
|
+
startedAtIso,
|
|
39
|
+
endedAtIso: this.clock.nowIso(),
|
|
40
|
+
durationMs: endedAtMs - startedAtMs,
|
|
41
|
+
});
|
|
42
|
+
this.sampleMemory();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
recordFileCount(category, delta = 1) {
|
|
47
|
+
this.fileCounts[category] = (this.fileCounts[category] ?? 0) + delta;
|
|
48
|
+
}
|
|
49
|
+
recordCacheAccess(name, hit) {
|
|
50
|
+
const counters = this.cacheCounters[name] ?? { hits: 0, misses: 0 };
|
|
51
|
+
if (hit) {
|
|
52
|
+
counters.hits += 1;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
counters.misses += 1;
|
|
56
|
+
}
|
|
57
|
+
this.cacheCounters[name] = counters;
|
|
58
|
+
}
|
|
59
|
+
recordEmbeddingLatency(ms) {
|
|
60
|
+
this.embeddingLatencies.push(ms);
|
|
61
|
+
this.sampleMemory();
|
|
62
|
+
}
|
|
63
|
+
recordQdrantLatency(ms) {
|
|
64
|
+
this.qdrantLatencies.push(ms);
|
|
65
|
+
this.sampleMemory();
|
|
66
|
+
}
|
|
67
|
+
addQdrantBytes(bytes) {
|
|
68
|
+
this.qdrantBytesTotal += bytes;
|
|
69
|
+
}
|
|
70
|
+
recordParserOutcome(language, outcome) {
|
|
71
|
+
const entry = this.parserBreakdown[language] ?? { files: 0, failures: 0 };
|
|
72
|
+
entry.files += 1;
|
|
73
|
+
if (outcome === "failure") {
|
|
74
|
+
entry.failures += 1;
|
|
75
|
+
}
|
|
76
|
+
this.parserBreakdown[language] = entry;
|
|
77
|
+
}
|
|
78
|
+
/** Re-samples the memory high water mark on demand (also sampled automatically around stages/latencies). */
|
|
79
|
+
sampleMemory() {
|
|
80
|
+
const current = this.readMemoryBytes();
|
|
81
|
+
if (current > this.memoryHighWaterMarkBytes) {
|
|
82
|
+
this.memoryHighWaterMarkBytes = current;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
snapshot() {
|
|
86
|
+
const cacheHitRates = {};
|
|
87
|
+
for (const [name, counters] of Object.entries(this.cacheCounters)) {
|
|
88
|
+
const total = counters.hits + counters.misses;
|
|
89
|
+
cacheHitRates[name] = {
|
|
90
|
+
hits: counters.hits,
|
|
91
|
+
misses: counters.misses,
|
|
92
|
+
rate: total === 0 ? null : counters.hits / total,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
stages: [...this.stages],
|
|
97
|
+
fileCounts: { ...this.fileCounts },
|
|
98
|
+
cacheHitRates,
|
|
99
|
+
embeddingLatencyMs: summarizeHistogram(this.embeddingLatencies),
|
|
100
|
+
qdrantLatencyMs: summarizeHistogram(this.qdrantLatencies),
|
|
101
|
+
qdrantBytes: this.qdrantBytesTotal,
|
|
102
|
+
parserBreakdown: { ...this.parserBreakdown },
|
|
103
|
+
memoryHighWaterMarkBytes: this.memoryHighWaterMarkBytes,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=profiling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiling.js","sourceRoot":"","sources":["../src/profiling.ts"],"names":[],"mappings":"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"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction (spec.md §24, §23.2): keeps secrets and full source text out of
|
|
3
|
+
* logs, status output and crash reports by default.
|
|
4
|
+
*
|
|
5
|
+
* `redact()` is a pure function — no I/O, no logging decisions of its own.
|
|
6
|
+
* The logger (`logger.ts`) applies it to every event's `context` before
|
|
7
|
+
* writing.
|
|
8
|
+
*/
|
|
9
|
+
export interface RedactOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Opt-in escape hatch for interactive debugging (spec.md §24: only on
|
|
12
|
+
* explicit debug opt-in). When `false` (default), values under a
|
|
13
|
+
* {@link SOURCE_TEXT_KEYS} key are replaced instead of logged verbatim.
|
|
14
|
+
*/
|
|
15
|
+
includeSourceText?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Recursively redacts `value`: keys that look like secrets are masked
|
|
19
|
+
* regardless of their value's shape, keys that carry source text are
|
|
20
|
+
* suppressed unless `includeSourceText` is set, and string values that look
|
|
21
|
+
* like a secret themselves (bearer tokens, JWTs, common vendor key prefixes)
|
|
22
|
+
* are masked even under an innocuous key name.
|
|
23
|
+
*
|
|
24
|
+
* Never throws: unsupported value kinds (bigint, function, symbol) are
|
|
25
|
+
* stringified rather than propagating a serialization error into a logging
|
|
26
|
+
* call.
|
|
27
|
+
*/
|
|
28
|
+
export declare function redact(value: unknown, options?: RedactOptions): unknown;
|
|
29
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAoCH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAE3E"}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction (spec.md §24, §23.2): keeps secrets and full source text out of
|
|
3
|
+
* logs, status output and crash reports by default.
|
|
4
|
+
*
|
|
5
|
+
* `redact()` is a pure function — no I/O, no logging decisions of its own.
|
|
6
|
+
* The logger (`logger.ts`) applies it to every event's `context` before
|
|
7
|
+
* writing.
|
|
8
|
+
*/
|
|
9
|
+
const MASK = "[REDACTED]";
|
|
10
|
+
const SOURCE_TEXT_SUPPRESSED = "[SOURCE_TEXT_SUPPRESSED]";
|
|
11
|
+
/**
|
|
12
|
+
* Key names spec.md §24 calls out by name, plus the common synonyms and
|
|
13
|
+
* variants ("api_key", "apiKey", "auth", …) adapters realistically use.
|
|
14
|
+
* Matched case-insensitively against the whole key.
|
|
15
|
+
*/
|
|
16
|
+
const SECRET_KEY_PATTERN = /(api[-_]?key|access[-_]?key|secret[-_]?key|private[-_]?key|token|password|passwd|pwd|secret|authorization|auth|cookie|credential)/i;
|
|
17
|
+
/**
|
|
18
|
+
* Field names that carry full source text / code (spec.md §23.2: "vollständige
|
|
19
|
+
* Quelltexte standardmäßig redigieren"). Mirrors the content-bearing fields of
|
|
20
|
+
* domain's `CodeChunkPayloadV1`/`CodeChunkDraft` plus generic synonyms, so a
|
|
21
|
+
* logged chunk, diff or search hit does not dump raw code into logs by
|
|
22
|
+
* default. Matched case-insensitively against the whole key.
|
|
23
|
+
*/
|
|
24
|
+
const SOURCE_TEXT_KEYS = new Set([
|
|
25
|
+
"content",
|
|
26
|
+
"text",
|
|
27
|
+
"sourcetext",
|
|
28
|
+
"source",
|
|
29
|
+
"sourcecode",
|
|
30
|
+
"code",
|
|
31
|
+
"snippet",
|
|
32
|
+
"prepareddensetext",
|
|
33
|
+
"bm25text",
|
|
34
|
+
"diff",
|
|
35
|
+
"patch",
|
|
36
|
+
"raw",
|
|
37
|
+
"rawtext",
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Recursively redacts `value`: keys that look like secrets are masked
|
|
41
|
+
* regardless of their value's shape, keys that carry source text are
|
|
42
|
+
* suppressed unless `includeSourceText` is set, and string values that look
|
|
43
|
+
* like a secret themselves (bearer tokens, JWTs, common vendor key prefixes)
|
|
44
|
+
* are masked even under an innocuous key name.
|
|
45
|
+
*
|
|
46
|
+
* Never throws: unsupported value kinds (bigint, function, symbol) are
|
|
47
|
+
* stringified rather than propagating a serialization error into a logging
|
|
48
|
+
* call.
|
|
49
|
+
*/
|
|
50
|
+
export function redact(value, options = {}) {
|
|
51
|
+
return redactValue(value, options, new WeakSet());
|
|
52
|
+
}
|
|
53
|
+
function redactValue(value, options, seen) {
|
|
54
|
+
if (value === null || value === undefined) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
if (seen.has(value)) {
|
|
59
|
+
return "[CIRCULAR]";
|
|
60
|
+
}
|
|
61
|
+
seen.add(value);
|
|
62
|
+
return value.map((entry) => redactValue(entry, options, seen));
|
|
63
|
+
}
|
|
64
|
+
if (value instanceof Date) {
|
|
65
|
+
return value.toISOString();
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === "object") {
|
|
68
|
+
if (seen.has(value)) {
|
|
69
|
+
return "[CIRCULAR]";
|
|
70
|
+
}
|
|
71
|
+
seen.add(value);
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
74
|
+
out[key] = redactEntry(key, entryValue, options, seen);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "bigint") {
|
|
79
|
+
return value.toString();
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
82
|
+
return `[${typeof value}]`;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "string" && looksLikeSecretValue(value)) {
|
|
85
|
+
return MASK;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function redactEntry(key, entryValue, options, seen) {
|
|
90
|
+
const lowerKey = key.toLowerCase();
|
|
91
|
+
if (SECRET_KEY_PATTERN.test(lowerKey)) {
|
|
92
|
+
return MASK;
|
|
93
|
+
}
|
|
94
|
+
if (!options.includeSourceText && SOURCE_TEXT_KEYS.has(lowerKey)) {
|
|
95
|
+
return SOURCE_TEXT_SUPPRESSED;
|
|
96
|
+
}
|
|
97
|
+
return redactValue(entryValue, options, seen);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Heuristic for "a value that looks like a key" (spec.md §24): bearer
|
|
101
|
+
* headers, JWT-shaped triples, and common vendor secret prefixes
|
|
102
|
+
* (`sk-`, `ghp_`, `xox…`). Conservative on purpose — false negatives are
|
|
103
|
+
* expected, false positives on ordinary prose are not.
|
|
104
|
+
*/
|
|
105
|
+
function looksLikeSecretValue(value) {
|
|
106
|
+
if (/^bearer\s+\S+/i.test(value)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (/^[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/.test(value)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (/^(sk|pk|rk)-[A-Za-z0-9_-]{16,}$/i.test(value)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (/^(ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{16,}$/i.test(value)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (/^xox[baprs]-[A-Za-z0-9-]{10,}$/i.test(value)) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=redact.js.map
|