@graphorin/observability 0.5.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/cost/cost-tracker.d.ts +28 -0
- package/dist/cost/cost-tracker.d.ts.map +1 -0
- package/dist/cost/cost-tracker.js +169 -0
- package/dist/cost/cost-tracker.js.map +1 -0
- package/dist/cost/index.d.ts +3 -0
- package/dist/cost/index.js +3 -0
- package/dist/cost/types.d.ts +104 -0
- package/dist/cost/types.d.ts.map +1 -0
- package/dist/eval/index.d.ts +3 -0
- package/dist/eval/index.js +3 -0
- package/dist/eval/runner.d.ts +11 -0
- package/dist/eval/runner.d.ts.map +1 -0
- package/dist/eval/runner.js +113 -0
- package/dist/eval/runner.js.map +1 -0
- package/dist/eval/types.d.ts +108 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/exporters/console.d.ts +27 -0
- package/dist/exporters/console.d.ts.map +1 -0
- package/dist/exporters/console.js +47 -0
- package/dist/exporters/console.js.map +1 -0
- package/dist/exporters/index.d.ts +6 -0
- package/dist/exporters/index.js +7 -0
- package/dist/exporters/jsonl.d.ts +57 -0
- package/dist/exporters/jsonl.d.ts.map +1 -0
- package/dist/exporters/jsonl.js +95 -0
- package/dist/exporters/jsonl.js.map +1 -0
- package/dist/exporters/otlp-http.d.ts +38 -0
- package/dist/exporters/otlp-http.d.ts.map +1 -0
- package/dist/exporters/otlp-http.js +115 -0
- package/dist/exporters/otlp-http.js.map +1 -0
- package/dist/exporters/types.d.ts +72 -0
- package/dist/exporters/types.d.ts.map +1 -0
- package/dist/exporters/types.js +13 -0
- package/dist/exporters/types.js.map +1 -0
- package/dist/exporters/with-validation.d.ts +45 -0
- package/dist/exporters/with-validation.d.ts.map +1 -0
- package/dist/exporters/with-validation.js +99 -0
- package/dist/exporters/with-validation.js.map +1 -0
- package/dist/gen-ai/emit.d.ts +27 -0
- package/dist/gen-ai/emit.d.ts.map +1 -0
- package/dist/gen-ai/emit.js +88 -0
- package/dist/gen-ai/emit.js.map +1 -0
- package/dist/gen-ai/index.d.ts +5 -0
- package/dist/gen-ai/index.js +5 -0
- package/dist/gen-ai/operation-mapping.d.ts +24 -0
- package/dist/gen-ai/operation-mapping.d.ts.map +1 -0
- package/dist/gen-ai/operation-mapping.js +67 -0
- package/dist/gen-ai/operation-mapping.js.map +1 -0
- package/dist/gen-ai/system-derivation.d.ts +41 -0
- package/dist/gen-ai/system-derivation.d.ts.map +1 -0
- package/dist/gen-ai/system-derivation.js +67 -0
- package/dist/gen-ai/system-derivation.js.map +1 -0
- package/dist/gen-ai/types.d.ts +87 -0
- package/dist/gen-ai/types.d.ts.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +3 -0
- package/dist/logger/logger.d.ts +53 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +145 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/openinference/index.d.ts +45 -0
- package/dist/openinference/index.d.ts.map +1 -0
- package/dist/openinference/index.js +92 -0
- package/dist/openinference/index.js.map +1 -0
- package/dist/redaction/config.d.ts +39 -0
- package/dist/redaction/config.d.ts.map +1 -0
- package/dist/redaction/config.js +15 -0
- package/dist/redaction/config.js.map +1 -0
- package/dist/redaction/errors.d.ts +36 -0
- package/dist/redaction/errors.d.ts.map +1 -0
- package/dist/redaction/errors.js +42 -0
- package/dist/redaction/errors.js.map +1 -0
- package/dist/redaction/imperative-patterns.d.ts +108 -0
- package/dist/redaction/imperative-patterns.d.ts.map +1 -0
- package/dist/redaction/imperative-patterns.js +187 -0
- package/dist/redaction/imperative-patterns.js.map +1 -0
- package/dist/redaction/index.d.ts +7 -0
- package/dist/redaction/index.js +7 -0
- package/dist/redaction/patterns.d.ts +85 -0
- package/dist/redaction/patterns.d.ts.map +1 -0
- package/dist/redaction/patterns.js +172 -0
- package/dist/redaction/patterns.js.map +1 -0
- package/dist/redaction/types.d.ts +103 -0
- package/dist/redaction/types.d.ts.map +1 -0
- package/dist/redaction/validator.d.ts +26 -0
- package/dist/redaction/validator.d.ts.map +1 -0
- package/dist/redaction/validator.js +195 -0
- package/dist/redaction/validator.js.map +1 -0
- package/dist/replay/config.d.ts +56 -0
- package/dist/replay/config.d.ts.map +1 -0
- package/dist/replay/config.js +18 -0
- package/dist/replay/config.js.map +1 -0
- package/dist/replay/index.d.ts +5 -0
- package/dist/replay/index.js +5 -0
- package/dist/replay/log.d.ts +39 -0
- package/dist/replay/log.d.ts.map +1 -0
- package/dist/replay/log.js +59 -0
- package/dist/replay/log.js.map +1 -0
- package/dist/replay/replay.d.ts +25 -0
- package/dist/replay/replay.d.ts.map +1 -0
- package/dist/replay/replay.js +173 -0
- package/dist/replay/replay.js.map +1 -0
- package/dist/replay/types.d.ts +110 -0
- package/dist/replay/types.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +63 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +49 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/tracer/ids.d.ts +23 -0
- package/dist/tracer/ids.d.ts.map +1 -0
- package/dist/tracer/ids.js +30 -0
- package/dist/tracer/ids.js.map +1 -0
- package/dist/tracer/index.d.ts +6 -0
- package/dist/tracer/index.js +6 -0
- package/dist/tracer/sampling.d.ts +77 -0
- package/dist/tracer/sampling.d.ts.map +1 -0
- package/dist/tracer/sampling.js +67 -0
- package/dist/tracer/sampling.js.map +1 -0
- package/dist/tracer/span-names.d.ts +11 -0
- package/dist/tracer/span-names.d.ts.map +1 -0
- package/dist/tracer/span-names.js +11 -0
- package/dist/tracer/span-names.js.map +1 -0
- package/dist/tracer/span.d.ts +27 -0
- package/dist/tracer/span.d.ts.map +1 -0
- package/dist/tracer/span.js +80 -0
- package/dist/tracer/span.js.map +1 -0
- package/dist/tracer/tracer.d.ts +101 -0
- package/dist/tracer/tracer.d.ts.map +1 -0
- package/dist/tracer/tracer.js +150 -0
- package/dist/tracer/tracer.js.map +1 -0
- package/package.json +138 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { compareSensitivityTiers, createRedactionValidator } from "../redaction/validator.js";
|
|
2
|
+
import { sanitizeRecord } from "../exporters/with-validation.js";
|
|
3
|
+
|
|
4
|
+
//#region src/replay/replay.ts
|
|
5
|
+
const DEFAULT_VALIDATOR = createRedactionValidator({ minTier: "public" });
|
|
6
|
+
const DEFAULT_ACTOR = {
|
|
7
|
+
kind: "system",
|
|
8
|
+
id: "in-process"
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Build a replay primitive. The returned object exposes a single
|
|
12
|
+
* `run(...)` async iterator that yields {@link ReplayEvent} records.
|
|
13
|
+
*
|
|
14
|
+
* Sanitized mode is the default and applies the configured
|
|
15
|
+
* {@link RedactionValidatorInstance} to every record. Raw mode
|
|
16
|
+
* requires the `canReadRaw` callback to return `true` AND emits an
|
|
17
|
+
* audit log entry on every invocation.
|
|
18
|
+
*
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
function createReplay(opts = {}) {
|
|
22
|
+
const validator = opts.validator ?? DEFAULT_VALIDATOR;
|
|
23
|
+
const audit = opts.audit;
|
|
24
|
+
const defaultActor = opts.defaultActor ?? DEFAULT_ACTOR;
|
|
25
|
+
const canReadRaw = opts.canReadRaw ?? (() => true);
|
|
26
|
+
return { async *run(input) {
|
|
27
|
+
const mode = input.mode ?? "sanitized";
|
|
28
|
+
const minSensitivity = input.minSensitivity ?? "public";
|
|
29
|
+
const actor = input.actor ?? defaultActor;
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
if (mode === "raw" && !canReadRaw({ target: input.target })) {
|
|
32
|
+
emitAudit(audit, {
|
|
33
|
+
action: "trace.replay.accessed",
|
|
34
|
+
actor,
|
|
35
|
+
target: input.target,
|
|
36
|
+
decision: "denied",
|
|
37
|
+
metadata: {
|
|
38
|
+
mode,
|
|
39
|
+
minSensitivity,
|
|
40
|
+
eventCount: 0,
|
|
41
|
+
durationMs: Date.now() - start
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
yield {
|
|
45
|
+
type: "replay.start",
|
|
46
|
+
target: input.target,
|
|
47
|
+
mode
|
|
48
|
+
};
|
|
49
|
+
yield {
|
|
50
|
+
type: "replay.skipped",
|
|
51
|
+
reason: "access-denied",
|
|
52
|
+
spanId: input.target
|
|
53
|
+
};
|
|
54
|
+
yield {
|
|
55
|
+
type: "replay.end",
|
|
56
|
+
durationMs: Date.now() - start,
|
|
57
|
+
eventsEmitted: 0,
|
|
58
|
+
eventsSkipped: 1
|
|
59
|
+
};
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let emitted = 0;
|
|
63
|
+
let skipped = 0;
|
|
64
|
+
let started = false;
|
|
65
|
+
let reachedFrom = input.fromSpanId === void 0;
|
|
66
|
+
const iterator = isAsyncIterable(input.source) ? input.source[Symbol.asyncIterator]() : asAsync(input.source[Symbol.iterator]());
|
|
67
|
+
try {
|
|
68
|
+
while (true) {
|
|
69
|
+
const next = await iterator.next();
|
|
70
|
+
if (next.done === true) break;
|
|
71
|
+
const record = next.value;
|
|
72
|
+
if (!started) {
|
|
73
|
+
started = true;
|
|
74
|
+
yield {
|
|
75
|
+
type: "replay.start",
|
|
76
|
+
target: input.target,
|
|
77
|
+
mode
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!reachedFrom) if (record.id === input.fromSpanId) reachedFrom = true;
|
|
81
|
+
else continue;
|
|
82
|
+
if (mode === "raw") {
|
|
83
|
+
yield {
|
|
84
|
+
type: "replay.event",
|
|
85
|
+
span: record,
|
|
86
|
+
sanitized: false
|
|
87
|
+
};
|
|
88
|
+
emitted += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (skipBySensitivity(record, minSensitivity)) {
|
|
92
|
+
skipped += 1;
|
|
93
|
+
yield {
|
|
94
|
+
type: "replay.skipped",
|
|
95
|
+
reason: "sensitivity",
|
|
96
|
+
spanId: record.id
|
|
97
|
+
};
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const sanitized = sanitizeRecord(record, validator);
|
|
101
|
+
if (sanitized.droppedReason !== void 0) {
|
|
102
|
+
skipped += 1;
|
|
103
|
+
yield {
|
|
104
|
+
type: "replay.skipped",
|
|
105
|
+
reason: "redaction-violation",
|
|
106
|
+
spanId: record.id
|
|
107
|
+
};
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
yield {
|
|
111
|
+
type: "replay.event",
|
|
112
|
+
span: sanitized,
|
|
113
|
+
sanitized: true
|
|
114
|
+
};
|
|
115
|
+
emitted += 1;
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
const durationMs = Date.now() - start;
|
|
119
|
+
if (!started) yield {
|
|
120
|
+
type: "replay.start",
|
|
121
|
+
target: input.target,
|
|
122
|
+
mode
|
|
123
|
+
};
|
|
124
|
+
emitAudit(audit, {
|
|
125
|
+
action: "trace.replay.accessed",
|
|
126
|
+
actor,
|
|
127
|
+
target: input.target,
|
|
128
|
+
decision: "success",
|
|
129
|
+
metadata: {
|
|
130
|
+
mode,
|
|
131
|
+
minSensitivity,
|
|
132
|
+
...input.fromSpanId === void 0 ? {} : { fromSpanId: input.fromSpanId },
|
|
133
|
+
eventCount: emitted,
|
|
134
|
+
durationMs
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
yield {
|
|
138
|
+
type: "replay.end",
|
|
139
|
+
durationMs,
|
|
140
|
+
eventsEmitted: emitted,
|
|
141
|
+
eventsSkipped: skipped
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
} };
|
|
145
|
+
}
|
|
146
|
+
function emitAudit(bridge, event) {
|
|
147
|
+
if (bridge === void 0) return;
|
|
148
|
+
try {
|
|
149
|
+
bridge.emit(event);
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
function skipBySensitivity(record, floor) {
|
|
153
|
+
if (record.sensitivityByAttribute === void 0) return false;
|
|
154
|
+
for (const value of Object.values(record.sensitivityByAttribute)) if (typeof value === "string") {
|
|
155
|
+
const tier = value;
|
|
156
|
+
if (tier === "public" || tier === "internal" || tier === "secret") {
|
|
157
|
+
if (compareSensitivityTiers(tier, floor) > 0) return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
function isAsyncIterable(value) {
|
|
163
|
+
return value !== null && typeof value === "object" && Symbol.asyncIterator in value;
|
|
164
|
+
}
|
|
165
|
+
function asAsync(it) {
|
|
166
|
+
return { next() {
|
|
167
|
+
return Promise.resolve(it.next());
|
|
168
|
+
} };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { createReplay };
|
|
173
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","names":["DEFAULT_ACTOR: ReplayAuditEvent['actor']","validator: RedactionValidatorInstance","audit: ReplayAuditBridge | undefined","minSensitivity: Sensitivity"],"sources":["../../src/replay/replay.ts"],"sourcesContent":["/**\n * `createReplay(...)` — sanitized-by-default replay primitives.\n *\n * Replay never opens the underlying file directly; callers feed it an\n * iterable of `SpanRecord`s. The {@link createTraceLogReader} helper\n * (file-backed JSONL) and `getTraceLog(...)` aggregate utility live\n * alongside this module.\n *\n * @packageDocumentation\n */\n\nimport type { Sensitivity } from '@graphorin/core';\n\nimport type { SpanRecord } from '../exporters/types.js';\nimport { sanitizeRecord } from '../exporters/with-validation.js';\nimport type { RedactionValidatorInstance } from '../redaction/types.js';\nimport { compareSensitivityTiers, createRedactionValidator } from '../redaction/validator.js';\n\nimport type {\n ReplayAuditBridge,\n ReplayAuditEvent,\n ReplayEvent,\n ReplayOptions,\n ReplayRunInput,\n} from './types.js';\n\nconst DEFAULT_VALIDATOR = createRedactionValidator({ minTier: 'public' });\nconst DEFAULT_ACTOR: ReplayAuditEvent['actor'] = { kind: 'system', id: 'in-process' };\n\n/**\n * @stable\n */\nexport interface Replay {\n run(input: ReplayRunInput): AsyncIterable<ReplayEvent>;\n}\n\n/**\n * Build a replay primitive. The returned object exposes a single\n * `run(...)` async iterator that yields {@link ReplayEvent} records.\n *\n * Sanitized mode is the default and applies the configured\n * {@link RedactionValidatorInstance} to every record. Raw mode\n * requires the `canReadRaw` callback to return `true` AND emits an\n * audit log entry on every invocation.\n *\n * @stable\n */\nexport function createReplay(opts: ReplayOptions = {}): Replay {\n const validator: RedactionValidatorInstance = opts.validator ?? DEFAULT_VALIDATOR;\n const audit: ReplayAuditBridge | undefined = opts.audit;\n const defaultActor = opts.defaultActor ?? DEFAULT_ACTOR;\n const canReadRaw = opts.canReadRaw ?? (() => true);\n\n return {\n async *run(input: ReplayRunInput): AsyncIterable<ReplayEvent> {\n const mode = input.mode ?? 'sanitized';\n const minSensitivity: Sensitivity = input.minSensitivity ?? 'public';\n const actor = input.actor ?? defaultActor;\n const start = Date.now();\n\n if (mode === 'raw' && !canReadRaw({ target: input.target })) {\n emitAudit(audit, {\n action: 'trace.replay.accessed',\n actor,\n target: input.target,\n decision: 'denied',\n metadata: { mode, minSensitivity, eventCount: 0, durationMs: Date.now() - start },\n });\n yield { type: 'replay.start', target: input.target, mode };\n yield { type: 'replay.skipped', reason: 'access-denied', spanId: input.target };\n yield {\n type: 'replay.end',\n durationMs: Date.now() - start,\n eventsEmitted: 0,\n eventsSkipped: 1,\n };\n return;\n }\n\n let emitted = 0;\n let skipped = 0;\n let started = false;\n let reachedFrom = input.fromSpanId === undefined;\n\n const iterator = isAsyncIterable(input.source)\n ? input.source[Symbol.asyncIterator]()\n : asAsync(input.source[Symbol.iterator]());\n\n try {\n while (true) {\n const next = await iterator.next();\n if (next.done === true) break;\n const record = next.value;\n\n if (!started) {\n started = true;\n yield { type: 'replay.start', target: input.target, mode };\n }\n\n if (!reachedFrom) {\n if (record.id === input.fromSpanId) reachedFrom = true;\n else continue;\n }\n\n if (mode === 'raw') {\n yield { type: 'replay.event', span: record, sanitized: false };\n emitted += 1;\n continue;\n }\n\n if (skipBySensitivity(record, minSensitivity)) {\n skipped += 1;\n yield { type: 'replay.skipped', reason: 'sensitivity', spanId: record.id };\n continue;\n }\n\n const sanitized = sanitizeRecord(record, validator);\n // RP-18: sanitization is attribute-granular and never drops the whole\n // span. A record arrives pre-flagged with `droppedReason` only if an\n // upstream stage marked it; honour that, otherwise replay the\n // stripped span.\n if (sanitized.droppedReason !== undefined) {\n skipped += 1;\n yield {\n type: 'replay.skipped',\n reason: 'redaction-violation',\n spanId: record.id,\n };\n continue;\n }\n\n yield { type: 'replay.event', span: sanitized, sanitized: true };\n emitted += 1;\n }\n } finally {\n const durationMs = Date.now() - start;\n if (!started) {\n yield { type: 'replay.start', target: input.target, mode };\n }\n emitAudit(audit, {\n action: 'trace.replay.accessed',\n actor,\n target: input.target,\n decision: 'success',\n metadata: {\n mode,\n minSensitivity,\n ...(input.fromSpanId === undefined ? {} : { fromSpanId: input.fromSpanId }),\n eventCount: emitted,\n durationMs,\n },\n });\n yield {\n type: 'replay.end',\n durationMs,\n eventsEmitted: emitted,\n eventsSkipped: skipped,\n };\n }\n },\n };\n}\n\nfunction emitAudit(bridge: ReplayAuditBridge | undefined, event: ReplayAuditEvent): void {\n if (bridge === undefined) return;\n try {\n bridge.emit(event);\n } catch {\n // Audit listener errors must never break the replay pipeline.\n }\n}\n\nfunction skipBySensitivity(record: SpanRecord, floor: Sensitivity): boolean {\n if (record.sensitivityByAttribute === undefined) return false;\n for (const value of Object.values(record.sensitivityByAttribute)) {\n if (typeof value === 'string') {\n const tier = value as Sensitivity;\n if (tier === 'public' || tier === 'internal' || tier === 'secret') {\n if (compareSensitivityTiers(tier, floor) > 0) return true;\n }\n }\n }\n return false;\n}\n\nfunction isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {\n return value !== null && typeof value === 'object' && Symbol.asyncIterator in (value as object);\n}\n\nfunction asAsync<T>(it: Iterator<T>): AsyncIterator<T> {\n return {\n next(): Promise<IteratorResult<T>> {\n return Promise.resolve(it.next());\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { SpanRecord } from "../exporters/types.js";
|
|
2
|
+
import { RedactionValidatorInstance } from "../redaction/types.js";
|
|
3
|
+
import { Sensitivity } from "@graphorin/core";
|
|
4
|
+
|
|
5
|
+
//#region src/replay/types.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Replay scope hint. Server mode requires `'raw'` to be backed by a
|
|
9
|
+
* token carrying the `traces:read:raw` scope. The library mode uses
|
|
10
|
+
* the same flag but without scope enforcement (the server is the only
|
|
11
|
+
* boundary that can grant `'raw'`).
|
|
12
|
+
*
|
|
13
|
+
* @stable
|
|
14
|
+
*/
|
|
15
|
+
type ReplayMode = 'sanitized' | 'raw';
|
|
16
|
+
/**
|
|
17
|
+
* Audit-bridge contract used by the replay layer. Every replay
|
|
18
|
+
* invocation emits one entry through the bridge — sanitized + raw
|
|
19
|
+
* alike. The actual audit storage lives in `@graphorin/security`; the
|
|
20
|
+
* replay layer keeps the bridge generic so the package stays free of
|
|
21
|
+
* a hard dependency on the security package.
|
|
22
|
+
*
|
|
23
|
+
* @stable
|
|
24
|
+
*/
|
|
25
|
+
interface ReplayAuditBridge {
|
|
26
|
+
emit(event: ReplayAuditEvent): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Sanitized event passed to the audit bridge.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
interface ReplayAuditEvent {
|
|
34
|
+
readonly action: 'trace.replay.accessed';
|
|
35
|
+
readonly actor: {
|
|
36
|
+
readonly kind: 'token' | 'cli' | 'agent' | 'system';
|
|
37
|
+
readonly id: string;
|
|
38
|
+
};
|
|
39
|
+
readonly target: string;
|
|
40
|
+
readonly decision: 'success' | 'denied';
|
|
41
|
+
readonly metadata: {
|
|
42
|
+
readonly mode: ReplayMode;
|
|
43
|
+
readonly minSensitivity: Sensitivity;
|
|
44
|
+
readonly fromSpanId?: string;
|
|
45
|
+
readonly eventCount: number;
|
|
46
|
+
readonly durationMs: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Single record yielded by the replay iterator.
|
|
51
|
+
*
|
|
52
|
+
* @stable
|
|
53
|
+
*/
|
|
54
|
+
type ReplayEvent = {
|
|
55
|
+
readonly type: 'replay.start';
|
|
56
|
+
readonly target: string;
|
|
57
|
+
readonly mode: ReplayMode;
|
|
58
|
+
readonly eventCount?: number;
|
|
59
|
+
} | {
|
|
60
|
+
readonly type: 'replay.event';
|
|
61
|
+
readonly span: SpanRecord;
|
|
62
|
+
readonly sanitized: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
readonly type: 'replay.skipped';
|
|
65
|
+
readonly reason: 'sensitivity' | 'redaction-violation' | 'access-denied';
|
|
66
|
+
readonly spanId: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly type: 'replay.end';
|
|
69
|
+
readonly durationMs: number;
|
|
70
|
+
readonly eventsEmitted: number;
|
|
71
|
+
readonly eventsSkipped: number;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Configuration shape for {@link createReplay}.
|
|
75
|
+
*
|
|
76
|
+
* @stable
|
|
77
|
+
*/
|
|
78
|
+
interface ReplayOptions {
|
|
79
|
+
/** Validator used to sanitize records when `mode === 'sanitized'`. */
|
|
80
|
+
readonly validator?: RedactionValidatorInstance;
|
|
81
|
+
/** Optional audit bridge — called once per replay invocation. */
|
|
82
|
+
readonly audit?: ReplayAuditBridge;
|
|
83
|
+
/** Default actor reported via `audit.actor` when none is supplied. */
|
|
84
|
+
readonly defaultActor?: ReplayAuditEvent['actor'];
|
|
85
|
+
/**
|
|
86
|
+
* Scope check invoked when the caller asks for `mode: 'raw'`. Returns
|
|
87
|
+
* `true` to allow, `false` to deny. The server (Phase 14) wires this
|
|
88
|
+
* to the `traces:read:raw` token scope; in library mode it defaults
|
|
89
|
+
* to `() => true` (operators trust their own process).
|
|
90
|
+
*/
|
|
91
|
+
readonly canReadRaw?: (context: {
|
|
92
|
+
readonly target: string;
|
|
93
|
+
}) => boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Per-call options consumed by `Replay.run(...)`.
|
|
97
|
+
*
|
|
98
|
+
* @stable
|
|
99
|
+
*/
|
|
100
|
+
interface ReplayRunInput {
|
|
101
|
+
readonly source: AsyncIterable<SpanRecord> | Iterable<SpanRecord>;
|
|
102
|
+
readonly target: string;
|
|
103
|
+
readonly mode?: ReplayMode;
|
|
104
|
+
readonly minSensitivity?: Sensitivity;
|
|
105
|
+
readonly fromSpanId?: string;
|
|
106
|
+
readonly actor?: ReplayAuditEvent['actor'];
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { ReplayAuditBridge, ReplayAuditEvent, ReplayEvent, ReplayMode, ReplayOptions, ReplayRunInput };
|
|
110
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/replay/types.ts"],"sourcesContent":[],"mappings":";;;;;;AA0DA;AAyBA;;;;;AAqBA;;AACmB,KAtFP,UAAA,GAsFO,WAAA,GAAA,KAAA;;;;;;;;;;UA3EF,iBAAA;cACH;;;;;;;UAQG,gBAAA;;;;;;;;;mBAME;6BACU;;;;;;;;;;;KAYjB,WAAA;;;iBAIS;;;;iBAG+B;;;;;;;;;;;;;;;;;UAkBnC,aAAA;;uBAEM;;mBAEJ;;0BAEO;;;;;;;;;;;;;;;;UAeT,cAAA;mBACE,cAAc,cAAc,SAAS;;kBAEtC;4BACU;;mBAET"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/telemetry/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Zero-default telemetry stub.
|
|
4
|
+
*
|
|
5
|
+
* The framework promises to make zero outbound network calls without
|
|
6
|
+
* an explicit user action. This module exists so consumers can:
|
|
7
|
+
*
|
|
8
|
+
* - inspect the telemetry posture from CLI / health endpoints,
|
|
9
|
+
* - register the reserved `GRAPHORIN_TELEMETRY` / `GRAPHORIN_NO_PHONE_HOME`
|
|
10
|
+
* environment variables for forward compatibility,
|
|
11
|
+
* - assert in tests that no implicit telemetry surface exists.
|
|
12
|
+
*
|
|
13
|
+
* Attempting to enable telemetry pre-v0.2 returns a sentinel result; it
|
|
14
|
+
* never opens a socket.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Result returned by {@link getTelemetryStatus}.
|
|
20
|
+
*
|
|
21
|
+
* @stable
|
|
22
|
+
*/
|
|
23
|
+
interface TelemetryStatus {
|
|
24
|
+
/** Always `false` in v0.1. Reserved field. */
|
|
25
|
+
readonly enabled: false;
|
|
26
|
+
/** Plain-English explanation of the current state. */
|
|
27
|
+
readonly reason: string;
|
|
28
|
+
/** Resolved value of `GRAPHORIN_TELEMETRY` (if any). */
|
|
29
|
+
readonly env?: string;
|
|
30
|
+
/** Resolved value of `GRAPHORIN_NO_PHONE_HOME` (if any). */
|
|
31
|
+
readonly noPhoneHome?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Snapshot of the telemetry posture. Reads from `process.env` once
|
|
35
|
+
* unless `env` is provided.
|
|
36
|
+
*
|
|
37
|
+
* @stable
|
|
38
|
+
*/
|
|
39
|
+
declare function getTelemetryStatus(env?: NodeJS.ProcessEnv): TelemetryStatus;
|
|
40
|
+
/**
|
|
41
|
+
* Best-effort enable hook. Always returns the sentinel
|
|
42
|
+
* `{ status: 'disabled', reason: ... }` payload. Reserved for v0.2+.
|
|
43
|
+
*
|
|
44
|
+
* @stable
|
|
45
|
+
*/
|
|
46
|
+
declare function enableTelemetry(): {
|
|
47
|
+
readonly status: 'disabled';
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Detect the reserved env vars and emit one informational line per
|
|
52
|
+
* process. Returns the lines as an array so callers can route them to
|
|
53
|
+
* any sink they like (defaults to `console.info`).
|
|
54
|
+
*
|
|
55
|
+
* @stable
|
|
56
|
+
*/
|
|
57
|
+
declare function announceTelemetryPosture(opts?: {
|
|
58
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
59
|
+
readonly sink?: (line: string) => void;
|
|
60
|
+
}): ReadonlyArray<string>;
|
|
61
|
+
//#endregion
|
|
62
|
+
export { TelemetryStatus, announceTelemetryPosture, enableTelemetry, getTelemetryStatus };
|
|
63
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/telemetry/index.ts"],"sourcesContent":[],"mappings":";;AAsBA;AAiBA;AAiBA;AAiBA;;;;;;;;;;;;;;;;;UAnDiB,eAAA;;;;;;;;;;;;;;;;iBAiBD,kBAAA,OAAwB,MAAA,CAAO,aAA2B;;;;;;;iBAiB1D,eAAA,CAAA;;;;;;;;;;;iBAiBA,wBAAA;iBACS,MAAA,CAAO;;IAC7B"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/telemetry/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot of the telemetry posture. Reads from `process.env` once
|
|
4
|
+
* unless `env` is provided.
|
|
5
|
+
*
|
|
6
|
+
* @stable
|
|
7
|
+
*/
|
|
8
|
+
function getTelemetryStatus(env = process.env) {
|
|
9
|
+
const telemetry = env.GRAPHORIN_TELEMETRY;
|
|
10
|
+
const noPhoneHome = env.GRAPHORIN_NO_PHONE_HOME;
|
|
11
|
+
return {
|
|
12
|
+
enabled: false,
|
|
13
|
+
reason: "telemetry not yet implemented (v0.1 ships zero default telemetry)",
|
|
14
|
+
...telemetry === void 0 ? {} : { env: telemetry },
|
|
15
|
+
...noPhoneHome === void 0 ? {} : { noPhoneHome }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Best-effort enable hook. Always returns the sentinel
|
|
20
|
+
* `{ status: 'disabled', reason: ... }` payload. Reserved for v0.2+.
|
|
21
|
+
*
|
|
22
|
+
* @stable
|
|
23
|
+
*/
|
|
24
|
+
function enableTelemetry() {
|
|
25
|
+
return {
|
|
26
|
+
status: "disabled",
|
|
27
|
+
reason: "telemetry not yet implemented (v0.1 ships zero default telemetry)"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detect the reserved env vars and emit one informational line per
|
|
32
|
+
* process. Returns the lines as an array so callers can route them to
|
|
33
|
+
* any sink they like (defaults to `console.info`).
|
|
34
|
+
*
|
|
35
|
+
* @stable
|
|
36
|
+
*/
|
|
37
|
+
function announceTelemetryPosture(opts = {}) {
|
|
38
|
+
const env = opts.env ?? process.env;
|
|
39
|
+
const sink = opts.sink ?? ((line) => console.info(line));
|
|
40
|
+
const lines = [];
|
|
41
|
+
if (env.GRAPHORIN_TELEMETRY !== void 0) lines.push(`[graphorin/observability] info: GRAPHORIN_TELEMETRY=${env.GRAPHORIN_TELEMETRY} — acknowledged. Telemetry is hardcoded \`disabled\` in v0.1.`);
|
|
42
|
+
if (env.GRAPHORIN_NO_PHONE_HOME !== void 0) lines.push(`[graphorin/observability] info: GRAPHORIN_NO_PHONE_HOME=${env.GRAPHORIN_NO_PHONE_HOME} — acknowledged. Graphorin already makes zero outbound calls without explicit user action.`);
|
|
43
|
+
for (const line of lines) sink(line);
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { announceTelemetryPosture, enableTelemetry, getTelemetryStatus };
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["lines: string[]"],"sources":["../../src/telemetry/index.ts"],"sourcesContent":["/**\n * Zero-default telemetry stub.\n *\n * The framework promises to make zero outbound network calls without\n * an explicit user action. This module exists so consumers can:\n *\n * - inspect the telemetry posture from CLI / health endpoints,\n * - register the reserved `GRAPHORIN_TELEMETRY` / `GRAPHORIN_NO_PHONE_HOME`\n * environment variables for forward compatibility,\n * - assert in tests that no implicit telemetry surface exists.\n *\n * Attempting to enable telemetry pre-v0.2 returns a sentinel result; it\n * never opens a socket.\n *\n * @packageDocumentation\n */\n\n/**\n * Result returned by {@link getTelemetryStatus}.\n *\n * @stable\n */\nexport interface TelemetryStatus {\n /** Always `false` in v0.1. Reserved field. */\n readonly enabled: false;\n /** Plain-English explanation of the current state. */\n readonly reason: string;\n /** Resolved value of `GRAPHORIN_TELEMETRY` (if any). */\n readonly env?: string;\n /** Resolved value of `GRAPHORIN_NO_PHONE_HOME` (if any). */\n readonly noPhoneHome?: string;\n}\n\n/**\n * Snapshot of the telemetry posture. Reads from `process.env` once\n * unless `env` is provided.\n *\n * @stable\n */\nexport function getTelemetryStatus(env: NodeJS.ProcessEnv = process.env): TelemetryStatus {\n const telemetry = env.GRAPHORIN_TELEMETRY;\n const noPhoneHome = env.GRAPHORIN_NO_PHONE_HOME;\n return {\n enabled: false,\n reason: 'telemetry not yet implemented (v0.1 ships zero default telemetry)',\n ...(telemetry === undefined ? {} : { env: telemetry }),\n ...(noPhoneHome === undefined ? {} : { noPhoneHome }),\n };\n}\n\n/**\n * Best-effort enable hook. Always returns the sentinel\n * `{ status: 'disabled', reason: ... }` payload. Reserved for v0.2+.\n *\n * @stable\n */\nexport function enableTelemetry(): {\n readonly status: 'disabled';\n readonly reason: string;\n} {\n return {\n status: 'disabled',\n reason: 'telemetry not yet implemented (v0.1 ships zero default telemetry)',\n };\n}\n\n/**\n * Detect the reserved env vars and emit one informational line per\n * process. Returns the lines as an array so callers can route them to\n * any sink they like (defaults to `console.info`).\n *\n * @stable\n */\nexport function announceTelemetryPosture(\n opts: { readonly env?: NodeJS.ProcessEnv; readonly sink?: (line: string) => void } = {},\n): ReadonlyArray<string> {\n const env = opts.env ?? process.env;\n const sink = opts.sink ?? ((line: string) => console.info(line));\n const lines: string[] = [];\n if (env.GRAPHORIN_TELEMETRY !== undefined) {\n lines.push(\n `[graphorin/observability] info: GRAPHORIN_TELEMETRY=${env.GRAPHORIN_TELEMETRY} ` +\n '— acknowledged. Telemetry is hardcoded `disabled` in v0.1.',\n );\n }\n if (env.GRAPHORIN_NO_PHONE_HOME !== undefined) {\n lines.push(\n `[graphorin/observability] info: GRAPHORIN_NO_PHONE_HOME=${env.GRAPHORIN_NO_PHONE_HOME} ` +\n '— acknowledged. Graphorin already makes zero outbound calls without explicit user action.',\n );\n }\n for (const line of lines) sink(line);\n return lines;\n}\n"],"mappings":";;;;;;;AAuCA,SAAgB,mBAAmB,MAAyB,QAAQ,KAAsB;CACxF,MAAM,YAAY,IAAI;CACtB,MAAM,cAAc,IAAI;AACxB,QAAO;EACL,SAAS;EACT,QAAQ;EACR,GAAI,cAAc,SAAY,EAAE,GAAG,EAAE,KAAK,WAAW;EACrD,GAAI,gBAAgB,SAAY,EAAE,GAAG,EAAE,aAAa;EACrD;;;;;;;;AASH,SAAgB,kBAGd;AACA,QAAO;EACL,QAAQ;EACR,QAAQ;EACT;;;;;;;;;AAUH,SAAgB,yBACd,OAAqF,EAAE,EAChE;CACvB,MAAM,MAAM,KAAK,OAAO,QAAQ;CAChC,MAAM,OAAO,KAAK,UAAU,SAAiB,QAAQ,KAAK,KAAK;CAC/D,MAAMA,QAAkB,EAAE;AAC1B,KAAI,IAAI,wBAAwB,OAC9B,OAAM,KACJ,uDAAuD,IAAI,oBAAoB,+DAEhF;AAEH,KAAI,IAAI,4BAA4B,OAClC,OAAM,KACJ,2DAA2D,IAAI,wBAAwB,4FAExF;AAEH,MAAK,MAAM,QAAQ,MAAO,MAAK,KAAK;AACpC,QAAO"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/tracer/ids.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Trace and span ID generators. The tracer prefers `crypto.randomUUID()`
|
|
4
|
+
* over a custom RNG for parity with the OpenTelemetry SDK behaviour and
|
|
5
|
+
* to avoid pulling another runtime dependency.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Generate a 32-hex-character (16-byte) trace id.
|
|
11
|
+
*
|
|
12
|
+
* @stable
|
|
13
|
+
*/
|
|
14
|
+
declare function newTraceId(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate a 16-hex-character (8-byte) span id.
|
|
17
|
+
*
|
|
18
|
+
* @stable
|
|
19
|
+
*/
|
|
20
|
+
declare function newSpanId(): string;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { newSpanId, newTraceId };
|
|
23
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","names":[],"sources":["../../src/tracer/ids.ts"],"sourcesContent":[],"mappings":";;AAeA;AASA;;;;;;;;;;iBATgB,UAAA,CAAA;;;;;;iBASA,SAAA,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/tracer/ids.ts
|
|
4
|
+
/**
|
|
5
|
+
* Trace and span ID generators. The tracer prefers `crypto.randomUUID()`
|
|
6
|
+
* over a custom RNG for parity with the OpenTelemetry SDK behaviour and
|
|
7
|
+
* to avoid pulling another runtime dependency.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Generate a 32-hex-character (16-byte) trace id.
|
|
13
|
+
*
|
|
14
|
+
* @stable
|
|
15
|
+
*/
|
|
16
|
+
function newTraceId() {
|
|
17
|
+
return randomBytes(16).toString("hex");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a 16-hex-character (8-byte) span id.
|
|
21
|
+
*
|
|
22
|
+
* @stable
|
|
23
|
+
*/
|
|
24
|
+
function newSpanId() {
|
|
25
|
+
return randomBytes(8).toString("hex");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { newSpanId, newTraceId };
|
|
30
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.js","names":[],"sources":["../../src/tracer/ids.ts"],"sourcesContent":["/**\n * Trace and span ID generators. The tracer prefers `crypto.randomUUID()`\n * over a custom RNG for parity with the OpenTelemetry SDK behaviour and\n * to avoid pulling another runtime dependency.\n *\n * @packageDocumentation\n */\n\nimport { randomBytes } from 'node:crypto';\n\n/**\n * Generate a 32-hex-character (16-byte) trace id.\n *\n * @stable\n */\nexport function newTraceId(): string {\n return randomBytes(16).toString('hex');\n}\n\n/**\n * Generate a 16-hex-character (8-byte) span id.\n *\n * @stable\n */\nexport function newSpanId(): string {\n return randomBytes(8).toString('hex');\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,aAAqB;AACnC,QAAO,YAAY,GAAG,CAAC,SAAS,MAAM;;;;;;;AAQxC,SAAgB,YAAoB;AAClC,QAAO,YAAY,EAAE,CAAC,SAAS,MAAM"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { newSpanId, newTraceId } from "./ids.js";
|
|
2
|
+
import { Sampler, SamplingDecisionMaker, SamplingOptions, SamplingRule, createSampler } from "./sampling.js";
|
|
3
|
+
import { GraphorinSpan, SetAttributeOptions } from "./span.js";
|
|
4
|
+
import { spanNameFor } from "./span-names.js";
|
|
5
|
+
import { GraphorinTracer, TracerOptions, asGraphorinSpan, createTracer } from "./tracer.js";
|
|
6
|
+
export { type GraphorinSpan, type GraphorinTracer, type Sampler, type SamplingDecisionMaker, type SamplingOptions, type SamplingRule, type SetAttributeOptions, type TracerOptions, asGraphorinSpan, createSampler, createTracer, newSpanId, newTraceId, spanNameFor };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { newSpanId, newTraceId } from "./ids.js";
|
|
2
|
+
import { createSampler } from "./sampling.js";
|
|
3
|
+
import { spanNameFor } from "./span-names.js";
|
|
4
|
+
import { asGraphorinSpan, createTracer } from "./tracer.js";
|
|
5
|
+
|
|
6
|
+
export { asGraphorinSpan, createSampler, createTracer, newSpanId, newTraceId, spanNameFor };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SpanType } from "@graphorin/core";
|
|
2
|
+
|
|
3
|
+
//#region src/tracer/sampling.d.ts
|
|
4
|
+
|
|
5
|
+
/** @stable */
|
|
6
|
+
type SamplingDecisionMaker = 'parent-based' | 'always-on' | 'rate-limit';
|
|
7
|
+
/**
|
|
8
|
+
* Per-span-type rate override.
|
|
9
|
+
*
|
|
10
|
+
* @stable
|
|
11
|
+
*/
|
|
12
|
+
interface SamplingRule {
|
|
13
|
+
readonly type: SpanType | string;
|
|
14
|
+
readonly rate: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Configuration shape consumed by {@link createSampler}.
|
|
18
|
+
*
|
|
19
|
+
* @stable
|
|
20
|
+
*/
|
|
21
|
+
interface SamplingOptions {
|
|
22
|
+
/** Default head-sampling rate. Must be in `[0, 1]`. Defaults to `1.0`. */
|
|
23
|
+
readonly rate?: number;
|
|
24
|
+
/** Per-type overrides. Last write wins on duplicate `type`. */
|
|
25
|
+
readonly rules?: ReadonlyArray<SamplingRule>;
|
|
26
|
+
/** Decision maker. Defaults to `'parent-based'`. */
|
|
27
|
+
readonly decisionMaker?: SamplingDecisionMaker;
|
|
28
|
+
/**
|
|
29
|
+
* Cap for the `'rate-limit'` decision maker: at most this many root spans
|
|
30
|
+
* are sampled per rolling 1-second window (RP-19). `undefined` ⇒ no cap
|
|
31
|
+
* (samples everything); `0` ⇒ sample nothing. Ignored by the other
|
|
32
|
+
* decision makers.
|
|
33
|
+
*/
|
|
34
|
+
readonly maxPerSecond?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Clock for the `'rate-limit'` window. Defaults to `Date.now`.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
readonly now?: () => number;
|
|
41
|
+
/**
|
|
42
|
+
* Optional override for streaming-event sampling.
|
|
43
|
+
* @see RB-52 — streaming event family `tool.execute.{progress,partial}`.
|
|
44
|
+
*/
|
|
45
|
+
readonly streaming?: {
|
|
46
|
+
readonly eventSamplingRate?: number;
|
|
47
|
+
readonly includeChunkContent?: 'none' | 'text-only' | 'all';
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Override for the random source. Useful for deterministic tests.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
readonly random?: () => number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* @stable
|
|
58
|
+
*/
|
|
59
|
+
interface Sampler {
|
|
60
|
+
/** Decide whether a span of the given type should be recorded. */
|
|
61
|
+
shouldSample(type: SpanType | string, parentSampled?: boolean): boolean;
|
|
62
|
+
/** Decide whether a span event of the given name should be recorded. */
|
|
63
|
+
shouldRecordEvent(name: string): boolean;
|
|
64
|
+
/** Returns whether chunk *content* should travel through the exporter. */
|
|
65
|
+
shouldIncludeChunkContent(): boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a {@link Sampler} from the supplied options. The sampler is
|
|
69
|
+
* intentionally inexpensive — every decision boils down to a single
|
|
70
|
+
* `random < threshold` comparison.
|
|
71
|
+
*
|
|
72
|
+
* @stable
|
|
73
|
+
*/
|
|
74
|
+
declare function createSampler(opts?: SamplingOptions): Sampler;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { Sampler, SamplingDecisionMaker, SamplingOptions, SamplingRule, createSampler };
|
|
77
|
+
//# sourceMappingURL=sampling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampling.d.ts","names":[],"sources":["../../src/tracer/sampling.ts"],"sourcesContent":[],"mappings":";;;;;AAoEiB,KAxDL,qBAAA,GA0DiB,cAAA,GAAA,WAAA,GAAA,YAAA;AAc7B;;;;;UAjEiB,YAAA;iBACA;;;;;;;;UASA,eAAA;;;;mBAIE,cAAc;;2BAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAiCV,OAAA;;qBAEI;;;;;;;;;;;;;iBAcL,aAAA,QAAoB,kBAAuB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region src/tracer/sampling.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build a {@link Sampler} from the supplied options. The sampler is
|
|
4
|
+
* intentionally inexpensive — every decision boils down to a single
|
|
5
|
+
* `random < threshold` comparison.
|
|
6
|
+
*
|
|
7
|
+
* @stable
|
|
8
|
+
*/
|
|
9
|
+
function createSampler(opts = {}) {
|
|
10
|
+
const baseRate = clampRate(opts.rate ?? 1);
|
|
11
|
+
const decisionMaker = opts.decisionMaker ?? "parent-based";
|
|
12
|
+
const ruleByType = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const rule of opts.rules ?? []) ruleByType.set(rule.type, clampRate(rule.rate));
|
|
14
|
+
const streamingRate = clampRate(opts.streaming?.eventSamplingRate ?? 1);
|
|
15
|
+
const includeChunks = opts.streaming?.includeChunkContent ?? "none";
|
|
16
|
+
const random = opts.random ?? Math.random;
|
|
17
|
+
const maxPerSecond = opts.maxPerSecond;
|
|
18
|
+
const now = opts.now ?? Date.now;
|
|
19
|
+
let windowStart;
|
|
20
|
+
let windowCount = 0;
|
|
21
|
+
return {
|
|
22
|
+
shouldSample(type, parentSampled) {
|
|
23
|
+
if (decisionMaker === "always-on") return true;
|
|
24
|
+
if (decisionMaker === "parent-based") {
|
|
25
|
+
if (parentSampled === true) return true;
|
|
26
|
+
if (parentSampled === false) return false;
|
|
27
|
+
}
|
|
28
|
+
if (decisionMaker === "rate-limit") {
|
|
29
|
+
if (maxPerSecond === void 0) return true;
|
|
30
|
+
if (maxPerSecond <= 0) return false;
|
|
31
|
+
const t = now();
|
|
32
|
+
if (windowStart === void 0 || t - windowStart >= 1e3) {
|
|
33
|
+
windowStart = t;
|
|
34
|
+
windowCount = 0;
|
|
35
|
+
}
|
|
36
|
+
if (windowCount < maxPerSecond) {
|
|
37
|
+
windowCount += 1;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const rate = ruleByType.get(type) ?? baseRate;
|
|
43
|
+
if (rate >= 1) return true;
|
|
44
|
+
if (rate <= 0) return false;
|
|
45
|
+
return random() < rate;
|
|
46
|
+
},
|
|
47
|
+
shouldRecordEvent(name) {
|
|
48
|
+
const rate = ruleByType.get(name) ?? streamingRate;
|
|
49
|
+
if (rate >= 1) return true;
|
|
50
|
+
if (rate <= 0) return false;
|
|
51
|
+
return random() < rate;
|
|
52
|
+
},
|
|
53
|
+
shouldIncludeChunkContent() {
|
|
54
|
+
return includeChunks !== "none";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function clampRate(rate) {
|
|
59
|
+
if (Number.isNaN(rate)) return 1;
|
|
60
|
+
if (rate < 0) return 0;
|
|
61
|
+
if (rate > 1) return 1;
|
|
62
|
+
return rate;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
export { createSampler };
|
|
67
|
+
//# sourceMappingURL=sampling.js.map
|