@graphorin/observability 0.6.1 → 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 +31 -0
- package/README.md +24 -4
- 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/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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -8
- package/dist/index.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- 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/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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concrete `AISpan<T>` implementation. The span is intentionally
|
|
3
|
+
* decoupled from `@opentelemetry/api`: the tracer materialises a
|
|
4
|
+
* {@link SpanRecord} on `end()` and lets exporters do the rest.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AddEventOptions,
|
|
11
|
+
AISpan,
|
|
12
|
+
Sensitivity,
|
|
13
|
+
SpanAttributes,
|
|
14
|
+
SpanAttributeValue,
|
|
15
|
+
SpanStatus,
|
|
16
|
+
SpanType,
|
|
17
|
+
} from '@graphorin/core';
|
|
18
|
+
|
|
19
|
+
import type { SpanRecord } from '../exporters/types.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional metadata accepted by `setAttribute(...)` to declare the
|
|
23
|
+
* sensitivity of a single attribute. The validator uses the declared
|
|
24
|
+
* tier when deciding whether to drop the attribute.
|
|
25
|
+
*
|
|
26
|
+
* @stable
|
|
27
|
+
*/
|
|
28
|
+
export interface SetAttributeOptions {
|
|
29
|
+
readonly sensitivity?: Sensitivity;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The internal span carries the convenience `setAttribute(...)` method
|
|
34
|
+
* exposed by the tracer surface (per-attribute sensitivity tagging) on
|
|
35
|
+
* top of the standard {@link AISpan} contract.
|
|
36
|
+
*
|
|
37
|
+
* @stable
|
|
38
|
+
*/
|
|
39
|
+
export interface GraphorinSpan<T extends SpanType = SpanType> extends AISpan<T> {
|
|
40
|
+
setAttribute(name: string, value: SpanAttributeValue, opts?: SetAttributeOptions): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @internal - sink invoked when a span ends.
|
|
45
|
+
*/
|
|
46
|
+
export type SpanSink = (record: SpanRecord) => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @internal - parameters passed by the tracer to {@link createSpan}.
|
|
50
|
+
*/
|
|
51
|
+
export interface SpanCreateInput<T extends SpanType = SpanType> {
|
|
52
|
+
readonly type: T;
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly traceId: string;
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly parentId?: string;
|
|
57
|
+
readonly attrs?: SpanAttributes;
|
|
58
|
+
readonly attrSensitivities?: Readonly<Record<string, Sensitivity>>;
|
|
59
|
+
readonly sink: SpanSink;
|
|
60
|
+
readonly now: () => number;
|
|
61
|
+
readonly recordEvent: (name: string) => boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export function createSpan<T extends SpanType>(input: SpanCreateInput<T>): GraphorinSpan<T> {
|
|
68
|
+
const startUnixNano = Math.floor(input.now() * 1_000_000);
|
|
69
|
+
const attributes: Record<string, SpanAttributeValue> = { ...(input.attrs ?? {}) };
|
|
70
|
+
const sensitivities: Record<string, Sensitivity> = { ...(input.attrSensitivities ?? {}) };
|
|
71
|
+
const events: SpanRecord['events'][number][] = [];
|
|
72
|
+
let status: SpanStatus = 'ok';
|
|
73
|
+
let statusMessage: string | undefined;
|
|
74
|
+
let ended = false;
|
|
75
|
+
|
|
76
|
+
const span: GraphorinSpan<T> = {
|
|
77
|
+
type: input.type,
|
|
78
|
+
id: input.id,
|
|
79
|
+
traceId: input.traceId,
|
|
80
|
+
...(input.parentId === undefined ? {} : { parentId: input.parentId }),
|
|
81
|
+
setAttributes(attrs: SpanAttributes): void {
|
|
82
|
+
if (ended) return;
|
|
83
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
84
|
+
attributes[k] = v;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
setAttribute(name: string, value: SpanAttributeValue, opts?: SetAttributeOptions): void {
|
|
88
|
+
if (ended) return;
|
|
89
|
+
attributes[name] = value;
|
|
90
|
+
if (opts?.sensitivity !== undefined) {
|
|
91
|
+
sensitivities[name] = opts.sensitivity;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
addEvent(name: string, attrs?: SpanAttributes, opts?: AddEventOptions): void {
|
|
95
|
+
if (ended) return;
|
|
96
|
+
if (!input.recordEvent(name)) return;
|
|
97
|
+
// W-094: resolve the event default + per-attribute overrides into
|
|
98
|
+
// one map the validation exporter consumes. Untagged attributes
|
|
99
|
+
// stay untagged - default-deny below the floor is preserved.
|
|
100
|
+
const eventSensitivities: Record<string, 'public' | 'internal' | 'secret'> = {};
|
|
101
|
+
if (opts?.sensitivity !== undefined) {
|
|
102
|
+
for (const key of Object.keys(attrs ?? {})) {
|
|
103
|
+
eventSensitivities[key] = opts.sensitivity;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const [key, tier] of Object.entries(opts?.sensitivityByAttribute ?? {})) {
|
|
107
|
+
eventSensitivities[key] = tier;
|
|
108
|
+
}
|
|
109
|
+
events.push({
|
|
110
|
+
name,
|
|
111
|
+
timeUnixNano: Math.floor(input.now() * 1_000_000),
|
|
112
|
+
attributes: Object.freeze({ ...(attrs ?? {}) }) as SpanAttributes,
|
|
113
|
+
...(Object.keys(eventSensitivities).length > 0
|
|
114
|
+
? { sensitivityByAttribute: Object.freeze(eventSensitivities) }
|
|
115
|
+
: {}),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
recordException(err: unknown): void {
|
|
119
|
+
if (ended) return;
|
|
120
|
+
const e = err as { name?: unknown; message?: unknown; stack?: unknown };
|
|
121
|
+
const attrs: SpanAttributes = {
|
|
122
|
+
'exception.type': typeof e?.name === 'string' && e.name.length > 0 ? e.name : 'Error',
|
|
123
|
+
'exception.message': typeof e?.message === 'string' ? e.message : String(err),
|
|
124
|
+
...(typeof e?.stack === 'string' ? { 'exception.stacktrace': e.stack } : {}),
|
|
125
|
+
};
|
|
126
|
+
events.push({
|
|
127
|
+
name: 'exception',
|
|
128
|
+
timeUnixNano: Math.floor(input.now() * 1_000_000),
|
|
129
|
+
attributes: attrs,
|
|
130
|
+
// W-094: the CLASS NAME of an error is safe and load-bearing for
|
|
131
|
+
// out-of-the-box error dashboards - tag it 'public'. Message +
|
|
132
|
+
// stacktrace stay 'internal' (paritous with the un-sanitized
|
|
133
|
+
// statusMessage; the PII patterns still apply at that floor).
|
|
134
|
+
sensitivityByAttribute: Object.freeze({
|
|
135
|
+
'exception.type': 'public',
|
|
136
|
+
'exception.message': 'internal',
|
|
137
|
+
'exception.stacktrace': 'internal',
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
setStatus(s: SpanStatus, message?: string): void {
|
|
142
|
+
if (ended) return;
|
|
143
|
+
status = s;
|
|
144
|
+
if (message !== undefined) statusMessage = message;
|
|
145
|
+
},
|
|
146
|
+
end(): void {
|
|
147
|
+
if (ended) return;
|
|
148
|
+
ended = true;
|
|
149
|
+
const endUnixNano = Math.floor(input.now() * 1_000_000);
|
|
150
|
+
const record: SpanRecord<T> = {
|
|
151
|
+
type: input.type,
|
|
152
|
+
id: input.id,
|
|
153
|
+
traceId: input.traceId,
|
|
154
|
+
...(input.parentId === undefined ? {} : { parentId: input.parentId }),
|
|
155
|
+
name: input.name,
|
|
156
|
+
startUnixNano,
|
|
157
|
+
endUnixNano,
|
|
158
|
+
status,
|
|
159
|
+
...(statusMessage === undefined ? {} : { statusMessage }),
|
|
160
|
+
attributes: Object.freeze({ ...attributes }) as SpanAttributes,
|
|
161
|
+
events: Object.freeze([...events]) as SpanRecord['events'],
|
|
162
|
+
...(Object.keys(sensitivities).length === 0
|
|
163
|
+
? {}
|
|
164
|
+
: {
|
|
165
|
+
sensitivityByAttribute: Object.freeze({ ...sensitivities }) as Readonly<
|
|
166
|
+
Record<string, SpanAttributeValue>
|
|
167
|
+
>,
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
input.sink(record);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return span;
|
|
175
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createTracer(...)` - the public entry point for the observability
|
|
3
|
+
* tracer. Wires together the {@link Sampler}, the
|
|
4
|
+
* {@link RedactionValidator}, and the registered {@link TraceExporter}s.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AISpan,
|
|
11
|
+
Sensitivity,
|
|
12
|
+
SpanAttributeValue,
|
|
13
|
+
SpanType,
|
|
14
|
+
StartSpanOptions,
|
|
15
|
+
Tracer,
|
|
16
|
+
} from '@graphorin/core';
|
|
17
|
+
|
|
18
|
+
import { createConsoleExporter } from '../exporters/console.js';
|
|
19
|
+
import type { SpanRecord, TraceExporter } from '../exporters/types.js';
|
|
20
|
+
import { isValidatedExporter, withValidation } from '../exporters/with-validation.js';
|
|
21
|
+
import { UnvalidatedExporterError } from '../redaction/errors.js';
|
|
22
|
+
import type { RedactionValidatorInstance, RedactionValidatorOptions } from '../redaction/types.js';
|
|
23
|
+
import { createRedactionValidator } from '../redaction/validator.js';
|
|
24
|
+
|
|
25
|
+
import { newSpanId, newTraceId } from './ids.js';
|
|
26
|
+
import { createSampler, type SamplingOptions } from './sampling.js';
|
|
27
|
+
import { createSpan, type GraphorinSpan } from './span.js';
|
|
28
|
+
import { spanNameFor } from './span-names.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Configuration shape consumed by {@link createTracer}.
|
|
32
|
+
*
|
|
33
|
+
* @stable
|
|
34
|
+
*/
|
|
35
|
+
export interface TracerOptions {
|
|
36
|
+
/** Logical service name. Embedded in the OTLP `Resource`. */
|
|
37
|
+
readonly serviceName?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Configured exporters. Each exporter MUST be wrapped via
|
|
40
|
+
* `withValidation(...)` before reaching the tracer - pass them
|
|
41
|
+
* through unwrapped to use the tracer-managed validator (set by
|
|
42
|
+
* `validation`) or pass already-wrapped exporters when you want
|
|
43
|
+
* per-exporter policies.
|
|
44
|
+
*/
|
|
45
|
+
readonly exporters: ReadonlyArray<TraceExporter>;
|
|
46
|
+
/**
|
|
47
|
+
* Tracer-managed validator policy.
|
|
48
|
+
*
|
|
49
|
+
* - `RedactionValidatorOptions` (default): the tracer auto-wraps any
|
|
50
|
+
* un-wrapped exporter with `withValidation(...)` using these
|
|
51
|
+
* options.
|
|
52
|
+
* - `'off'`: NOT recommended. Skips auto-wrap, and the tracer logs a
|
|
53
|
+
* startup WARN to the supplied {@link warnSink}. Pre-wrapped
|
|
54
|
+
* exporters still flow through their validators.
|
|
55
|
+
*
|
|
56
|
+
* @default `{ minTier: 'public', failOnUnredactedSensitive: false }`
|
|
57
|
+
*/
|
|
58
|
+
readonly validation?: RedactionValidatorOptions | 'off';
|
|
59
|
+
/** Sampler configuration. */
|
|
60
|
+
readonly sampling?: SamplingOptions;
|
|
61
|
+
/**
|
|
62
|
+
* Default sensitivity for attributes that omit `setAttribute(_, _, { sensitivity })`.
|
|
63
|
+
* Defaults to `'internal'` per the default-deny non-public posture.
|
|
64
|
+
*/
|
|
65
|
+
readonly defaultAttributeSensitivity?: Sensitivity;
|
|
66
|
+
/**
|
|
67
|
+
* Sink used for startup WARNings. Defaults to `console.warn`.
|
|
68
|
+
*
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
readonly warnSink?: (line: string) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Override the wall clock used for span timestamps. Returns
|
|
74
|
+
* milliseconds-since-epoch as a `number`. Default: `Date.now`.
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
readonly now?: () => number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The {@link createTracer} return value extends the standard
|
|
83
|
+
* {@link Tracer} contract from `@graphorin/core` with introspection
|
|
84
|
+
* helpers (counter snapshots, validator handle).
|
|
85
|
+
*
|
|
86
|
+
* @stable
|
|
87
|
+
*/
|
|
88
|
+
export interface GraphorinTracer extends Tracer {
|
|
89
|
+
/** Service name embedded in the OTLP resource. */
|
|
90
|
+
readonly serviceName: string;
|
|
91
|
+
/**
|
|
92
|
+
* Snapshot of the redaction counters (`droppedTotal`,
|
|
93
|
+
* `droppedByReason`, `matchesByPattern`) maintained by the
|
|
94
|
+
* tracer-managed validator.
|
|
95
|
+
*/
|
|
96
|
+
getMetrics(): import('../redaction/types.js').RedactionCounters;
|
|
97
|
+
/** The tracer-managed validator. `null` when `validation: 'off'`. */
|
|
98
|
+
readonly validator: RedactionValidatorInstance | null;
|
|
99
|
+
/** Force-flush every registered exporter. */
|
|
100
|
+
flush(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DEFAULT_VALIDATION: RedactionValidatorOptions = Object.freeze({
|
|
104
|
+
minTier: 'public',
|
|
105
|
+
failOnUnredactedSensitive: false,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a {@link GraphorinTracer} from the supplied options. Every
|
|
110
|
+
* exporter passed in must already be wrapped via
|
|
111
|
+
* `withValidation(...)` OR `validation` must be set to a concrete
|
|
112
|
+
* options object so the tracer can auto-wrap on your behalf.
|
|
113
|
+
* Registering a raw exporter while `validation: 'off'` triggers an
|
|
114
|
+
* {@link UnvalidatedExporterError} at startup.
|
|
115
|
+
*
|
|
116
|
+
* @stable
|
|
117
|
+
*/
|
|
118
|
+
export function createTracer(opts: TracerOptions): GraphorinTracer {
|
|
119
|
+
const serviceName = opts.serviceName ?? 'graphorin';
|
|
120
|
+
const sampler = createSampler(opts.sampling ?? {});
|
|
121
|
+
const warnSink = opts.warnSink ?? ((line: string) => console.warn(line));
|
|
122
|
+
const now = opts.now ?? (() => Date.now());
|
|
123
|
+
const defaultAttrSensitivity: Sensitivity = opts.defaultAttributeSensitivity ?? 'internal';
|
|
124
|
+
|
|
125
|
+
const validationMode = opts.validation ?? DEFAULT_VALIDATION;
|
|
126
|
+
|
|
127
|
+
let validator: RedactionValidatorInstance | null = null;
|
|
128
|
+
let exporters: TraceExporter[] = [];
|
|
129
|
+
|
|
130
|
+
if (validationMode === 'off') {
|
|
131
|
+
warnSink(
|
|
132
|
+
"[graphorin/observability] WARN: validation: 'off' - exporters are NOT " +
|
|
133
|
+
'auto-wrapped. Every exporter must call withValidation(...) explicitly. ' +
|
|
134
|
+
'See ADR-035: OTLP RedactionValidator + default-deny non-public.',
|
|
135
|
+
);
|
|
136
|
+
for (const exporter of opts.exporters) {
|
|
137
|
+
if (!isValidatedExporter(exporter)) {
|
|
138
|
+
throw new UnvalidatedExporterError(exporter.id);
|
|
139
|
+
}
|
|
140
|
+
exporters.push(exporter);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
validator = createRedactionValidator(validationMode);
|
|
144
|
+
for (const exporter of opts.exporters) {
|
|
145
|
+
exporters.push(
|
|
146
|
+
isValidatedExporter(exporter) ? exporter : withValidation(exporter, { validator }),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (exporters.length === 0) {
|
|
152
|
+
// The tracer is still useful (it emits records to /dev/null) but
|
|
153
|
+
// we want to surface that explicitly so consumers don't think the
|
|
154
|
+
// logger is broken.
|
|
155
|
+
const fallback = withValidation(createConsoleExporter({ id: 'console-fallback' }), {
|
|
156
|
+
validator: validator ?? createRedactionValidator(DEFAULT_VALIDATION),
|
|
157
|
+
});
|
|
158
|
+
warnSink(
|
|
159
|
+
'[graphorin/observability] WARN: createTracer() received zero exporters. ' +
|
|
160
|
+
'Falling back to a sanitized ConsoleExporter so spans are not lost.',
|
|
161
|
+
);
|
|
162
|
+
exporters = [fallback];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// RP-20: sink() exports are fire-and-forget; hold each promise so flush()
|
|
166
|
+
// can await it. Without this a span emitted moments before shutdown is lost
|
|
167
|
+
// when the exporter's closed-guard trips before its export() resolves.
|
|
168
|
+
const inFlight = new Set<Promise<void>>();
|
|
169
|
+
|
|
170
|
+
function sink(record: SpanRecord): void {
|
|
171
|
+
for (const exporter of exporters) {
|
|
172
|
+
const p = Promise.resolve()
|
|
173
|
+
.then(() => exporter.export(record))
|
|
174
|
+
.catch((err: unknown) => {
|
|
175
|
+
warnSink(
|
|
176
|
+
`[graphorin/observability] WARN: exporter ${exporter.id} failed: ${
|
|
177
|
+
err instanceof Error ? err.message : String(err)
|
|
178
|
+
}`,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
inFlight.add(p);
|
|
182
|
+
void p.finally(() => {
|
|
183
|
+
inFlight.delete(p);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function startSpan<T extends SpanType>(spec: StartSpanOptions<T>): AISpan<T> {
|
|
189
|
+
const traceId = spec.parent?.traceId ?? newTraceId();
|
|
190
|
+
const id = newSpanId();
|
|
191
|
+
const parentId = spec.parent?.id;
|
|
192
|
+
// RP-19: a real parent-sampled flag for `'parent-based'` sampling. An
|
|
193
|
+
// unsampled parent is a noop span (no `setAttribute`), so `asGraphorinSpan`
|
|
194
|
+
// returns null - `parentId !== undefined` used to treat that as
|
|
195
|
+
// parent-sampled and record the child as an orphan.
|
|
196
|
+
const parentSampled =
|
|
197
|
+
spec.parent === undefined ? undefined : asGraphorinSpan(spec.parent) !== null;
|
|
198
|
+
const sampled = sampler.shouldSample(spec.type, parentSampled);
|
|
199
|
+
if (!sampled) {
|
|
200
|
+
return noopSpan<T>(spec.type, traceId, id, parentId);
|
|
201
|
+
}
|
|
202
|
+
const sensitivities = readAttrSensitivities(spec.attrs);
|
|
203
|
+
return createSpan<T>({
|
|
204
|
+
type: spec.type,
|
|
205
|
+
name: spanNameFor(spec.type, spec.attrs),
|
|
206
|
+
traceId,
|
|
207
|
+
id,
|
|
208
|
+
...(parentId === undefined ? {} : { parentId }),
|
|
209
|
+
...(spec.attrs === undefined ? {} : { attrs: spec.attrs }),
|
|
210
|
+
...(sensitivities === undefined ? {} : { attrSensitivities: sensitivities }),
|
|
211
|
+
sink,
|
|
212
|
+
now,
|
|
213
|
+
recordEvent: (name) => sampler.shouldRecordEvent(name),
|
|
214
|
+
}) as unknown as AISpan<T>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function span<T extends SpanType, R>(
|
|
218
|
+
spec: StartSpanOptions<T>,
|
|
219
|
+
fn: (s: AISpan<T>) => R | Promise<R>,
|
|
220
|
+
): Promise<R> {
|
|
221
|
+
const s = startSpan(spec);
|
|
222
|
+
try {
|
|
223
|
+
const out = await fn(s);
|
|
224
|
+
s.setStatus('ok');
|
|
225
|
+
return out;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
s.recordException(err);
|
|
228
|
+
s.setStatus('error', err instanceof Error ? err.message : String(err));
|
|
229
|
+
throw err;
|
|
230
|
+
} finally {
|
|
231
|
+
s.end();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function flush(): Promise<void> {
|
|
236
|
+
// RP-20: drain in-flight fire-and-forget exports first, then flush.
|
|
237
|
+
while (inFlight.size > 0) {
|
|
238
|
+
await Promise.all([...inFlight]);
|
|
239
|
+
}
|
|
240
|
+
await Promise.all(exporters.map((e) => e.flush()));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function shutdown(): Promise<void> {
|
|
244
|
+
await flush();
|
|
245
|
+
await Promise.all(exporters.map((e) => e.shutdown()));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
startSpan,
|
|
250
|
+
span,
|
|
251
|
+
flush,
|
|
252
|
+
shutdown,
|
|
253
|
+
serviceName,
|
|
254
|
+
getMetrics: () =>
|
|
255
|
+
validator?.counters() ?? {
|
|
256
|
+
droppedTotal: 0,
|
|
257
|
+
droppedByReason: Object.freeze({}),
|
|
258
|
+
matchesByPattern: Object.freeze({}),
|
|
259
|
+
},
|
|
260
|
+
validator,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function readAttrSensitivities(
|
|
264
|
+
attrs: undefined | Readonly<Record<string, SpanAttributeValue>>,
|
|
265
|
+
): Readonly<Record<string, Sensitivity>> | undefined {
|
|
266
|
+
if (attrs === undefined) return undefined;
|
|
267
|
+
// RP-19: initial attrs that omit an explicit `setAttribute(_, _, {
|
|
268
|
+
// sensitivity })` default to `defaultAttributeSensitivity`. Threading it
|
|
269
|
+
// here makes the knob effective - untagged framework attributes carry the
|
|
270
|
+
// configured tier instead of the validator's hardcoded fallback.
|
|
271
|
+
const out: Record<string, Sensitivity> = {};
|
|
272
|
+
for (const key of Object.keys(attrs)) out[key] = defaultAttrSensitivity;
|
|
273
|
+
return Object.freeze(out);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function noopSpan<T extends SpanType>(
|
|
277
|
+
type: T,
|
|
278
|
+
traceId: string,
|
|
279
|
+
id: string,
|
|
280
|
+
parentId?: string,
|
|
281
|
+
): AISpan<T> {
|
|
282
|
+
const stub: AISpan<T> = {
|
|
283
|
+
type,
|
|
284
|
+
id,
|
|
285
|
+
traceId,
|
|
286
|
+
...(parentId === undefined ? {} : { parentId }),
|
|
287
|
+
setAttributes() {},
|
|
288
|
+
addEvent() {},
|
|
289
|
+
recordException() {},
|
|
290
|
+
setStatus() {},
|
|
291
|
+
end() {},
|
|
292
|
+
};
|
|
293
|
+
return stub;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Returns the underlying {@link GraphorinSpan} when `span` is a Graphorin
|
|
299
|
+
* span. Useful when callers want to reach the per-attribute sensitivity
|
|
300
|
+
* helper from a generic `AISpan<T>`.
|
|
301
|
+
*
|
|
302
|
+
* @stable
|
|
303
|
+
*/
|
|
304
|
+
export function asGraphorinSpan<T extends SpanType>(span: AISpan<T>): GraphorinSpan<T> | null {
|
|
305
|
+
if (typeof (span as Partial<GraphorinSpan<T>>).setAttribute === 'function') {
|
|
306
|
+
return span as GraphorinSpan<T>;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|