@telemetry-dev/otel 0.1.1 → 0.1.2

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/src/session.ts ADDED
@@ -0,0 +1,260 @@
1
+ import {
2
+ type Attributes,
3
+ type Context,
4
+ context as apiContext,
5
+ diag,
6
+ isSpanContextValid,
7
+ type Span,
8
+ type SpanContext,
9
+ type SpanOptions,
10
+ trace,
11
+ TraceFlags,
12
+ type Tracer,
13
+ type TracerProvider,
14
+ } from "@opentelemetry/api";
15
+ import { getNumberFromEnv, getStringFromEnv } from "@opentelemetry/core";
16
+ import {
17
+ AlwaysOffSampler,
18
+ AlwaysOnSampler,
19
+ ParentBasedSampler,
20
+ type Sampler,
21
+ TraceIdRatioBasedSampler,
22
+ } from "@opentelemetry/sdk-trace-base";
23
+
24
+ import { propagatedFromContext } from "./context.ts";
25
+ import { reportError } from "./debug.ts";
26
+
27
+ const SESSION_PARENTS = new WeakMap<Span, Context>();
28
+
29
+ /**
30
+ * Install on the provider used with withSessionParent or sessionRootTracerProvider.
31
+ * Pass the provider's configured sampler explicitly; OTel cannot read it back from a provider.
32
+ * Without an argument, uses OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG defaults.
33
+ */
34
+ export function sessionSampler(inner: Sampler = samplerFromEnv()): Sampler {
35
+ return {
36
+ shouldSample(ctx, traceId, name, kind, attributes, links) {
37
+ const parent = trace.getSpan(ctx);
38
+ const original = parent && SESSION_PARENTS.get(parent);
39
+ if (original) {
40
+ const span = trace.getSpan(original);
41
+ ctx = span ? trace.setSpan(ctx, span) : trace.deleteSpan(ctx);
42
+ }
43
+ return inner.shouldSample(ctx, traceId, name, kind, attributes, links);
44
+ },
45
+ toString: () => `SessionSampler{${inner.toString()}}`,
46
+ };
47
+ }
48
+
49
+ function samplerFromEnv(): Sampler {
50
+ const name = getStringFromEnv("OTEL_TRACES_SAMPLER") ?? "parentbased_always_on";
51
+ let root: Sampler;
52
+ switch (name) {
53
+ case "always_off":
54
+ case "parentbased_always_off":
55
+ root = new AlwaysOffSampler();
56
+ break;
57
+ case "traceidratio":
58
+ case "parentbased_traceidratio": {
59
+ const ratio = getNumberFromEnv("OTEL_TRACES_SAMPLER_ARG");
60
+ const valid = ratio !== undefined && Number.isFinite(ratio) && ratio >= 0 && ratio <= 1;
61
+ if (!valid) diag.error("Invalid OTEL_TRACES_SAMPLER_ARG; using 1.");
62
+ root = new TraceIdRatioBasedSampler(valid ? ratio : 1);
63
+ break;
64
+ }
65
+ case "always_on":
66
+ case "parentbased_always_on":
67
+ root = new AlwaysOnSampler();
68
+ break;
69
+ default:
70
+ diag.error(`Invalid OTEL_TRACES_SAMPLER "${name}"; using parentbased_always_on.`);
71
+ return new ParentBasedSampler({ root: new AlwaysOnSampler() });
72
+ }
73
+ return name.startsWith("parentbased_") ? new ParentBasedSampler({ root }) : root;
74
+ }
75
+ /**
76
+ * Deterministic remote parent for a session: every root span of the session joins one trace
77
+ * (`traceId = SHA-256(apiKey ‖ 0x00 ‖ sessionId)[0:16]`, parent `spanId = digest[16:24]`).
78
+ * The API key is part of the input so two projects with the same session id never share a trace
79
+ * id. No span is ever emitted for the parent. Its flags are not a sampling decision:
80
+ * use withSessionParent and install sessionSampler on the provider. Python mirrors these IDs.
81
+ */
82
+ export function sessionSpanContext(apiKey: string | undefined, sessionId: string): SpanContext {
83
+ const digest = sha256(new TextEncoder().encode(`${apiKey ?? ""}\0${sessionId}`));
84
+ return {
85
+ traceId: hex(digest.subarray(0, 16)),
86
+ spanId: hex(digest.subarray(16, 24)),
87
+ traceFlags: TraceFlags.NONE,
88
+ isRemote: true,
89
+ };
90
+ }
91
+
92
+ /** Returns the session id when a span must start a new turn in the session trace. */
93
+ export type SessionRootOf = (name: string, attributes: Attributes) => string | undefined;
94
+
95
+ /**
96
+ * Wraps a provider so that spans `sessionRootOf` recognizes are parented under the session
97
+ * parent even when a parent is active. Frameworks that run each turn inside their own
98
+ * engine span (eve's "workflow" scope) otherwise give every turn its own trace.
99
+ * The inner provider MUST use sessionSampler(configuredSampler) to retain sampling policy.
100
+ */
101
+ export function sessionRootTracerProvider(
102
+ inner: TracerProvider,
103
+ apiKey: string | undefined,
104
+ sessionRootOf: SessionRootOf,
105
+ onError?: (error: Error) => void,
106
+ ): TracerProvider {
107
+ const reparent = (name: string, options: SpanOptions | undefined, ctx: Context): Context => {
108
+ if (!apiKey) return ctx;
109
+ try {
110
+ const sessionId = sessionRootOf(name, options?.attributes ?? {});
111
+ return sessionId
112
+ ? setSessionParent(options?.root ? trace.deleteSpan(ctx) : ctx, sessionId, apiKey)
113
+ : ctx;
114
+ } catch (error) {
115
+ reportError(onError, error);
116
+ return ctx;
117
+ }
118
+ };
119
+ return {
120
+ getTracer(name, version, options): Tracer {
121
+ const tracer = inner.getTracer(name, version, options);
122
+ return {
123
+ startSpan: (spanName, spanOptions, ctx = apiContext.active()) => {
124
+ const parent = reparent(spanName, spanOptions, ctx);
125
+ return tracer.startSpan(
126
+ spanName,
127
+ parent !== ctx && spanOptions?.root ? { ...spanOptions, root: false } : spanOptions,
128
+ parent,
129
+ );
130
+ },
131
+ startActiveSpan<F extends (span: Span) => unknown>(
132
+ spanName: string,
133
+ ...args: unknown[]
134
+ ): ReturnType<F> {
135
+ const fn = args.at(-1) as F;
136
+ const spanOptions = args.length > 1 ? (args[0] as SpanOptions) : undefined;
137
+ const ctx = args.length > 2 ? (args[1] as Context) : apiContext.active();
138
+ const parent = reparent(spanName, spanOptions, ctx);
139
+ return tracer.startActiveSpan(
140
+ spanName,
141
+ parent !== ctx && spanOptions?.root
142
+ ? { ...spanOptions, root: false }
143
+ : (spanOptions ?? {}),
144
+ parent,
145
+ fn,
146
+ );
147
+ },
148
+ };
149
+ },
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Parent a would-be root under the session; nested spans keep their real parent.
155
+ * Requires sessionSampler(configuredSampler) on the provider that starts the span.
156
+ */
157
+ export function withSessionParent(
158
+ ctx: Context,
159
+ sessionId: string | undefined,
160
+ apiKey: string | undefined,
161
+ ): Context {
162
+ if (!apiKey || !sessionId) return ctx;
163
+ const current = trace.getSpanContext(ctx);
164
+ if (current && isSpanContextValid(current)) return ctx;
165
+ return setSessionParent(ctx, sessionId, apiKey);
166
+ }
167
+
168
+ function setSessionParent(ctx: Context, sessionId: string, apiKey: string): Context {
169
+ const parent = trace.wrapSpanContext({
170
+ ...sessionSpanContext(apiKey, sessionId),
171
+ traceState: trace.getSpanContext(ctx)?.traceState,
172
+ });
173
+ SESSION_PARENTS.set(parent, ctx);
174
+ return trace.setSpan(ctx, parent);
175
+ }
176
+
177
+ export function sessionIdOf(ctx: Context, attributes?: Attributes): string | undefined {
178
+ const explicit = attributes?.["gen_ai.conversation.id"];
179
+ if (typeof explicit === "string") return explicit;
180
+ const propagated = propagatedFromContext(ctx)?.["gen_ai.conversation.id"];
181
+ return typeof propagated === "string" ? propagated : undefined;
182
+ }
183
+
184
+ function hex(bytes: Uint8Array): string {
185
+ let s = "";
186
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
187
+ return s;
188
+ }
189
+
190
+ const K = new Uint32Array([
191
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
192
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
193
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
194
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
195
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
196
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
197
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
198
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
199
+ ]);
200
+
201
+ const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n));
202
+
203
+ /** FIPS 180-4 SHA-256 in plain JS: the package must stay synchronous and runtime-neutral. */
204
+ export function sha256(bytes: Uint8Array): Uint8Array {
205
+ const h = new Uint32Array([
206
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
207
+ ]);
208
+ const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
209
+ padded.set(bytes);
210
+ padded[bytes.length] = 0x80;
211
+ const view = new DataView(padded.buffer);
212
+ const bitLen = bytes.length * 8;
213
+ view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000));
214
+ view.setUint32(padded.length - 4, bitLen >>> 0);
215
+ const w = new Uint32Array(64);
216
+ for (let off = 0; off < padded.length; off += 64) {
217
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
218
+ for (let i = 16; i < 64; i++) {
219
+ const x = w[i - 15]!;
220
+ const y = w[i - 2]!;
221
+ const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3);
222
+ const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10);
223
+ w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0;
224
+ }
225
+ let a = h[0]!;
226
+ let b = h[1]!;
227
+ let c = h[2]!;
228
+ let d = h[3]!;
229
+ let e = h[4]!;
230
+ let f = h[5]!;
231
+ let g = h[6]!;
232
+ let hh = h[7]!;
233
+ for (let i = 0; i < 64; i++) {
234
+ const t1 =
235
+ (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i]! + w[i]!) >>>
236
+ 0;
237
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
238
+ hh = g;
239
+ g = f;
240
+ f = e;
241
+ e = (d + t1) >>> 0;
242
+ d = c;
243
+ c = b;
244
+ b = a;
245
+ a = (t1 + t2) >>> 0;
246
+ }
247
+ h[0] = (h[0]! + a) >>> 0;
248
+ h[1] = (h[1]! + b) >>> 0;
249
+ h[2] = (h[2]! + c) >>> 0;
250
+ h[3] = (h[3]! + d) >>> 0;
251
+ h[4] = (h[4]! + e) >>> 0;
252
+ h[5] = (h[5]! + f) >>> 0;
253
+ h[6] = (h[6]! + g) >>> 0;
254
+ h[7] = (h[7]! + hh) >>> 0;
255
+ }
256
+ const out = new Uint8Array(32);
257
+ const outView = new DataView(out.buffer);
258
+ for (let i = 0; i < 8; i++) outView.setUint32(i * 4, h[i]!);
259
+ return out;
260
+ }