@graphorin/observability 0.6.0 → 0.7.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 +42 -0
- package/README.md +29 -5
- package/dist/cost/cost-tracker.d.ts.map +1 -1
- package/dist/cost/cost-tracker.js +34 -0
- package/dist/cost/cost-tracker.js.map +1 -1
- package/dist/cost/delegate.d.ts +40 -0
- package/dist/cost/delegate.d.ts.map +1 -0
- package/dist/cost/delegate.js +31 -0
- package/dist/cost/delegate.js.map +1 -0
- package/dist/cost/index.d.ts +2 -1
- package/dist/cost/index.js +2 -1
- package/dist/cost/types.d.ts +39 -0
- package/dist/cost/types.d.ts.map +1 -1
- package/dist/exporters/otlp-http.d.ts.map +1 -1
- package/dist/exporters/otlp-http.js +4 -2
- package/dist/exporters/otlp-http.js.map +1 -1
- package/dist/exporters/types.d.ts +7 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/exporters/types.js.map +1 -1
- package/dist/exporters/with-validation.d.ts.map +1 -1
- package/dist/exporters/with-validation.js +5 -4
- package/dist/exporters/with-validation.js.map +1 -1
- package/dist/gen-ai/emit.js +9 -1
- package/dist/gen-ai/emit.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -10
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/redaction/imperative-patterns.d.ts +3 -3
- package/dist/redaction/imperative-patterns.d.ts.map +1 -1
- package/dist/redaction/imperative-patterns.js +7 -0
- package/dist/redaction/imperative-patterns.js.map +1 -1
- package/dist/redaction/patterns.d.ts.map +1 -1
- package/dist/redaction/patterns.js +2 -2
- package/dist/redaction/patterns.js.map +1 -1
- package/dist/tracer/sampling.d.ts +6 -1
- package/dist/tracer/sampling.d.ts.map +1 -1
- package/dist/tracer/sampling.js +7 -1
- package/dist/tracer/sampling.js.map +1 -1
- package/dist/tracer/span.d.ts.map +1 -1
- package/dist/tracer/span.js +12 -3
- package/dist/tracer/span.js.map +1 -1
- package/package.json +18 -35
- package/src/cost/cost-tracker.ts +315 -0
- package/src/cost/delegate.ts +79 -0
- package/src/cost/index.ts +23 -0
- package/src/cost/types.ts +151 -0
- package/src/eval/index.ts +16 -0
- package/src/eval/runner.ts +146 -0
- package/src/eval/types.ts +122 -0
- package/src/exporters/console.ts +75 -0
- package/src/exporters/index.ts +36 -0
- package/src/exporters/jsonl.ts +167 -0
- package/src/exporters/otlp-http.ts +178 -0
- package/src/exporters/types.ts +85 -0
- package/src/exporters/with-validation.ts +176 -0
- package/src/gen-ai/emit.ts +157 -0
- package/src/gen-ai/index.ts +26 -0
- package/src/gen-ai/operation-mapping.ts +89 -0
- package/src/gen-ai/system-derivation.ts +84 -0
- package/src/gen-ai/types.ts +143 -0
- package/src/index.ts +36 -0
- package/src/logger/index.ts +13 -0
- package/src/logger/logger.ts +178 -0
- package/src/openinference/index.ts +133 -0
- package/src/redaction/config.ts +50 -0
- package/src/redaction/errors.ts +53 -0
- package/src/redaction/imperative-patterns.ts +268 -0
- package/src/redaction/index.ts +42 -0
- package/src/redaction/patterns.ts +263 -0
- package/src/redaction/types.ts +116 -0
- package/src/redaction/validator.ts +302 -0
- package/src/replay/config.ts +58 -0
- package/src/replay/index.ts +17 -0
- package/src/replay/log.ts +89 -0
- package/src/replay/replay.ts +196 -0
- package/src/replay/types.ts +112 -0
- package/src/telemetry/index.ts +94 -0
- package/src/tracer/ids.ts +27 -0
- package/src/tracer/index.ts +22 -0
- package/src/tracer/sampling.ts +170 -0
- package/src/tracer/span-names.ts +52 -0
- package/src/tracer/span.ts +175 -0
- package/src/tracer/tracer.ts +309 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
2
|
+
/**
|
|
3
|
+
* `OTLPHttpExporter` - reference implementation that POSTs span
|
|
4
|
+
* records to an OpenTelemetry HTTP endpoint.
|
|
5
|
+
*
|
|
6
|
+
* The exporter is deliberately minimal: production hardening (retry
|
|
7
|
+
* logic, batching policy tuning, gzip compression, bearer-token auth
|
|
8
|
+
* rotation) is left to operators. The wire format follows the OTLP
|
|
9
|
+
* `traces` resource shape and is compatible with collectors that accept
|
|
10
|
+
* the `application/json` content type.
|
|
11
|
+
*
|
|
12
|
+
* Operators wanting the upstream OpenTelemetry SDK exporter
|
|
13
|
+
* (`@opentelemetry/exporter-trace-otlp-http`) can adapt
|
|
14
|
+
* {@link toOtlpEnvelope} into their own wrapper - the helper is
|
|
15
|
+
* exported so the OTel adapter does not need to duplicate the wire
|
|
16
|
+
* shape.
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { SpanAttributes, SpanAttributeValue, SpanType } from '@graphorin/core';
|
|
22
|
+
|
|
23
|
+
import type { SpanRecord, SpanRecordEvent, TraceExporter } from './types.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Configuration shape for {@link createOTLPHttpExporter}.
|
|
27
|
+
*
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
export interface OTLPHttpExporterOptions {
|
|
31
|
+
/** Identifier reported via `exporter.id`. Defaults to `'otlp-http'`. */
|
|
32
|
+
readonly id?: string;
|
|
33
|
+
/** OTLP collector URL (e.g. `http://localhost:4318/v1/traces`). */
|
|
34
|
+
readonly url: string;
|
|
35
|
+
/** Optional headers (auth, tenant id, …). Avoid passing secrets in plain. */
|
|
36
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
37
|
+
/** Service name embedded in the OTLP `Resource`. */
|
|
38
|
+
readonly serviceName?: string;
|
|
39
|
+
/** Optional `fetch` override - useful for testing without the network. */
|
|
40
|
+
readonly fetchImpl?: typeof fetch;
|
|
41
|
+
/** Optional timeout (ms). Defaults to 10000. */
|
|
42
|
+
readonly timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a minimal OTLP-over-HTTP trace exporter. Call
|
|
47
|
+
* `withValidation(exporter)` before passing the result to
|
|
48
|
+
* `createTracer({ exporters })`.
|
|
49
|
+
*
|
|
50
|
+
* The implementation issues one `POST` per finished span - no
|
|
51
|
+
* batching. Operators wanting batching should wrap this exporter
|
|
52
|
+
* with their own queue or use a sidecar collector.
|
|
53
|
+
*
|
|
54
|
+
* @stable
|
|
55
|
+
*/
|
|
56
|
+
export function createOTLPHttpExporter(opts: OTLPHttpExporterOptions): TraceExporter {
|
|
57
|
+
const id = opts.id ?? 'otlp-http';
|
|
58
|
+
const url = opts.url;
|
|
59
|
+
const headers = opts.headers ?? {};
|
|
60
|
+
const serviceName = opts.serviceName ?? 'graphorin';
|
|
61
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
62
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
63
|
+
let closed = false;
|
|
64
|
+
|
|
65
|
+
if (typeof fetchImpl !== 'function') {
|
|
66
|
+
throw new TypeError(
|
|
67
|
+
'createOTLPHttpExporter: no fetch implementation available. Provide ' +
|
|
68
|
+
'`fetchImpl` explicitly or run on a Node.js version with global fetch ' +
|
|
69
|
+
'(>= 18).',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
async export(record: SpanRecord): Promise<void> {
|
|
76
|
+
if (closed) return;
|
|
77
|
+
const body = toOtlpEnvelope(record, serviceName);
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetchImpl(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
84
|
+
body: JSON.stringify(body),
|
|
85
|
+
signal: controller.signal,
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(`OTLP exporter received HTTP ${res.status} ${res.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async flush(): Promise<void> {
|
|
95
|
+
// No internal queue; each export() awaits the POST already.
|
|
96
|
+
},
|
|
97
|
+
async shutdown(): Promise<void> {
|
|
98
|
+
closed = true;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @internal - exposed for unit tests
|
|
105
|
+
*/
|
|
106
|
+
export function toOtlpEnvelope(record: SpanRecord, serviceName: string): unknown {
|
|
107
|
+
const otelStatus = record.status === 'ok' ? 1 : record.status === 'error' ? 2 : 0;
|
|
108
|
+
return {
|
|
109
|
+
resourceSpans: [
|
|
110
|
+
{
|
|
111
|
+
resource: {
|
|
112
|
+
attributes: [
|
|
113
|
+
{ key: 'service.name', value: { stringValue: serviceName } },
|
|
114
|
+
{ key: 'graphorin.framework.version', value: { stringValue: pkg.version } },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
scopeSpans: [
|
|
118
|
+
{
|
|
119
|
+
scope: { name: '@graphorin/observability', version: pkg.version },
|
|
120
|
+
spans: [
|
|
121
|
+
{
|
|
122
|
+
traceId: record.traceId,
|
|
123
|
+
spanId: record.id,
|
|
124
|
+
...(record.parentId === undefined ? {} : { parentSpanId: record.parentId }),
|
|
125
|
+
name: record.name,
|
|
126
|
+
kind: spanKindFor(record.type),
|
|
127
|
+
startTimeUnixNano: String(record.startUnixNano),
|
|
128
|
+
endTimeUnixNano: String(record.endUnixNano),
|
|
129
|
+
attributes: toOtlpAttributes(record.attributes),
|
|
130
|
+
events: record.events.map(toOtlpEvent),
|
|
131
|
+
status: {
|
|
132
|
+
code: otelStatus,
|
|
133
|
+
...(record.statusMessage === undefined ? {} : { message: record.statusMessage }),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function spanKindFor(type: SpanType): number {
|
|
145
|
+
// OTel SpanKind enum: 0 unspecified, 1 internal, 2 server, 3 client, 4 producer, 5 consumer.
|
|
146
|
+
if (type === 'provider.generate' || type === 'provider.stream' || type === 'mcp.call') return 3;
|
|
147
|
+
if (type === 'tool.execute' || type === 'memory.embed') return 1;
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function toOtlpAttributes(attrs: SpanAttributes): unknown[] {
|
|
152
|
+
return Object.entries(attrs).map(([k, v]) => ({ key: k, value: toOtlpAttributeValue(v) }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function toOtlpEvent(event: SpanRecordEvent): unknown {
|
|
156
|
+
return {
|
|
157
|
+
timeUnixNano: String(event.timeUnixNano),
|
|
158
|
+
name: event.name,
|
|
159
|
+
attributes: toOtlpAttributes(event.attributes),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toOtlpAttributeValue(v: SpanAttributeValue): unknown {
|
|
164
|
+
if (typeof v === 'string') return { stringValue: v };
|
|
165
|
+
if (typeof v === 'number') {
|
|
166
|
+
return Number.isInteger(v) ? { intValue: String(v) } : { doubleValue: v };
|
|
167
|
+
}
|
|
168
|
+
if (typeof v === 'boolean') return { boolValue: v };
|
|
169
|
+
if (v === null) return { stringValue: '' };
|
|
170
|
+
if (Array.isArray(v)) {
|
|
171
|
+
return {
|
|
172
|
+
arrayValue: {
|
|
173
|
+
values: v.map((item) => toOtlpAttributeValue(item as SpanAttributeValue)),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { stringValue: String(v) };
|
|
178
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the exporter surface.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SpanAttributes, SpanAttributeValue, SpanStatus, SpanType } from '@graphorin/core';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discriminator marker - every exporter that has been wrapped via
|
|
11
|
+
* `withValidation(...)` is branded with this symbol so the tracer can
|
|
12
|
+
* fail-fast at startup when a raw exporter is registered.
|
|
13
|
+
*
|
|
14
|
+
* @stable
|
|
15
|
+
*/
|
|
16
|
+
export const VALIDATED_EXPORTER_BRAND: unique symbol = Symbol.for(
|
|
17
|
+
'graphorin.observability.exporter.validated',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sanitized, JSON-serialisable representation of a finished span. The
|
|
22
|
+
* exporters never see the live OTel span; the tracer materialises this
|
|
23
|
+
* record once the span ends and runs it through the validator.
|
|
24
|
+
*
|
|
25
|
+
* @stable
|
|
26
|
+
*/
|
|
27
|
+
export interface SpanRecord<T extends SpanType = SpanType> {
|
|
28
|
+
readonly type: T;
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly traceId: string;
|
|
31
|
+
readonly parentId?: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly startUnixNano: number;
|
|
34
|
+
readonly endUnixNano: number;
|
|
35
|
+
readonly status: SpanStatus;
|
|
36
|
+
readonly statusMessage?: string;
|
|
37
|
+
readonly attributes: SpanAttributes;
|
|
38
|
+
readonly events: ReadonlyArray<SpanRecordEvent>;
|
|
39
|
+
/** Optional per-attribute sensitivity map - see `setAttribute({ sensitivity })`. */
|
|
40
|
+
readonly sensitivityByAttribute?: Readonly<Record<string, SpanAttributeValue>>;
|
|
41
|
+
/** Set when the validator dropped the span entirely (replay marker). */
|
|
42
|
+
readonly droppedReason?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Single span event carried alongside the span record.
|
|
47
|
+
*
|
|
48
|
+
* @stable
|
|
49
|
+
*/
|
|
50
|
+
export interface SpanRecordEvent {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly timeUnixNano: number;
|
|
53
|
+
readonly attributes: SpanAttributes;
|
|
54
|
+
/**
|
|
55
|
+
* W-094: per-attribute sensitivity map recorded by
|
|
56
|
+
* `addEvent(name, attrs, { sensitivity, sensitivityByAttribute })`.
|
|
57
|
+
* Consumed by the validation exporter; absent ⇒ every attribute is
|
|
58
|
+
* untagged (default-deny below the export floor).
|
|
59
|
+
*/
|
|
60
|
+
readonly sensitivityByAttribute?: Readonly<Record<string, 'public' | 'internal' | 'secret'>>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A trace exporter contract. Exporters consume a stream of finished
|
|
65
|
+
* spans and forward them to a sink (console, file, OTLP wire, …).
|
|
66
|
+
*
|
|
67
|
+
* @stable
|
|
68
|
+
*/
|
|
69
|
+
export interface TraceExporter {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
/** Forward a finished span record. Implementations should be cheap. */
|
|
72
|
+
export(record: SpanRecord): Promise<void>;
|
|
73
|
+
/** Flush any buffered spans. Called on `tracer.shutdown()`. */
|
|
74
|
+
flush(): Promise<void>;
|
|
75
|
+
/** Close any underlying resources. Idempotent. */
|
|
76
|
+
shutdown(): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Branded-marker stub. Set by `withValidation(...)` to signal that the
|
|
79
|
+
* exporter has been wrapped. Direct exporters omit the brand and the
|
|
80
|
+
* tracer fails fast at startup.
|
|
81
|
+
*
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
readonly [VALIDATED_EXPORTER_BRAND]?: true;
|
|
85
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withValidation(exporter, opts)` - the **mandatory wrapper** applied
|
|
3
|
+
* to every exporter. Forwards each span record through a
|
|
4
|
+
* `RedactionValidator` first; drops + counts when a value exceeds the
|
|
5
|
+
* configured tier floor or matches a secret / PII pattern.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Sensitivity, SpanAttributes, SpanAttributeValue } from '@graphorin/core';
|
|
11
|
+
import type {
|
|
12
|
+
RedactionCounters,
|
|
13
|
+
RedactionValidatorInstance,
|
|
14
|
+
RedactionValidatorOptions,
|
|
15
|
+
} from '../redaction/types.js';
|
|
16
|
+
import { createRedactionValidator } from '../redaction/validator.js';
|
|
17
|
+
|
|
18
|
+
import { type SpanRecord, type TraceExporter, VALIDATED_EXPORTER_BRAND } from './types.js';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_ATTR_SENSITIVITY: Sensitivity = 'internal';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for {@link withValidation}.
|
|
24
|
+
*
|
|
25
|
+
* @stable
|
|
26
|
+
*/
|
|
27
|
+
export interface WithValidationOptions extends RedactionValidatorOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Optional pre-built validator. When supplied, the rest of the
|
|
30
|
+
* options on this object are ignored and the supplied validator is
|
|
31
|
+
* reused - useful for sharing one validator across multiple exporters.
|
|
32
|
+
*/
|
|
33
|
+
readonly validator?: RedactionValidatorInstance;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wrap an exporter so every span flows through a {@link RedactionValidator}
|
|
38
|
+
* before reaching the sink. Exporters that are not wrapped are rejected
|
|
39
|
+
* by the tracer at startup.
|
|
40
|
+
*
|
|
41
|
+
* @stable
|
|
42
|
+
*/
|
|
43
|
+
export function withValidation<E extends TraceExporter>(
|
|
44
|
+
exporter: E,
|
|
45
|
+
opts: WithValidationOptions = {},
|
|
46
|
+
): TraceExporter {
|
|
47
|
+
const validator = opts.validator ?? createRedactionValidator(opts);
|
|
48
|
+
const wrapped: TraceExporter = {
|
|
49
|
+
id: `${exporter.id}+validated`,
|
|
50
|
+
[VALIDATED_EXPORTER_BRAND]: true,
|
|
51
|
+
async export(record: SpanRecord): Promise<void> {
|
|
52
|
+
// RP-18: the validator strips offending attributes (counting each drop)
|
|
53
|
+
// and always returns an exportable record - a single untagged or
|
|
54
|
+
// over-tier attribute no longer makes the whole span vanish from every
|
|
55
|
+
// exporter.
|
|
56
|
+
await exporter.export(sanitizeRecord(record, validator));
|
|
57
|
+
},
|
|
58
|
+
flush: () => exporter.flush(),
|
|
59
|
+
shutdown: () => exporter.shutdown(),
|
|
60
|
+
};
|
|
61
|
+
return wrapped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export function sanitizeRecord(
|
|
68
|
+
record: SpanRecord,
|
|
69
|
+
validator: RedactionValidatorInstance,
|
|
70
|
+
): SpanRecord {
|
|
71
|
+
const sensitivities = record.sensitivityByAttribute ?? {};
|
|
72
|
+
// RP-18: attribute-granular sanitization. Offending attributes are stripped
|
|
73
|
+
// (and counted by the validator); the span itself always survives so
|
|
74
|
+
// framework spans carrying untagged attributes still reach every exporter.
|
|
75
|
+
const sanitizedAttrs = sanitizeAttributes(
|
|
76
|
+
record.attributes,
|
|
77
|
+
sensitivities,
|
|
78
|
+
validator,
|
|
79
|
+
record.type,
|
|
80
|
+
);
|
|
81
|
+
const sanitizedEvents = sanitizeEvents(record.events, validator, record.type);
|
|
82
|
+
return {
|
|
83
|
+
...record,
|
|
84
|
+
attributes: sanitizedAttrs,
|
|
85
|
+
events: sanitizedEvents,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function sanitizeAttributes(
|
|
93
|
+
attrs: SpanAttributes,
|
|
94
|
+
sensitivities: Readonly<Record<string, SpanAttributeValue>>,
|
|
95
|
+
validator: RedactionValidatorInstance,
|
|
96
|
+
spanType?: string,
|
|
97
|
+
origin?: string,
|
|
98
|
+
): SpanAttributes {
|
|
99
|
+
const out: Record<string, SpanAttributeValue> = {};
|
|
100
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
101
|
+
const tier = readTier(sensitivities[key]);
|
|
102
|
+
const result = validator.validate({
|
|
103
|
+
value,
|
|
104
|
+
tier,
|
|
105
|
+
context: {
|
|
106
|
+
attribute: key,
|
|
107
|
+
...(spanType !== undefined ? { spanType } : {}),
|
|
108
|
+
// W-094: lets onViolation / droppedByReason tell an EVENT
|
|
109
|
+
// attribute from a span attribute.
|
|
110
|
+
...(origin !== undefined ? { origin } : {}),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (result === null) {
|
|
114
|
+
// RP-18: this single attribute exceeded the floor (or matched a secret
|
|
115
|
+
// pattern). Strip it - the validator has already counted the drop - and
|
|
116
|
+
// keep the rest of the span rather than discarding the whole record.
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
out[key] = result.value as SpanAttributeValue;
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze(out) as SpanAttributes;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
export function sanitizeEvents(
|
|
128
|
+
events: SpanRecord['events'],
|
|
129
|
+
validator: RedactionValidatorInstance,
|
|
130
|
+
spanType?: string,
|
|
131
|
+
): SpanRecord['events'] {
|
|
132
|
+
const out: SpanRecord['events'][number][] = [];
|
|
133
|
+
for (const event of events) {
|
|
134
|
+
// W-094: honour the per-event sensitivity map recorded by
|
|
135
|
+
// `addEvent(..., opts)` (pre-fix an empty map was passed here, so
|
|
136
|
+
// EVERY event attribute - including `exception.type` - was dropped
|
|
137
|
+
// under the default 'public' floor). Untagged attributes still
|
|
138
|
+
// default-deny; the `event:<name>` origin distinguishes event drops
|
|
139
|
+
// from span-attribute drops in onViolation.
|
|
140
|
+
const sanitizedAttrs = sanitizeAttributes(
|
|
141
|
+
event.attributes,
|
|
142
|
+
event.sensitivityByAttribute ?? {},
|
|
143
|
+
validator,
|
|
144
|
+
spanType,
|
|
145
|
+
`event:${event.name}`,
|
|
146
|
+
);
|
|
147
|
+
out.push({ name: event.name, timeUnixNano: event.timeUnixNano, attributes: sanitizedAttrs });
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readTier(value: SpanAttributeValue | undefined): Sensitivity {
|
|
153
|
+
if (value === 'public' || value === 'internal' || value === 'secret') return value;
|
|
154
|
+
return DEFAULT_ATTR_SENSITIVITY;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Quickly check whether an exporter is the result of a previous
|
|
159
|
+
* {@link withValidation} call. The tracer uses this to fail fast at
|
|
160
|
+
* startup.
|
|
161
|
+
*
|
|
162
|
+
* @stable
|
|
163
|
+
*/
|
|
164
|
+
export function isValidatedExporter(exporter: TraceExporter): boolean {
|
|
165
|
+
return exporter[VALIDATED_EXPORTER_BRAND] === true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Pull the counters out of any exporter wrapped by {@link withValidation}.
|
|
170
|
+
* Returns `null` for exporters that were never wrapped.
|
|
171
|
+
*
|
|
172
|
+
* @stable
|
|
173
|
+
*/
|
|
174
|
+
export function tryGetValidatorCounters(validator: RedactionValidatorInstance): RedactionCounters {
|
|
175
|
+
return validator.counters();
|
|
176
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emission helpers that attach OpenTelemetry GenAI semantic-convention
|
|
3
|
+
* attributes / events to an existing span.
|
|
4
|
+
*
|
|
5
|
+
* The helpers are deliberately additive - they NEVER remove or rename
|
|
6
|
+
* existing attributes. The new `gen_ai.*` family sits on top of the
|
|
7
|
+
* Graphorin-prefixed family. Sensitivity defaults follow the catalogue
|
|
8
|
+
* documented alongside the canonical mapping table:
|
|
9
|
+
*
|
|
10
|
+
* - `gen_ai.system` / `gen_ai.request.model` / `gen_ai.response.model` /
|
|
11
|
+
* `gen_ai.response.id` / `gen_ai.usage.{input,output}_tokens` /
|
|
12
|
+
* `gen_ai.response.finish_reasons` / `gen_ai.tool.{name,type,call.id,description}` /
|
|
13
|
+
* `gen_ai.agent.{id,name}` / `gen_ai.session.id` /
|
|
14
|
+
* `gen_ai.operation.name` → `'public'`
|
|
15
|
+
* - `gen_ai.request.messages` / per-message events / `gen_ai.response.content`
|
|
16
|
+
* → `'internal'` (default-deny non-public per DEC-141)
|
|
17
|
+
* - `gen_ai.tool.output` → inherits per-tool `Tool.sensitivity`
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { AISpan, SpanAttributes, SpanAttributeValue, SpanType } from '@graphorin/core';
|
|
23
|
+
|
|
24
|
+
import type { GraphorinSpan } from '../tracer/span.js';
|
|
25
|
+
import { asGraphorinSpan } from '../tracer/tracer.js';
|
|
26
|
+
|
|
27
|
+
import { operationNameFor } from './operation-mapping.js';
|
|
28
|
+
import type { GenAIAttributes, GenAIMessage, GenAIMessageRole } from './types.js';
|
|
29
|
+
|
|
30
|
+
const PUBLIC_KEY_PREFIXES = [
|
|
31
|
+
'gen_ai.system',
|
|
32
|
+
'gen_ai.request.model',
|
|
33
|
+
'gen_ai.response.model',
|
|
34
|
+
'gen_ai.response.id',
|
|
35
|
+
'gen_ai.response.finish_reasons',
|
|
36
|
+
'gen_ai.usage.input_tokens',
|
|
37
|
+
'gen_ai.usage.output_tokens',
|
|
38
|
+
'gen_ai.tool.name',
|
|
39
|
+
'gen_ai.tool.type',
|
|
40
|
+
'gen_ai.tool.call.id',
|
|
41
|
+
'gen_ai.tool.description',
|
|
42
|
+
'gen_ai.agent.id',
|
|
43
|
+
'gen_ai.agent.name',
|
|
44
|
+
'gen_ai.session.id',
|
|
45
|
+
'gen_ai.operation.name',
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Attach the canonical `gen_ai.*` attribute set to a span. The helper
|
|
50
|
+
* is additive on the existing Graphorin-prefixed attributes and applies
|
|
51
|
+
* the per-attribute sensitivity defaults catalogue.
|
|
52
|
+
*
|
|
53
|
+
* @stable
|
|
54
|
+
*/
|
|
55
|
+
export function emitGenAIAttributes<T extends SpanType>(
|
|
56
|
+
span: AISpan<T>,
|
|
57
|
+
attrs: GenAIAttributes,
|
|
58
|
+
): void {
|
|
59
|
+
const gs = asGraphorinSpan(span);
|
|
60
|
+
const operation = attrs.operation ?? operationNameFor(span.type);
|
|
61
|
+
if (operation !== undefined) setPublic(gs, span, 'gen_ai.operation.name', operation);
|
|
62
|
+
|
|
63
|
+
if (attrs.system !== undefined) setPublic(gs, span, 'gen_ai.system', attrs.system);
|
|
64
|
+
if (attrs.requestModel !== undefined)
|
|
65
|
+
setPublic(gs, span, 'gen_ai.request.model', attrs.requestModel);
|
|
66
|
+
if (attrs.responseModel !== undefined)
|
|
67
|
+
setPublic(gs, span, 'gen_ai.response.model', attrs.responseModel);
|
|
68
|
+
if (attrs.responseId !== undefined) setPublic(gs, span, 'gen_ai.response.id', attrs.responseId);
|
|
69
|
+
if (attrs.inputTokens !== undefined)
|
|
70
|
+
setPublic(gs, span, 'gen_ai.usage.input_tokens', attrs.inputTokens);
|
|
71
|
+
if (attrs.outputTokens !== undefined)
|
|
72
|
+
setPublic(gs, span, 'gen_ai.usage.output_tokens', attrs.outputTokens);
|
|
73
|
+
if (attrs.finishReasons !== undefined)
|
|
74
|
+
setPublic(gs, span, 'gen_ai.response.finish_reasons', [...attrs.finishReasons]);
|
|
75
|
+
if (attrs.agentId !== undefined) setPublic(gs, span, 'gen_ai.agent.id', attrs.agentId);
|
|
76
|
+
if (attrs.agentName !== undefined) setPublic(gs, span, 'gen_ai.agent.name', attrs.agentName);
|
|
77
|
+
if (attrs.sessionId !== undefined) setPublic(gs, span, 'gen_ai.session.id', attrs.sessionId);
|
|
78
|
+
if (attrs.toolName !== undefined) setPublic(gs, span, 'gen_ai.tool.name', attrs.toolName);
|
|
79
|
+
if (attrs.toolType !== undefined) setPublic(gs, span, 'gen_ai.tool.type', attrs.toolType);
|
|
80
|
+
if (attrs.toolCallId !== undefined) setPublic(gs, span, 'gen_ai.tool.call.id', attrs.toolCallId);
|
|
81
|
+
if (attrs.toolDescription !== undefined)
|
|
82
|
+
setPublic(gs, span, 'gen_ai.tool.description', attrs.toolDescription);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Emit per-message OpenTelemetry GenAI span events. The helper records
|
|
87
|
+
* one event per message - the per-message-event emission shape per the
|
|
88
|
+
* OTel semconv discipline (size-bounded individually; safer than the
|
|
89
|
+
* aggregate-attribute shape on large prompts).
|
|
90
|
+
*
|
|
91
|
+
* @stable
|
|
92
|
+
*/
|
|
93
|
+
export function emitGenAIMessageEvents<T extends SpanType>(
|
|
94
|
+
span: AISpan<T>,
|
|
95
|
+
messages: ReadonlyArray<GenAIMessage>,
|
|
96
|
+
opts: { readonly system?: string } = {},
|
|
97
|
+
): void {
|
|
98
|
+
for (const message of messages) {
|
|
99
|
+
// W-094: structural metadata (role, provider name, message name,
|
|
100
|
+
// tool-call id) is 'public' so a message event survives the default
|
|
101
|
+
// export floor as a marker; the CONTENT (and tool-call payloads)
|
|
102
|
+
// stays 'internal' - exported only when the operator opted the
|
|
103
|
+
// floor up, with the PII patterns applying there.
|
|
104
|
+
span.addEvent(eventNameFor(message.role), buildEventAttrs(message, opts.system), {
|
|
105
|
+
sensitivity: 'internal',
|
|
106
|
+
sensitivityByAttribute: {
|
|
107
|
+
'gen_ai.message.role': 'public',
|
|
108
|
+
'gen_ai.system': 'public',
|
|
109
|
+
'gen_ai.message.name': 'public',
|
|
110
|
+
'gen_ai.tool.call.id': 'public',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function eventNameFor(role: GenAIMessageRole): string {
|
|
117
|
+
switch (role) {
|
|
118
|
+
case 'system':
|
|
119
|
+
return 'gen_ai.system.message';
|
|
120
|
+
case 'user':
|
|
121
|
+
return 'gen_ai.user.message';
|
|
122
|
+
case 'assistant':
|
|
123
|
+
return 'gen_ai.assistant.message';
|
|
124
|
+
case 'tool':
|
|
125
|
+
return 'gen_ai.tool.message';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildEventAttrs(message: GenAIMessage, system?: string): SpanAttributes {
|
|
130
|
+
const out: Record<string, SpanAttributeValue> = {
|
|
131
|
+
'gen_ai.message.role': message.role,
|
|
132
|
+
content: message.content,
|
|
133
|
+
};
|
|
134
|
+
if (system !== undefined) out['gen_ai.system'] = system;
|
|
135
|
+
if (message.name !== undefined) out['gen_ai.message.name'] = message.name;
|
|
136
|
+
if (message.toolCallId !== undefined) out['gen_ai.tool.call.id'] = message.toolCallId;
|
|
137
|
+
if (message.toolCalls !== undefined && message.toolCalls.length > 0) {
|
|
138
|
+
out['gen_ai.tool.calls'] = JSON.stringify(message.toolCalls);
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze(out) as SpanAttributes;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function setPublic(
|
|
144
|
+
gs: GraphorinSpan | null,
|
|
145
|
+
span: AISpan,
|
|
146
|
+
key: string,
|
|
147
|
+
value: SpanAttributeValue,
|
|
148
|
+
): void {
|
|
149
|
+
if (
|
|
150
|
+
gs !== null &&
|
|
151
|
+
PUBLIC_KEY_PREFIXES.some((prefix) => key === prefix || key.startsWith(`${prefix}.`))
|
|
152
|
+
) {
|
|
153
|
+
gs.setAttribute(key, value, { sensitivity: 'public' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
span.setAttributes({ [key]: value });
|
|
157
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry GenAI semantic-conventions conformance helpers.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { emitGenAIAttributes, emitGenAIMessageEvents } from './emit.js';
|
|
8
|
+
export {
|
|
9
|
+
OPERATION_NAME_TABLE,
|
|
10
|
+
operationNameFor,
|
|
11
|
+
} from './operation-mapping.js';
|
|
12
|
+
export {
|
|
13
|
+
_resetGenAISystemWarningsForTesting,
|
|
14
|
+
deriveGenAISystem,
|
|
15
|
+
PROVIDER_CLASS_TO_GEN_AI_SYSTEM,
|
|
16
|
+
setGenAISystemWarnSink,
|
|
17
|
+
} from './system-derivation.js';
|
|
18
|
+
export type {
|
|
19
|
+
GenAIAttributes,
|
|
20
|
+
GenAIMessage,
|
|
21
|
+
GenAIMessageRole,
|
|
22
|
+
GenAIOperationName,
|
|
23
|
+
GenAISystem,
|
|
24
|
+
GenAIToolType,
|
|
25
|
+
SpanTypeToOperationName,
|
|
26
|
+
} from './types.js';
|