Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"summary": "Repair OpenTelemetry 2.x hierarchy, error, and workflow fields for LangChain, LlamaIndex, and Mastra",
"packages": {
"@respan/instrumentation-langchain": "patch",
"@respan/instrumentation-llama-index": "patch",
"@respan/instrumentation-mastra": "patch"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ import {
TraceloopSpanKindValues,
} from "@traceloop/ai-semantic-conventions";
import {
DIRECT_COMPLETION_TOKENS,
DIRECT_INPUT,
DIRECT_MODEL,
DIRECT_OUTPUT,
DIRECT_PROMPT_TOKENS,
DIRECT_TOOLS,
DIRECT_TOTAL_REQUEST_TOKENS,
LANGCHAIN_FRAMEWORK_ATTR,
LANGCHAIN_METADATA_ATTR,
LANGCHAIN_PARENT_RUN_ID_ATTR,
Expand Down Expand Up @@ -76,11 +69,13 @@ export function getCallbackHandler(
});
}

export function addRespanCallback(
config: LangChainCallbackConfig = {},
export function addRespanCallback<
TConfig extends object = LangChainCallbackConfig,
>(
config: TConfig = {} as TConfig,
handler: RespanCallbackHandler = getCallbackHandler(),
): LangChainCallbackConfig {
const nextConfig: LangChainCallbackConfig = { ...config };
): TConfig {
const nextConfig = { ...config } as TConfig & LangChainCallbackConfig;
nextConfig.callbacks = withRespanCallback(nextConfig.callbacks, handler);
return nextConfig;
}
Expand Down Expand Up @@ -140,7 +135,10 @@ export class RespanCallbackHandler {
private readonly _runs = new Map<string, RunRecord>();
private readonly _runTraceIds = new Map<string, string>();
private readonly _runPaths = new Map<string, string>();
private readonly _runWorkflowNames = new Map<string, string>();
private readonly _langflowTraceId = generateTraceId();
private _langflowWorkflowName?: string;
private _langflowRootSpanId?: string;

constructor(options: RespanCallbackHandlerOptions = {}) {
this.includeContent = options.includeContent ?? true;
Expand All @@ -152,8 +150,10 @@ export class RespanCallbackHandler {
private _rememberRun(record: RunRecord): void {
this._runTraceIds.set(record.runId, record.traceId);
this._runPaths.set(record.runId, record.entityPath);
this._runWorkflowNames.set(record.runId, record.workflowName);
trimMap(this._runTraceIds, this.maxCachedRuns);
trimMap(this._runPaths, this.maxCachedRuns);
trimMap(this._runWorkflowNames, this.maxCachedRuns);
}

private _startRun({
Expand Down Expand Up @@ -199,24 +199,59 @@ export class RespanCallbackHandler {
activeParent?.traceId ??
fallbackTraceId;

const parentSpanId =
let parentSpanId =
parentHex !== undefined
? deriveSpanId(parentHex)
: activeParent?.spanId;
const isGroupedLangflowRoot =
!parentHex &&
!activeParent &&
framework === "langflow" &&
this.groupLangflowRootRuns;
if (isGroupedLangflowRoot && this._langflowRootSpanId) {
parentSpanId = this._langflowRootSpanId;
}

const parentPath =
(parentHex && this._runs.get(parentHex)?.entityPath) ??
this._runPaths.get(parentHex ?? "");
const entityPath = parentPath ? `${parentPath}.${name}` : name;

const parentWorkflowName =
(parentHex && this._runs.get(parentHex)?.workflowName) ??
this._runWorkflowNames.get(parentHex ?? "");
if (
!parentHex &&
!activeParent &&
framework === "langflow" &&
this.groupLangflowRootRuns &&
!this._langflowWorkflowName
) {
const componentName = metadata?.langflow_component;
this._langflowWorkflowName =
typeof componentName === "string" && componentName.trim()
? componentName.trim()
: name;
}
const workflowName =
parentWorkflowName ??
(framework === "langflow" && this.groupLangflowRootRuns
? this._langflowWorkflowName
: undefined) ??
name;

const spanId = deriveSpanId(runHex);
if (isGroupedLangflowRoot && !this._langflowRootSpanId) {
this._langflowRootSpanId = spanId;
}
this._runs.set(runHex, {
runId: runHex,
traceId,
spanId: deriveSpanId(runHex),
spanId,
parentRunId: parentHex,
parentSpanId,
name,
entityPath,
workflowName,
logType,
spanKind,
startTime: hrTime(),
Expand All @@ -234,13 +269,19 @@ export class RespanCallbackHandler {
const attrs: SpanAttributesRecord = {
[RespanSpanAttributes.RESPAN_LOG_METHOD]: RESPAN_LOG_METHOD_TS_TRACING,
[RespanSpanAttributes.RESPAN_LOG_TYPE]: record.logType,
[SpanAttributes.TRACELOOP_SPAN_KIND]: record.spanKind,
[SpanAttributes.TRACELOOP_ENTITY_NAME]: record.name,
[SpanAttributes.TRACELOOP_ENTITY_PATH]: record.entityPath,
[SpanAttributes.TRACELOOP_WORKFLOW_NAME]: record.workflowName,
[RespanSpanAttributes.RESPAN_TRACE_GROUP_ID]: record.workflowName,
[LANGCHAIN_RUN_ID_ATTR]: record.runId,
[LANGCHAIN_FRAMEWORK_ATTR]: record.framework,
};
setIfPresent(attrs, LANGCHAIN_PARENT_RUN_ID_ATTR, record.parentRunId);
setIfPresent(
attrs,
RespanSpanAttributes.RESPAN_SPAN_CUSTOM_ID,
record.metadata?.custom_identifier,
);

if (this.includeMetadata) {
setIfPresent(attrs, LANGCHAIN_TAGS_ATTR, safeJsonString(record.tags));
Expand All @@ -252,9 +293,7 @@ export class RespanCallbackHandler {
const inputString = safeJsonString(record.inputValue);
const outputString = safeJsonString(normalizeOutputForLogging(outputValue));
setIfPresent(attrs, SpanAttributes.TRACELOOP_ENTITY_INPUT, inputString);
setIfPresent(attrs, DIRECT_INPUT, inputString);
setIfPresent(attrs, SpanAttributes.TRACELOOP_ENTITY_OUTPUT, outputString);
setIfPresent(attrs, DIRECT_OUTPUT, outputString);
}

Object.assign(attrs, record.extraAttributes);
Expand Down Expand Up @@ -356,6 +395,10 @@ export class RespanCallbackHandler {
(parentHex && this._runs.get(parentHex)?.entityPath) ??
this._runPaths.get(parentHex ?? "");
const entityPath = parentPath ? `${parentPath}.${name}` : name;
const workflowName =
(parentHex && this._runs.get(parentHex)?.workflowName) ??
this._runWorkflowNames.get(parentHex ?? "") ??
name;
const record: RunRecord = {
runId: runIdToHex(`${traceId}:${parentHex ?? ""}:${name}:${Date.now()}:${Math.random()}`),
traceId,
Expand All @@ -364,6 +407,7 @@ export class RespanCallbackHandler {
parentSpanId,
name,
entityPath,
workflowName,
logType,
spanKind,
startTime: hrTime(),
Expand Down Expand Up @@ -407,16 +451,27 @@ export class RespanCallbackHandler {
runName?: unknown,
): void {
const isRoot = parentRunId === undefined || parentRunId === null;
const normalizedMetadata = normalizeMetadata(metadata);
const isCreateAgentRoot =
isRoot && normalizedMetadata?.ls_integration === "langchain_create_agent";
this._startRun({
runId,
parentRunId,
name: extractName(serialized, "chain", runName),
logType: isRoot ? RespanLogType.WORKFLOW : RespanLogType.TASK,
spanKind: isRoot ? TraceloopSpanKindValues.WORKFLOW : TraceloopSpanKindValues.TASK,
logType: isCreateAgentRoot
? RespanLogType.AGENT
: isRoot
? RespanLogType.WORKFLOW
: RespanLogType.TASK,
spanKind: isCreateAgentRoot
? TraceloopSpanKindValues.AGENT
: isRoot
? TraceloopSpanKindValues.WORKFLOW
: TraceloopSpanKindValues.TASK,
inputValue: inputs,
serialized,
tags: normalizeTags(tags),
metadata: normalizeMetadata(metadata),
metadata: normalizedMetadata,
});
}

Expand Down Expand Up @@ -446,7 +501,6 @@ export class RespanCallbackHandler {
};
const model = extractModel(serialized, undefined, normalizedMetadata);
setIfPresent(extraAttributes, ATTR_GEN_AI_REQUEST_MODEL, model);
setIfPresent(extraAttributes, DIRECT_MODEL, model);
for (const [index, message] of firstConversation.entries()) {
for (const [key, value] of Object.entries(message)) {
setIfPresent(
Expand All @@ -457,8 +511,11 @@ export class RespanCallbackHandler {
}
}
const tools = extractTools({ serialized, extraParams });
setIfPresent(extraAttributes, RespanSpanAttributes.RESPAN_SPAN_TOOLS, tools);
setIfPresent(extraAttributes, DIRECT_TOOLS, tools);
setIfPresent(
extraAttributes,
SpanAttributes.LLM_REQUEST_FUNCTIONS,
tools ? safeJsonString(tools) : undefined,
);

this._startRun({
runId,
Expand Down Expand Up @@ -487,14 +544,16 @@ export class RespanCallbackHandler {
const normalizedPrompts = Array.isArray(prompts) ? prompts : [prompts];
const normalizedMetadata = normalizeMetadata(metadata);
const extraAttributes: SpanAttributesRecord = {
[SpanAttributes.LLM_REQUEST_TYPE]: LLMRequestTypeValues.COMPLETION,
[SpanAttributes.LLM_REQUEST_TYPE]: LLMRequestTypeValues.CHAT,
};
const model = extractModel(serialized, undefined, normalizedMetadata);
setIfPresent(extraAttributes, ATTR_GEN_AI_REQUEST_MODEL, model);
setIfPresent(extraAttributes, DIRECT_MODEL, model);
const tools = extractTools({ serialized, extraParams });
setIfPresent(extraAttributes, RespanSpanAttributes.RESPAN_SPAN_TOOLS, tools);
setIfPresent(extraAttributes, DIRECT_TOOLS, tools);
setIfPresent(
extraAttributes,
SpanAttributes.LLM_REQUEST_FUNCTIONS,
tools ? safeJsonString(tools) : undefined,
);
for (const [index, prompt] of normalizedPrompts.entries()) {
extraAttributes[`${ATTR_GEN_AI_PROMPT}.${index}.role`] = "user";
extraAttributes[`${ATTR_GEN_AI_PROMPT}.${index}.content`] = String(prompt ?? "");
Expand All @@ -505,7 +564,7 @@ export class RespanCallbackHandler {
parentRunId,
name: extractName(serialized, "llm", runName),
logType: RespanLogType.TEXT,
spanKind: LLMRequestTypeValues.COMPLETION,
spanKind: LLMRequestTypeValues.CHAT,
inputValue: normalizedPrompts,
serialized,
tags: normalizeTags(tags),
Expand Down Expand Up @@ -533,7 +592,6 @@ export class RespanCallbackHandler {
if (record) {
const model = extractModel(record.serialized, output, record.metadata);
setIfPresent(extraAttributes, ATTR_GEN_AI_REQUEST_MODEL, model);
setIfPresent(extraAttributes, DIRECT_MODEL, model);
}

for (const [index, message] of completionMessages.entries()) {
Expand All @@ -547,17 +605,18 @@ export class RespanCallbackHandler {
}

const toolCalls = extractToolCallsFromMessages(completionMessages);
setIfPresent(extraAttributes, RespanSpanAttributes.RESPAN_SPAN_TOOL_CALLS, toolCalls);
setIfPresent(
extraAttributes,
`${ATTR_GEN_AI_COMPLETION}.0.tool_calls`,
toolCalls ? safeJsonString(toolCalls) : undefined,
);

const usage = extractUsage(output);
setIfPresent(extraAttributes, ATTR_GEN_AI_USAGE_PROMPT_TOKENS, usage.promptTokens);
setIfPresent(extraAttributes, ATTR_GEN_AI_USAGE_INPUT_TOKENS, usage.promptTokens);
setIfPresent(extraAttributes, DIRECT_PROMPT_TOKENS, usage.promptTokens);
setIfPresent(extraAttributes, ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, usage.completionTokens);
setIfPresent(extraAttributes, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, usage.completionTokens);
setIfPresent(extraAttributes, DIRECT_COMPLETION_TOKENS, usage.completionTokens);
setIfPresent(extraAttributes, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.totalTokens);
setIfPresent(extraAttributes, DIRECT_TOTAL_REQUEST_TOKENS, usage.totalTokens);

this._endRun({ runId, outputValue: outputPayload, extraAttributes });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@ export const LANGCHAIN_TAGS_ATTR = "langchain.tags";
export const LANGCHAIN_METADATA_ATTR = "langchain.metadata";
export const LANGCHAIN_SERIALIZED_ATTR = "langchain.serialized";

export const DIRECT_INPUT = "input";
export const DIRECT_OUTPUT = "output";
export const DIRECT_MODEL = "model";
export const DIRECT_PROMPT_TOKENS = "prompt_tokens";
export const DIRECT_COMPLETION_TOKENS = "completion_tokens";
export const DIRECT_TOTAL_REQUEST_TOKENS = "total_request_tokens";
export const DIRECT_TOOLS = "tools";

const JSON_CODE_FENCE_RE =
/^\s*(?<fence>`{3,}|~{3,})[ \t]*(?<language>jsonc?)?[ \t]*\r?\n(?<body>.*?)(?:\r?\n)?\k<fence>\s*$/is;

Expand All @@ -28,7 +20,6 @@ export type HrTimeTuple = [number, number];
export type FrameworkName = "langchain" | "langgraph" | "langflow";
export interface LangChainCallbackConfig {
callbacks?: unknown;
[key: string]: unknown;
}

export type SpanAttributesRecord = Record<string, unknown>;
Expand Down Expand Up @@ -58,6 +49,7 @@ export interface RunRecord {
parentSpanId?: string;
name: string;
entityPath: string;
workflowName: string;
logType: string;
spanKind: string;
startTime: HrTimeTuple;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export class LangChainInstrumentor {
return this._active;
}

addCallback(config: LangChainCallbackConfig = {}): LangChainCallbackConfig {
addCallback<TConfig extends object = LangChainCallbackConfig>(
config: TConfig = {} as TConfig,
): TConfig {
return addRespanCallback(config, this.callbackHandler);
}
}
Expand Down
Loading
Loading