@narumitw/pi-langfuse 0.50.4 → 0.50.6
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/README.md +46 -61
- package/dist/chunks/{runtime-247EMSG3.js → runtime-X75FRK2E.js} +2 -5
- package/dist/chunks/runtime-X75FRK2E.js.map +7 -0
- package/dist/index.ts +12 -48
- package/dist/index.ts.map +2 -2
- package/package.json +55 -59
- package/src/config.ts +211 -232
- package/src/langfuse.ts +484 -536
- package/src/runtime.ts +201 -218
- package/src/sanitizer.ts +112 -117
- package/src/tracing.ts +787 -840
- package/dist/chunks/runtime-247EMSG3.js.map +0 -7
package/src/runtime.ts
CHANGED
|
@@ -1,281 +1,264 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type LangfuseObservation,
|
|
5
|
+
type LangfuseObservationAttributes,
|
|
6
|
+
LangfuseOtelSpanAttributes,
|
|
7
|
+
setLangfuseTracerProvider,
|
|
8
|
+
startObservation,
|
|
9
9
|
} from "@langfuse/tracing";
|
|
10
10
|
import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
11
11
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
12
12
|
import type { LangfuseConfig } from "./config.js";
|
|
13
|
-
import type {
|
|
14
|
-
Observation,
|
|
15
|
-
ObservationAttributes,
|
|
16
|
-
ObservationType,
|
|
17
|
-
TraceBackend,
|
|
18
|
-
} from "./tracing.js";
|
|
13
|
+
import type { Observation, ObservationAttributes, ObservationType, TraceBackend } from "./tracing.js";
|
|
19
14
|
|
|
20
15
|
const RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v1");
|
|
21
16
|
|
|
22
17
|
interface SharedRuntime {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
fingerprint: string;
|
|
19
|
+
backend: ProductionTraceBackend;
|
|
20
|
+
shutdown: boolean;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
type GlobalWithRuntime = typeof globalThis & {
|
|
29
|
-
|
|
24
|
+
[RUNTIME_KEY]?: Promise<SharedRuntime>;
|
|
30
25
|
};
|
|
31
26
|
|
|
32
27
|
export interface RuntimeFactories {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
createProcessor(config: LangfuseConfig): SpanProcessor;
|
|
29
|
+
createProvider(processor: SpanProcessor): NodeTracerProvider;
|
|
30
|
+
selectProvider(provider: NodeTracerProvider): void;
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
class ProductionObservation implements Observation {
|
|
39
|
-
|
|
34
|
+
constructor(readonly native: LangfuseObservation) {}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
update(attributes: ObservationAttributes): Observation {
|
|
37
|
+
const { sessionId, userId, ...observationAttributes } = attributes;
|
|
38
|
+
this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
|
|
39
|
+
applySessionId(this.native, sessionId);
|
|
40
|
+
applyUserId(this.native, userId);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
return this;
|
|
72
|
-
}
|
|
44
|
+
updateTrace(attributes: ObservationAttributes): Observation {
|
|
45
|
+
const { input, output, metadata, name, sessionId, userId, tags, version } = attributes;
|
|
46
|
+
if (input !== undefined || output !== undefined) {
|
|
47
|
+
this.native.setTraceIO({ input, output });
|
|
48
|
+
}
|
|
49
|
+
if (name !== undefined) {
|
|
50
|
+
this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_NAME, name);
|
|
51
|
+
}
|
|
52
|
+
applySessionId(this.native, sessionId);
|
|
53
|
+
applyUserId(this.native, userId);
|
|
54
|
+
if (tags !== undefined) {
|
|
55
|
+
this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_TAGS, tags);
|
|
56
|
+
}
|
|
57
|
+
if (version !== undefined) {
|
|
58
|
+
this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.VERSION, version);
|
|
59
|
+
}
|
|
60
|
+
for (const [key, value] of Object.entries(metadata ?? {})) {
|
|
61
|
+
this.native.otelSpan.setAttribute(`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`, String(value));
|
|
62
|
+
}
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
end(endTime?: number): Observation {
|
|
67
|
+
this.native.end(endTime);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
class ProductionTraceBackend implements TraceBackend {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
constructor(
|
|
74
|
+
private readonly provider: NodeTracerProvider,
|
|
75
|
+
private readonly processor: SpanProcessor,
|
|
76
|
+
) {}
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
78
|
+
start(
|
|
79
|
+
name: string,
|
|
80
|
+
attributes: ObservationAttributes,
|
|
81
|
+
options: { asType: ObservationType; parent?: Observation },
|
|
82
|
+
): Observation {
|
|
83
|
+
const { sessionId, userId, ...observationAttributes } = attributes;
|
|
84
|
+
const parent = options.parent;
|
|
85
|
+
const native =
|
|
86
|
+
parent instanceof ProductionObservation
|
|
87
|
+
? startChild(parent.native, name, observationAttributes, options.asType)
|
|
88
|
+
: startRoot(name, observationAttributes, options.asType);
|
|
89
|
+
applySessionId(native, sessionId);
|
|
90
|
+
applyUserId(native, userId);
|
|
91
|
+
return new ProductionObservation(native);
|
|
92
|
+
}
|
|
101
93
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
async forceFlush(): Promise<void> {
|
|
95
|
+
await this.processor.forceFlush();
|
|
96
|
+
}
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
async shutdown(): Promise<void> {
|
|
99
|
+
await this.provider.shutdown();
|
|
100
|
+
}
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
export async function createProductionBackend(
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
config: LangfuseConfig,
|
|
105
|
+
factoryOverrides: Partial<RuntimeFactories> = {},
|
|
114
106
|
): Promise<TraceBackend> {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
107
|
+
const globalRuntime = globalThis as GlobalWithRuntime;
|
|
108
|
+
const fingerprint = configFingerprint(config);
|
|
109
|
+
const existing = globalRuntime[RUNTIME_KEY];
|
|
110
|
+
if (existing) {
|
|
111
|
+
const runtime = await existing;
|
|
112
|
+
if (runtime.shutdown) {
|
|
113
|
+
throw new Error("Langfuse tracing was already shut down; restart Pi to enable it again.");
|
|
114
|
+
}
|
|
115
|
+
if (runtime.fingerprint !== fingerprint) {
|
|
116
|
+
throw new Error("Langfuse configuration changed; restart Pi to apply the new credentials.");
|
|
117
|
+
}
|
|
118
|
+
return runtime.backend;
|
|
119
|
+
}
|
|
128
120
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
const factories = { ...defaultFactories, ...factoryOverrides };
|
|
122
|
+
const initializing = initializeRuntime(config, fingerprint, factories);
|
|
123
|
+
globalRuntime[RUNTIME_KEY] = initializing;
|
|
124
|
+
try {
|
|
125
|
+
return (await initializing).backend;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
delete globalRuntime[RUNTIME_KEY];
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
138
130
|
}
|
|
139
131
|
|
|
140
132
|
async function initializeRuntime(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
config: LangfuseConfig,
|
|
134
|
+
fingerprint: string,
|
|
135
|
+
factories: RuntimeFactories,
|
|
144
136
|
): Promise<SharedRuntime> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
137
|
+
const processor = factories.createProcessor(config);
|
|
138
|
+
let provider: NodeTracerProvider | undefined;
|
|
139
|
+
try {
|
|
140
|
+
provider = factories.createProvider(processor);
|
|
141
|
+
factories.selectProvider(provider);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
await (provider?.shutdown() ?? processor.shutdown()).catch(() => undefined);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const backend = new ProductionTraceBackend(provider, processor);
|
|
147
|
+
const runtime: SharedRuntime = { fingerprint, backend, shutdown: false };
|
|
148
|
+
const originalShutdown = backend.shutdown.bind(backend);
|
|
149
|
+
backend.shutdown = async () => {
|
|
150
|
+
if (runtime.shutdown) return;
|
|
151
|
+
runtime.shutdown = true;
|
|
152
|
+
await originalShutdown();
|
|
153
|
+
};
|
|
154
|
+
return runtime;
|
|
163
155
|
}
|
|
164
156
|
|
|
165
157
|
const defaultFactories: RuntimeFactories = {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
selectProvider: setLangfuseTracerProvider,
|
|
158
|
+
createProcessor: (config) => {
|
|
159
|
+
const secrets = [config.secretKey, config.publicKey];
|
|
160
|
+
return new LangfuseSpanProcessor({
|
|
161
|
+
publicKey: config.publicKey,
|
|
162
|
+
secretKey: config.secretKey,
|
|
163
|
+
baseUrl: config.baseUrl,
|
|
164
|
+
environment: config.environment ?? "",
|
|
165
|
+
release: config.release ?? "",
|
|
166
|
+
flushAt: 512,
|
|
167
|
+
flushInterval: 5,
|
|
168
|
+
timeout: 5,
|
|
169
|
+
mask: ({ data }) => maskSecrets(data, secrets),
|
|
170
|
+
shouldExportSpan: ({ otelSpan }) => typeof otelSpan.attributes["langfuse.observation.type"] === "string",
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
createProvider: (processor) => new NodeTracerProvider({ spanProcessors: [processor] }),
|
|
174
|
+
selectProvider: setLangfuseTracerProvider,
|
|
184
175
|
};
|
|
185
176
|
|
|
186
177
|
function applySessionId(observation: LangfuseObservation, sessionId: string | undefined): void {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
if (sessionId !== undefined) {
|
|
179
|
+
observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
|
|
180
|
+
}
|
|
190
181
|
}
|
|
191
182
|
|
|
192
183
|
function applyUserId(observation: LangfuseObservation, userId: string | undefined): void {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
184
|
+
if (userId !== undefined) {
|
|
185
|
+
observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
|
|
186
|
+
}
|
|
196
187
|
}
|
|
197
188
|
|
|
198
|
-
function startRoot(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (type === "tool") {
|
|
212
|
-
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
|
|
213
|
-
}
|
|
214
|
-
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
|
|
189
|
+
function startRoot(name: string, attributes: ObservationAttributes, type: ObservationType): LangfuseObservation {
|
|
190
|
+
if (type === "agent") {
|
|
191
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
|
|
192
|
+
}
|
|
193
|
+
if (type === "generation") {
|
|
194
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
195
|
+
asType: "generation",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (type === "tool") {
|
|
199
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
|
|
200
|
+
}
|
|
201
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
|
|
215
202
|
}
|
|
216
203
|
|
|
217
204
|
function startChild(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
205
|
+
parent: LangfuseObservation,
|
|
206
|
+
name: string,
|
|
207
|
+
attributes: ObservationAttributes,
|
|
208
|
+
type: ObservationType,
|
|
222
209
|
): LangfuseObservation {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
210
|
+
if (type === "agent") {
|
|
211
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
212
|
+
asType: "agent",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (type === "generation") {
|
|
216
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
217
|
+
asType: "generation",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (type === "tool") {
|
|
221
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
222
|
+
asType: "tool",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
226
|
+
asType: "span",
|
|
227
|
+
});
|
|
241
228
|
}
|
|
242
229
|
|
|
243
230
|
function configFingerprint(config: LangfuseConfig): string {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
231
|
+
return createHash("sha256")
|
|
232
|
+
.update(
|
|
233
|
+
`${config.publicKey}\0${config.secretKey}\0${config.baseUrl}\0${config.environment ?? ""}\0${config.release ?? ""}`,
|
|
234
|
+
)
|
|
235
|
+
.digest("hex");
|
|
249
236
|
}
|
|
250
237
|
|
|
251
238
|
export function maskSecrets(data: unknown, secrets: readonly string[]): unknown {
|
|
252
|
-
|
|
239
|
+
return maskSecretValue(data, secrets, new WeakSet<object>());
|
|
253
240
|
}
|
|
254
241
|
|
|
255
|
-
function maskSecretValue(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return masked;
|
|
266
|
-
}
|
|
267
|
-
if (!data || typeof data !== "object") return data;
|
|
268
|
-
if (active.has(data)) return "[circular]";
|
|
242
|
+
function maskSecretValue(data: unknown, secrets: readonly string[], active: WeakSet<object>): unknown {
|
|
243
|
+
if (typeof data === "string") {
|
|
244
|
+
let masked = data.replace(/\b(?:sk|pk)-lf-[A-Za-z0-9_-]+\b/g, "[LANGFUSE_KEY_REDACTED]");
|
|
245
|
+
for (const secret of secrets) {
|
|
246
|
+
if (secret) masked = masked.replaceAll(secret, "[LANGFUSE_KEY_REDACTED]");
|
|
247
|
+
}
|
|
248
|
+
return masked;
|
|
249
|
+
}
|
|
250
|
+
if (!data || typeof data !== "object") return data;
|
|
251
|
+
if (active.has(data)) return "[circular]";
|
|
269
252
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
253
|
+
active.add(data);
|
|
254
|
+
try {
|
|
255
|
+
if (Array.isArray(data)) {
|
|
256
|
+
return data.map((item) => maskSecretValue(item, secrets, active));
|
|
257
|
+
}
|
|
258
|
+
return Object.fromEntries(
|
|
259
|
+
Object.entries(data).map(([key, value]) => [key, maskSecretValue(value, secrets, active)]),
|
|
260
|
+
);
|
|
261
|
+
} finally {
|
|
262
|
+
active.delete(data);
|
|
263
|
+
}
|
|
281
264
|
}
|