pi-langfuse 1.5.4 → 1.5.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 +18 -0
- package/README_CN.md +18 -0
- package/index.ts +8 -1
- package/package.json +3 -1
- package/src/commands.ts +144 -5
- package/src/config.ts +28 -3
- package/src/langfuse.ts +84 -22
package/README.md
CHANGED
|
@@ -57,6 +57,14 @@ To run setup again:
|
|
|
57
57
|
/langfuse-setup
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
To inspect the active configuration without exposing secrets:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
/langfuse-status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The status command reports the config source, host, masked public key, capture policy, active-run state, config path, and last runtime error.
|
|
67
|
+
|
|
60
68
|
### Method 2: Environment variables
|
|
61
69
|
|
|
62
70
|
Set these before starting Pi:
|
|
@@ -124,6 +132,7 @@ Fine-grained capture flags can also be persisted:
|
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
> **Security**: Keep `~/.pi/agent/pi-langfuse/config.json` private. Never commit API keys to version control.
|
|
135
|
+
> When the extension writes this file itself, it creates the config directory with `0700` permissions and the file with `0600` permissions where the host filesystem supports POSIX modes.
|
|
127
136
|
|
|
128
137
|
## Verify the Extension
|
|
129
138
|
|
|
@@ -135,6 +144,14 @@ pi list
|
|
|
135
144
|
|
|
136
145
|
`pi-langfuse` should appear in the installed package list.
|
|
137
146
|
|
|
147
|
+
To verify the Langfuse host and API keys from inside Pi, run:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
/langfuse-test
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This command makes a timeout-bounded authenticated request to Langfuse and, if it succeeds, sends a small test trace.
|
|
154
|
+
|
|
138
155
|
## What Appears in Langfuse
|
|
139
156
|
|
|
140
157
|
- Each Pi session gets its own Langfuse session ID.
|
|
@@ -202,6 +219,7 @@ The extension must not upload raw absolute local paths, credentialed remotes, to
|
|
|
202
219
|
### No traces appearing?
|
|
203
220
|
|
|
204
221
|
- Verify the API keys and run `/langfuse-setup` again if needed.
|
|
222
|
+
- Run `/langfuse-status` to confirm the loaded host, config source, privacy mode, and last runtime error.
|
|
205
223
|
- Confirm the Langfuse project is active and accepts writes.
|
|
206
224
|
- Confirm the keys have write permission.
|
|
207
225
|
- Look for `📊 Langfuse:` log messages in Pi output.
|
package/README_CN.md
CHANGED
|
@@ -57,6 +57,14 @@ Langfuse API 密钥可在 **Langfuse Cloud** -> **Settings** -> **API Keys** 中
|
|
|
57
57
|
/langfuse-setup
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
如需查看当前配置状态且不泄漏密钥:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
/langfuse-status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
状态命令会显示配置来源、主机地址、脱敏后的公钥、采集策略、是否有活跃运行、配置文件路径,以及最近一次运行时错误。
|
|
67
|
+
|
|
60
68
|
### 方式 2:环境变量
|
|
61
69
|
|
|
62
70
|
在启动 Pi 前设置:
|
|
@@ -124,6 +132,7 @@ export LANGFUSE_CAPTURE_CWD=false
|
|
|
124
132
|
```
|
|
125
133
|
|
|
126
134
|
> **安全提醒**:`~/.pi/agent/pi-langfuse/config.json` 包含敏感信息,不应提交到版本控制。
|
|
135
|
+
> 扩展自行写入该文件时,会在支持 POSIX 权限的文件系统上使用 `0700` 创建配置目录,并使用 `0600` 写入配置文件。
|
|
127
136
|
|
|
128
137
|
## 验证扩展是否已加载
|
|
129
138
|
|
|
@@ -135,6 +144,14 @@ pi list
|
|
|
135
144
|
|
|
136
145
|
已安装包列表中应出现 `pi-langfuse`。
|
|
137
146
|
|
|
147
|
+
如需在 Pi 内验证 Langfuse 主机地址和 API key:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
/langfuse-test
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
该命令会先发起一次带超时的认证请求;认证通过后,再发送一条小的测试 trace。
|
|
154
|
+
|
|
138
155
|
## 在 Langfuse 中会看到什么
|
|
139
156
|
|
|
140
157
|
- 每个 Pi 会话对应一个独立的 Langfuse session ID。
|
|
@@ -155,6 +172,7 @@ pi list
|
|
|
155
172
|
### 没有看到 trace
|
|
156
173
|
|
|
157
174
|
- 先检查 API 密钥是否正确,必要时重新执行 `/langfuse-setup`。
|
|
175
|
+
- 执行 `/langfuse-status`,确认当前加载的主机、配置来源、隐私模式和最近一次运行时错误。
|
|
158
176
|
- 确认 Langfuse 项目处于可写状态。
|
|
159
177
|
- 确认密钥具备写权限。
|
|
160
178
|
- 在 Pi 输出中查找 `📊 Langfuse:` 日志。
|
package/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import { state, resetRunState, runWithSession, setCurrentSession } from "./src/state.js";
|
|
14
14
|
import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
|
|
15
15
|
import { shutdownRuntime } from "./src/langfuse.js";
|
|
16
|
-
import { handleLangfusePrivacyCommand, handleLangfuseTestCommand } from "./src/commands.js";
|
|
16
|
+
import { handleLangfusePrivacyCommand, handleLangfuseStatusCommand, handleLangfuseTestCommand } from "./src/commands.js";
|
|
17
17
|
import { getMessageFromEvent, extractAssistantOutput, getCapturePolicy } from "./src/utils.js";
|
|
18
18
|
import { applyCapturePolicy } from "./src/capture-policy.js";
|
|
19
19
|
import { startAgentRun, finishAgentRun } from "./src/handlers/agent.js";
|
|
@@ -60,6 +60,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
pi.registerCommand("langfuse-status", {
|
|
64
|
+
description: "Show Langfuse configuration and runtime status",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
await handleLangfuseStatusCommand(String(args ?? ""), ctx);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
63
70
|
pi.registerCommand("langfuse-privacy", {
|
|
64
71
|
description: "View or set Langfuse telemetry privacy preset",
|
|
65
72
|
handler: async (args, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-langfuse",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.6",
|
|
4
4
|
"description": "Langfuse extension for Pi coding agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"@langfuse/client": "^5.3.0",
|
|
47
47
|
"@langfuse/otel": "^5.3.0",
|
|
48
48
|
"@langfuse/tracing": "^5.3.0",
|
|
49
|
+
"@opentelemetry/api": "^1.9.0",
|
|
50
|
+
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
49
51
|
"@opentelemetry/sdk-node": "^0.218.0",
|
|
50
52
|
"@opentelemetry/sdk-trace-base": "^2.0.1"
|
|
51
53
|
},
|
package/src/commands.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
3
|
import { CONFIG_PATH } from "./constants.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
loadConfig,
|
|
6
|
+
loadConfigFromEnv,
|
|
7
|
+
loadConfigFromFile,
|
|
8
|
+
sanitizeConfigForLog,
|
|
9
|
+
saveConfig,
|
|
10
|
+
ensureConfig,
|
|
11
|
+
} from "./config.js";
|
|
5
12
|
import { createCapturePolicy, type PrivacyPreset, type CapturePolicy } from "./capture-policy.js";
|
|
6
|
-
import { getRuntime, forceShutdownRuntime as shutdownLangfuseRuntime } from "./langfuse.js";
|
|
13
|
+
import { getRuntime, getLastRuntimeError, forceShutdownRuntime as shutdownLangfuseRuntime } from "./langfuse.js";
|
|
7
14
|
import { state } from "./state.js";
|
|
8
|
-
import type { LangfuseRuntime } from "./types.js";
|
|
15
|
+
import type { Config, LangfuseRuntime } from "./types.js";
|
|
9
16
|
|
|
10
17
|
const PRIVACY_PRESETS = ["metadata-only", "prompts-only", "conversations", "full-debug"] as const;
|
|
11
18
|
|
|
@@ -21,6 +28,13 @@ interface CommandDeps {
|
|
|
21
28
|
configPath?: string;
|
|
22
29
|
getRuntime?: () => Promise<LangfuseRuntime>;
|
|
23
30
|
forceShutdownRuntime?: () => Promise<void>;
|
|
31
|
+
env?: Record<string, string | undefined>;
|
|
32
|
+
checkConnectivity?: (config: Config) => Promise<ConnectivityResult>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ConnectivityResult {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
message: string;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
function notify(ctx: CommandContextLike, message: string, level: "info" | "warning" | "error" = "info") {
|
|
@@ -128,6 +142,10 @@ function describePolicy(policy: CapturePolicy) {
|
|
|
128
142
|
].join("\n");
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
function flag(value: boolean): "on" | "off" {
|
|
146
|
+
return value ? "on" : "off";
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
function readPersistedConfig(path: string) {
|
|
132
150
|
if (!existsSync(path)) {
|
|
133
151
|
return {};
|
|
@@ -148,6 +166,108 @@ function hasActiveAgentObservation() {
|
|
|
148
166
|
return false;
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
function configSource(env: Record<string, string | undefined>, configPath: string): string {
|
|
170
|
+
const fileConfig = loadConfigFromFile(configPath, env);
|
|
171
|
+
const envConfig = loadConfigFromEnv(env);
|
|
172
|
+
if (fileConfig && envConfig) {
|
|
173
|
+
return "config file (env capture flags may override saved privacy)";
|
|
174
|
+
}
|
|
175
|
+
if (fileConfig) {
|
|
176
|
+
return "config file";
|
|
177
|
+
}
|
|
178
|
+
if (envConfig) {
|
|
179
|
+
return "environment variables";
|
|
180
|
+
}
|
|
181
|
+
return "none";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function lastErrorSummary() {
|
|
185
|
+
const lastError = getLastRuntimeError();
|
|
186
|
+
if (!lastError) {
|
|
187
|
+
return "none";
|
|
188
|
+
}
|
|
189
|
+
return `${lastError.scope}: ${lastError.message} (${lastError.timestamp.toISOString()})`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatStatus(configPath: string, env: Record<string, string | undefined>) {
|
|
193
|
+
const config = loadConfig(env, configPath);
|
|
194
|
+
if (!config) {
|
|
195
|
+
return [
|
|
196
|
+
"pi-langfuse status:",
|
|
197
|
+
"State: not configured",
|
|
198
|
+
`Config file: ${configPath}`,
|
|
199
|
+
"Action: run /langfuse-setup or set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY",
|
|
200
|
+
`Last error: ${lastErrorSummary()}`,
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const safeConfig = sanitizeConfigForLog(config);
|
|
205
|
+
const policy = config.capturePolicy ?? createCapturePolicy(env);
|
|
206
|
+
return [
|
|
207
|
+
"pi-langfuse status:",
|
|
208
|
+
"State: configured",
|
|
209
|
+
`Source: ${configSource(env, configPath)}`,
|
|
210
|
+
`Host: ${safeConfig?.host ?? config.host}`,
|
|
211
|
+
`Public key: ${safeConfig?.publicKey ?? "[REDACTED_SECRET]"}`,
|
|
212
|
+
`Config file: ${configPath}`,
|
|
213
|
+
`Privacy preset: ${inferPreset(policy)}`,
|
|
214
|
+
"Capture:",
|
|
215
|
+
` inputs: ${flag(policy.captureInputs)}`,
|
|
216
|
+
` outputs: ${flag(policy.captureOutputs)}`,
|
|
217
|
+
` tool IO: ${flag(policy.captureToolIo)}`,
|
|
218
|
+
` system prompt: ${flag(policy.captureSystemPrompt)}`,
|
|
219
|
+
` cwd: ${flag(policy.captureCwd)}`,
|
|
220
|
+
`Active run: ${hasActiveAgentObservation() ? "yes" : "no"}`,
|
|
221
|
+
`Last error: ${lastErrorSummary()}`,
|
|
222
|
+
].join("\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function checkLangfuseConnectivity(config: Config): Promise<ConnectivityResult> {
|
|
226
|
+
const host = config.host.replace(/\/+$/, "");
|
|
227
|
+
const auth = Buffer.from(`${config.publicKey}:${config.secretKey}`).toString("base64");
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const response = await fetch(`${host}/api/public/projects`, {
|
|
231
|
+
headers: {
|
|
232
|
+
Authorization: `Basic ${auth}`,
|
|
233
|
+
},
|
|
234
|
+
signal: AbortSignal.timeout(10_000),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (response.ok) {
|
|
238
|
+
return { ok: true, message: `Connected to ${config.host}` };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
message: `${config.host} returned ${response.status} ${response.statusText}`.trim(),
|
|
244
|
+
};
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
message: error instanceof Error ? error.message : String(error),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function handleLangfuseStatusCommand(
|
|
254
|
+
args: string,
|
|
255
|
+
ctx: CommandContextLike,
|
|
256
|
+
deps: CommandDeps = {},
|
|
257
|
+
): Promise<boolean> {
|
|
258
|
+
const parsed = parseCommandArgs(args);
|
|
259
|
+
const unexpected = parsed.malformed[0] ?? parsed.positional[0] ?? Object.keys(parsed.values)[0];
|
|
260
|
+
if (unexpected) {
|
|
261
|
+
notify(ctx, `Unexpected argument '${unexpected}'. Usage: /langfuse-status`, "warning");
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const env = deps.env ?? process.env;
|
|
266
|
+
const configPath = deps.configPath ?? CONFIG_PATH;
|
|
267
|
+
notify(ctx, formatStatus(configPath, env));
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
151
271
|
function savePrivacyPreset(
|
|
152
272
|
requestedPreset: PrivacyPreset,
|
|
153
273
|
ctx: CommandContextLike,
|
|
@@ -227,10 +347,17 @@ export async function handleLangfusePrivacyCommand(
|
|
|
227
347
|
}
|
|
228
348
|
|
|
229
349
|
export async function handleLangfuseTestCommand(
|
|
230
|
-
|
|
350
|
+
args: string,
|
|
231
351
|
ctx: CommandContextLike,
|
|
232
352
|
deps: CommandDeps = {},
|
|
233
353
|
): Promise<boolean> {
|
|
354
|
+
const parsed = parseCommandArgs(args);
|
|
355
|
+
const unexpected = parsed.malformed[0] ?? parsed.positional[0] ?? Object.keys(parsed.values)[0];
|
|
356
|
+
if (unexpected) {
|
|
357
|
+
notify(ctx, `Unexpected argument '${unexpected}'. Usage: /langfuse-test`, "warning");
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
234
361
|
if (!state.config && !(await ensureConfig(ctx))) {
|
|
235
362
|
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup first.", "warning");
|
|
236
363
|
return false;
|
|
@@ -241,6 +368,18 @@ export async function handleLangfuseTestCommand(
|
|
|
241
368
|
return false;
|
|
242
369
|
}
|
|
243
370
|
|
|
371
|
+
const config = state.config;
|
|
372
|
+
if (!config) {
|
|
373
|
+
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup first.", "warning");
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const connectivity = await (deps.checkConnectivity ?? checkLangfuseConnectivity)(config);
|
|
378
|
+
if (!connectivity.ok) {
|
|
379
|
+
notify(ctx, `Langfuse connectivity check failed: ${connectivity.message}`, "error");
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
244
383
|
let runtimeInitialized = false;
|
|
245
384
|
try {
|
|
246
385
|
const rt = await (deps.getRuntime ?? getRuntime)();
|
|
@@ -270,7 +409,7 @@ export async function handleLangfuseTestCommand(
|
|
|
270
409
|
return observation;
|
|
271
410
|
},
|
|
272
411
|
);
|
|
273
|
-
notify(ctx, `Langfuse test succeeded.
|
|
412
|
+
notify(ctx, `Langfuse test succeeded. ${connectivity.message}; test trace sent to ${config.host}.`);
|
|
274
413
|
return true;
|
|
275
414
|
} catch (error) {
|
|
276
415
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import type { Config } from "./types.js";
|
|
4
4
|
import { CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
|
|
@@ -52,8 +52,33 @@ export function loadConfig(env: EnvLike = process.env as EnvLike, path = CONFIG_
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function saveConfig(config: Config, path = CONFIG_PATH) {
|
|
55
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
-
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
56
|
+
chmodSync(dirname(path), 0o700);
|
|
57
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
58
|
+
chmodSync(path, 0o600);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function maskPublicKey(value: string): string {
|
|
62
|
+
if (value.length <= 9) {
|
|
63
|
+
return "[REDACTED_SECRET]";
|
|
64
|
+
}
|
|
65
|
+
return `${value.slice(0, 6)}...${value.slice(-4)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sanitizeConfigForLog(config: Pick<Config, "publicKey" | "secretKey" | "host"> | null): {
|
|
69
|
+
publicKey: string;
|
|
70
|
+
secretKey: string;
|
|
71
|
+
host: string;
|
|
72
|
+
} | null {
|
|
73
|
+
if (!config) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
publicKey: maskPublicKey(config.publicKey),
|
|
79
|
+
secretKey: "[REDACTED_SECRET]",
|
|
80
|
+
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
81
|
+
};
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | null> {
|
package/src/langfuse.ts
CHANGED
|
@@ -3,10 +3,23 @@ import { state } from "./state.js";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
|
|
5
5
|
let runtime: LangfuseRuntime | null = null;
|
|
6
|
+
let registeredContextManager: OtelContextManager | null = null;
|
|
6
7
|
const activeSessions = new Set<string>();
|
|
8
|
+
let lastRuntimeError: { scope: string; message: string; timestamp: Date } | null = null;
|
|
7
9
|
|
|
8
10
|
type FallbackObservationType = "SPAN" | "GENERATION";
|
|
9
11
|
|
|
12
|
+
interface OtelContextManager {
|
|
13
|
+
enable(): OtelContextManager;
|
|
14
|
+
disable(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface OtelContextApi {
|
|
18
|
+
setGlobalContextManager(contextManager: OtelContextManager): boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type AsyncHooksContextManagerCtor = new () => OtelContextManager;
|
|
22
|
+
|
|
10
23
|
interface RestFallbackTrace {
|
|
11
24
|
id: string;
|
|
12
25
|
timestamp: string;
|
|
@@ -64,6 +77,36 @@ function debugLog(message: string) {
|
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
|
|
80
|
+
export function ensureOtelContextManager(
|
|
81
|
+
contextApi: OtelContextApi,
|
|
82
|
+
AsyncHooksContextManager: AsyncHooksContextManagerCtor,
|
|
83
|
+
): boolean {
|
|
84
|
+
if (registeredContextManager) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const contextManager = new AsyncHooksContextManager().enable();
|
|
89
|
+
if (contextApi.setGlobalContextManager(contextManager)) {
|
|
90
|
+
registeredContextManager = contextManager;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
contextManager.disable();
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function rememberRuntimeError(scope: string, error: unknown) {
|
|
99
|
+
lastRuntimeError = {
|
|
100
|
+
scope,
|
|
101
|
+
message: error instanceof Error ? error.message : String(error),
|
|
102
|
+
timestamp: new Date(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getLastRuntimeError(): { scope: string; message: string; timestamp: Date } | null {
|
|
107
|
+
return lastRuntimeError;
|
|
108
|
+
}
|
|
109
|
+
|
|
67
110
|
async function withTimeout<T>(label: string, operation: Promise<T> | undefined): Promise<T | undefined> {
|
|
68
111
|
if (!operation) {
|
|
69
112
|
return undefined;
|
|
@@ -338,6 +381,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
338
381
|
const responseErrors = responseBody?.errors;
|
|
339
382
|
const errors = Array.isArray(responseErrors) ? responseErrors : [];
|
|
340
383
|
if (errors.length > 0) {
|
|
384
|
+
rememberRuntimeError("REST fallback ingestion", new Error(JSON.stringify(errors)));
|
|
341
385
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
342
386
|
} else {
|
|
343
387
|
debugLog(`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion`);
|
|
@@ -358,8 +402,17 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
358
402
|
}
|
|
359
403
|
|
|
360
404
|
if (!runtime) {
|
|
361
|
-
const [
|
|
405
|
+
const [
|
|
406
|
+
{ BasicTracerProvider },
|
|
407
|
+
{ context },
|
|
408
|
+
{ AsyncHooksContextManager },
|
|
409
|
+
{ LangfuseSpanProcessor },
|
|
410
|
+
tracing,
|
|
411
|
+
{ LangfuseClient },
|
|
412
|
+
] = await Promise.all([
|
|
362
413
|
import("@opentelemetry/sdk-trace-base"),
|
|
414
|
+
import("@opentelemetry/api"),
|
|
415
|
+
import("@opentelemetry/context-async-hooks"),
|
|
363
416
|
import("@langfuse/otel"),
|
|
364
417
|
import("@langfuse/tracing"),
|
|
365
418
|
import("@langfuse/client"),
|
|
@@ -371,30 +424,37 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
371
424
|
attempted: false,
|
|
372
425
|
};
|
|
373
426
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
baseUrl: state.config.host,
|
|
378
|
-
});
|
|
379
|
-
const tracerProvider = new BasicTracerProvider({ spanProcessors: [spanProcessor] });
|
|
380
|
-
tracing.setLangfuseTracerProvider(tracerProvider);
|
|
381
|
-
|
|
382
|
-
runtime = {
|
|
383
|
-
startObservation: ((name: string, body?: Record<string, unknown>, options?: { asType?: string }) => {
|
|
384
|
-
const observation = (tracing as any).startObservation(name, body, options);
|
|
385
|
-
return wrapObservation(observation, restFallback, name, body, options?.asType);
|
|
386
|
-
}) as unknown as LangfuseRuntime["startObservation"],
|
|
387
|
-
propagateAttributes: tracing.propagateAttributes as unknown as LangfuseRuntime["propagateAttributes"],
|
|
388
|
-
scoreClient: new LangfuseClient({
|
|
427
|
+
try {
|
|
428
|
+
ensureOtelContextManager(context, AsyncHooksContextManager);
|
|
429
|
+
const spanProcessor = new LangfuseSpanProcessor({
|
|
389
430
|
publicKey: state.config.publicKey,
|
|
390
431
|
secretKey: state.config.secretKey,
|
|
391
432
|
baseUrl: state.config.host,
|
|
392
|
-
})
|
|
393
|
-
spanProcessor
|
|
394
|
-
tracerProvider
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
433
|
+
});
|
|
434
|
+
const tracerProvider = new BasicTracerProvider({ spanProcessors: [spanProcessor] });
|
|
435
|
+
tracing.setLangfuseTracerProvider(tracerProvider);
|
|
436
|
+
|
|
437
|
+
runtime = {
|
|
438
|
+
startObservation: ((name: string, body?: Record<string, unknown>, options?: { asType?: string }) => {
|
|
439
|
+
const observation = (tracing as any).startObservation(name, body, options);
|
|
440
|
+
return wrapObservation(observation, restFallback, name, body, options?.asType);
|
|
441
|
+
}) as unknown as LangfuseRuntime["startObservation"],
|
|
442
|
+
propagateAttributes: tracing.propagateAttributes as unknown as LangfuseRuntime["propagateAttributes"],
|
|
443
|
+
scoreClient: new LangfuseClient({
|
|
444
|
+
publicKey: state.config.publicKey,
|
|
445
|
+
secretKey: state.config.secretKey,
|
|
446
|
+
baseUrl: state.config.host,
|
|
447
|
+
}) as LangfuseScoreClient,
|
|
448
|
+
spanProcessor,
|
|
449
|
+
tracerProvider,
|
|
450
|
+
clearTracerProvider: () => tracing.setLangfuseTracerProvider(null),
|
|
451
|
+
restFallback,
|
|
452
|
+
};
|
|
453
|
+
lastRuntimeError = null;
|
|
454
|
+
} catch (e) {
|
|
455
|
+
rememberRuntimeError("runtime init", e);
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
398
458
|
}
|
|
399
459
|
|
|
400
460
|
return runtime as LangfuseRuntime;
|
|
@@ -416,6 +476,7 @@ function doShutdownRuntime(): Promise<void> {
|
|
|
416
476
|
await withTimeout("Langfuse client shutdown", rt.scoreClient.shutdown?.());
|
|
417
477
|
await withTimeout("OTel tracer shutdown", rt.tracerProvider?.shutdown?.());
|
|
418
478
|
} catch (e) {
|
|
479
|
+
rememberRuntimeError("runtime shutdown", e);
|
|
419
480
|
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
420
481
|
} finally {
|
|
421
482
|
if (!runtime) {
|
|
@@ -472,6 +533,7 @@ export async function sendScore(name: string, value: number, options: { traceId?
|
|
|
472
533
|
sessionId: options.traceId ? undefined : state.currentSessionId || undefined,
|
|
473
534
|
});
|
|
474
535
|
} catch (e) {
|
|
536
|
+
rememberRuntimeError(`score ${name}`, e);
|
|
475
537
|
console.warn(`📊 Langfuse: Failed to send score ${name}`, e);
|
|
476
538
|
}
|
|
477
539
|
}
|