mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
feat: 添加对 langfuse 监控的支持 (#242)
* docs: 更新类型检查的 CLAUDE.md * feat: 添加模型 1M 上下文切换 * chore: remove prefetchOfficialMcpUrls call on startup * docs: 添加 git commit 规范 * feat: 第一次接入 langfuse * fix: 修复 generation 的计时的错误 * feat: 添加多 agent 的监控 * feat: 添加 /poor 省流模式,toggle 关闭 extract_memories 和 prompt_suggestion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 修复 lock 文件 * chore: 更新类型依赖 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
201
src/services/langfuse/tracing.ts
Normal file
201
src/services/langfuse/tracing.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { startObservation, LangfuseOtelSpanAttributes } from '@langfuse/tracing'
|
||||
import type { LangfuseSpan, LangfuseGeneration, LangfuseAgent } from '@langfuse/tracing'
|
||||
import { isLangfuseEnabled } from './client.js'
|
||||
import { sanitizeToolInput, sanitizeToolOutput } from './sanitize.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
|
||||
export type { LangfuseSpan }
|
||||
|
||||
// Root trace is an agent observation — represents one full agentic turn/session
|
||||
type RootTrace = LangfuseAgent & { _sessionId?: string }
|
||||
|
||||
export function createTrace(params: {
|
||||
sessionId: string
|
||||
model: string
|
||||
provider: string
|
||||
input?: unknown
|
||||
name?: string
|
||||
querySource?: string
|
||||
}): LangfuseSpan | null {
|
||||
if (!isLangfuseEnabled()) return null
|
||||
try {
|
||||
const traceName = params.name ?? (params.querySource ? `agent-run:${params.querySource}` : 'agent-run')
|
||||
const rootSpan = startObservation(traceName, {
|
||||
input: params.input,
|
||||
metadata: {
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
agentType: 'main',
|
||||
...(params.querySource && { querySource: params.querySource }),
|
||||
},
|
||||
}, { asType: 'agent' }) as RootTrace
|
||||
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
|
||||
rootSpan._sessionId = params.sessionId
|
||||
logForDebugging(`[langfuse] Trace created: ${rootSpan.id}`)
|
||||
return rootSpan as unknown as LangfuseSpan
|
||||
} catch (e) {
|
||||
logForDebugging(`[langfuse] createTrace failed: ${e}`, { level: 'error' })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const PROVIDER_GENERATION_NAMES: Record<string, string> = {
|
||||
firstParty: 'ChatAnthropic',
|
||||
bedrock: 'ChatBedrockAnthropic',
|
||||
vertex: 'ChatVertexAnthropic',
|
||||
foundry: 'ChatFoundry',
|
||||
openai: 'ChatOpenAI',
|
||||
gemini: 'ChatGoogleGenerativeAI',
|
||||
grok: 'ChatXAI',
|
||||
}
|
||||
|
||||
export function recordLLMObservation(
|
||||
rootSpan: LangfuseSpan | null,
|
||||
params: {
|
||||
model: string
|
||||
provider: string
|
||||
input: unknown
|
||||
output: unknown
|
||||
usage: { input_tokens: number; output_tokens: number }
|
||||
startTime?: Date
|
||||
endTime?: Date
|
||||
completionStartTime?: Date
|
||||
},
|
||||
): void {
|
||||
if (!rootSpan || !isLangfuseEnabled()) return
|
||||
try {
|
||||
const genName = PROVIDER_GENERATION_NAMES[params.provider] ?? `Chat${params.provider}`
|
||||
|
||||
// Use the global startObservation directly instead of rootSpan.startObservation().
|
||||
// The instance method only forwards asType to the global function and drops startTime,
|
||||
// which causes negative TTFT because the OTel span's start time defaults to "now".
|
||||
const gen: LangfuseGeneration = startObservation(
|
||||
genName,
|
||||
{
|
||||
model: params.model,
|
||||
input: params.input,
|
||||
metadata: {
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
},
|
||||
...(params.completionStartTime && { completionStartTime: params.completionStartTime }),
|
||||
},
|
||||
{
|
||||
asType: 'generation',
|
||||
...(params.startTime && { startTime: params.startTime }),
|
||||
parentSpanContext: rootSpan.otelSpan.spanContext(),
|
||||
},
|
||||
)
|
||||
|
||||
// Propagate session ID to generation span so Langfuse links it correctly
|
||||
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
||||
if (sessionId) {
|
||||
gen.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
||||
}
|
||||
|
||||
gen.update({
|
||||
output: params.output,
|
||||
usageDetails: {
|
||||
input: params.usage.input_tokens,
|
||||
output: params.usage.output_tokens,
|
||||
},
|
||||
})
|
||||
|
||||
gen.end(params.endTime)
|
||||
logForDebugging(`[langfuse] LLM observation recorded: ${gen.id}`)
|
||||
} catch (e) {
|
||||
logForDebugging(`[langfuse] recordLLMObservation failed: ${e}`, { level: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
export function recordToolObservation(
|
||||
rootSpan: LangfuseSpan | null,
|
||||
params: {
|
||||
toolName: string
|
||||
toolUseId: string
|
||||
input: unknown
|
||||
output: string
|
||||
startTime?: Date
|
||||
isError?: boolean
|
||||
},
|
||||
): void {
|
||||
if (!rootSpan || !isLangfuseEnabled()) return
|
||||
try {
|
||||
// Use the global startObservation directly instead of rootSpan.startObservation().
|
||||
// The instance method only forwards asType and drops startTime,
|
||||
// causing tool execution duration to be 0.
|
||||
const toolObs = startObservation(
|
||||
params.toolName,
|
||||
{
|
||||
input: sanitizeToolInput(params.toolName, params.input),
|
||||
metadata: {
|
||||
toolUseId: params.toolUseId,
|
||||
isError: String(params.isError ?? false),
|
||||
},
|
||||
},
|
||||
{
|
||||
asType: 'tool',
|
||||
...(params.startTime && { startTime: params.startTime }),
|
||||
parentSpanContext: rootSpan.otelSpan.spanContext(),
|
||||
},
|
||||
)
|
||||
|
||||
// Propagate session ID to tool span so Langfuse links it correctly
|
||||
const sessionId = (rootSpan as unknown as RootTrace)._sessionId
|
||||
if (sessionId) {
|
||||
toolObs.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)
|
||||
}
|
||||
|
||||
toolObs.update({
|
||||
output: sanitizeToolOutput(params.toolName, params.output),
|
||||
...(params.isError && { level: 'ERROR' as const }),
|
||||
})
|
||||
|
||||
toolObs.end()
|
||||
logForDebugging(`[langfuse] Tool observation recorded: ${params.toolName} (${toolObs.id})`)
|
||||
} catch (e) {
|
||||
logForDebugging(`[langfuse] recordToolObservation failed: ${e}`, { level: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
export function createSubagentTrace(params: {
|
||||
sessionId: string
|
||||
agentType: string
|
||||
agentId: string
|
||||
model: string
|
||||
provider: string
|
||||
input?: unknown
|
||||
}): LangfuseSpan | null {
|
||||
if (!isLangfuseEnabled()) return null
|
||||
try {
|
||||
const rootSpan = startObservation(`agent:${params.agentType}`, {
|
||||
input: params.input,
|
||||
metadata: {
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
agentType: params.agentType,
|
||||
agentId: params.agentId,
|
||||
},
|
||||
}, { asType: 'agent' }) as RootTrace
|
||||
rootSpan.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, params.sessionId)
|
||||
rootSpan._sessionId = params.sessionId
|
||||
logForDebugging(`[langfuse] Sub-agent trace created: ${rootSpan.id} (type=${params.agentType})`)
|
||||
return rootSpan as unknown as LangfuseSpan
|
||||
} catch (e) {
|
||||
logForDebugging(`[langfuse] createSubagentTrace failed: ${e}`, { level: 'error' })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function endTrace(rootSpan: LangfuseSpan | null, output?: unknown): void {
|
||||
if (!rootSpan) return
|
||||
try {
|
||||
if (output !== undefined) {
|
||||
rootSpan.update({ output })
|
||||
}
|
||||
rootSpan.end()
|
||||
logForDebugging(`[langfuse] Trace ended: ${rootSpan.id}`)
|
||||
} catch (e) {
|
||||
logForDebugging(`[langfuse] endTrace failed: ${e}`, { level: 'error' })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user