Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 0 additions & 1 deletion samples/src/langchainInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ async function main(): Promise<void> {
instrumentationOptions: {
langchain: {
enabled: true,
isContentRecordingEnabled: true,
},
},
});
Expand Down
1 change: 0 additions & 1 deletion samples/src/openaiInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ async function main(): Promise<void> {
instrumentationOptions: {
openaiAgents: {
enabled: true,
isContentRecordingEnabled: true,
},
},
});
Expand Down
4 changes: 2 additions & 2 deletions src/distro/distro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,15 @@ async function initializeOpenAIAgentsInstrumentation(
}

async function initializeLangChainInstrumentation(
options: LangChainInstrumentationConfig,
_options: LangChainInstrumentationConfig,
): Promise<void> {
try {
const [{ LangChainTraceInstrumentor }, callbackManagerModule] = await Promise.all([
import("../genai/instrumentations/langchain/langchainTraceInstrumentor.js"),
import("@langchain/core/callbacks/manager"),
]);
if (isShutdown) return;
LangChainTraceInstrumentor.instrument(callbackManagerModule, options);
LangChainTraceInstrumentor.instrument(callbackManagerModule);
} catch (error) {
Logger.getInstance().warn(
"[GenAI] Failed to initialize LangChain instrumentation. " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ class LangChainTraceInstrumentorImpl extends InstrumentationBase<Instrumentation
private _hasBeenEnabled = false;
private _isPatched = false;
protected otelTracer: Tracer;
private isContentRecordingEnabled: boolean;

private constructor(options?: { isContentRecordingEnabled?: boolean }) {
private constructor() {
if (LangChainTraceInstrumentorImpl._instance !== null) {
throw new Error("LangChainTraceInstrumentor can only be instantiated once.");
}
Expand All @@ -32,17 +31,14 @@ class LangChainTraceInstrumentorImpl extends InstrumentationBase<Instrumentation
});

this.otelTracer = trace.getTracer("microsoft-otel-langchain", "1.0.0");
this.isContentRecordingEnabled = options?.isContentRecordingEnabled ?? false;

LangChainTraceInstrumentorImpl._instance = this;
diag.info("[LangChainTraceInstrumentor] Initialized and automatically enabled");
}

static getInstance(options?: {
isContentRecordingEnabled?: boolean;
}): LangChainTraceInstrumentorImpl {
static getInstance(): LangChainTraceInstrumentorImpl {
if (!LangChainTraceInstrumentorImpl._instance) {
LangChainTraceInstrumentorImpl._instance = new LangChainTraceInstrumentorImpl(options);
LangChainTraceInstrumentorImpl._instance = new LangChainTraceInstrumentorImpl();
}
return LangChainTraceInstrumentorImpl._instance;
}
Expand Down Expand Up @@ -97,9 +93,7 @@ class LangChainTraceInstrumentorImpl extends InstrumentationBase<Instrumentation
this: CallbackManagerModuleType,
...args: Parameters<(typeof CallbackManager)["_configureSync"]>
) {
args[0] = addTracerToHandlers(instrumentor.otelTracer, args[0], {
isContentRecordingEnabled: instrumentor.isContentRecordingEnabled,
});
args[0] = addTracerToHandlers(instrumentor.otelTracer, args[0]);
diag.debug("[LangChainTraceInstrumentor] _configureSync wrapped to add LangChainTracer");
return original.apply(this, args);
};
Expand Down Expand Up @@ -153,9 +147,8 @@ export class LangChainTraceInstrumentor {
*/
static instrument(
module: CallbackManagerModuleType,
options?: { isContentRecordingEnabled?: boolean },
): void {
LangChainTraceInstrumentorImpl.getInstance(options).manuallyInstrumentImpl(module);
LangChainTraceInstrumentorImpl.getInstance().manuallyInstrumentImpl(module);
}

/**
Expand Down Expand Up @@ -189,21 +182,20 @@ export class LangChainTraceInstrumentor {
export function addTracerToHandlers(
tracer: Tracer,
handlers: CallbackManagerModule.Callbacks | undefined,
options?: { isContentRecordingEnabled?: boolean },
): CallbackManagerModule.Callbacks {
if (handlers == null) {
return [new LangChainTracer(tracer, options)];
return [new LangChainTracer(tracer)];
}

if (Array.isArray(handlers)) {
if (!handlers.some((h) => h instanceof LangChainTracer)) {
handlers.push(new LangChainTracer(tracer, options));
handlers.push(new LangChainTracer(tracer));
}
return handlers;
}

if (!handlers.inheritableHandlers.some((h) => h instanceof LangChainTracer)) {
handlers.addHandler(new LangChainTracer(tracer, options), true);
handlers.addHandler(new LangChainTracer(tracer), true);
}
return handlers;
}
64 changes: 48 additions & 16 deletions src/genai/instrumentations/langchain/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { context, trace, Span, SpanKind, SpanStatusCode, Tracer } from "@opentel
import { BaseTracer, Run } from "@langchain/core/tracers/base";
import { isTracingSuppressed } from "@opentelemetry/core";
import { diag } from "@opentelemetry/api";
import { ATTR_ERROR_MESSAGE, ATTR_GEN_AI_PROVIDER_NAME } from "../../index.js";
import {
ATTR_ERROR_MESSAGE,
ATTR_ERROR_TYPE,
ATTR_GEN_AI_CALLER_AGENT_NAME,
ATTR_GEN_AI_PROVIDER_NAME,
} from "../../index.js";
import * as Utils from "./utils.js";

type RunWithSpan = { run: Run; span: Span; startTime: number; lastAccessTime: number };
Expand All @@ -26,23 +31,21 @@ type RunWithSpan = { run: Run; span: Span; startTime: number; lastAccessTime: nu
* - Skips LangChain-internal runs (tagged `langsmith:hidden`, `Branch*`, or
* unmapped run types) to avoid noisy traces.
* - Guards against unbounded memory with a hard cap of {@link MAX_RUNS}.
* - Content-sensitive attributes (messages, tool args, system instructions)
* are only recorded when `isContentRecordingEnabled` is true.
* - Content attributes (messages, tool args) are always recorded
* (aligned with Python/.NET SDKs).
*/
export class LangChainTracer extends BaseTracer {
/** Hard cap on concurrent tracked runs to prevent memory leaks. */
private static readonly MAX_RUNS = 10_000;
private tracer: Tracer;
private isContentRecordingEnabled: boolean;
/** Active runs keyed by LangChain run ID. */
private runs = new Map<string, RunWithSpan>();
/** Maps each run ID → its parent run ID for parent-span-context lookup. */
private parentByRunId = new Map<string, string | undefined>();

constructor(tracer: Tracer, options?: { isContentRecordingEnabled?: boolean }) {
constructor(tracer: Tracer) {
super();
this.tracer = tracer;
this.isContentRecordingEnabled = options?.isContentRecordingEnabled ?? false;
}

name = "OpenTelemetryLangChainTracer";
Expand Down Expand Up @@ -93,12 +96,16 @@ export class LangChainTracer extends BaseTracer {

// Build span name: "<operation> <name|model>"
let spanName = run.name;
let kind: SpanKind = SpanKind.INTERNAL;
if (operation === "invoke_agent") {
spanName = `${operation} ${run.name}`;
kind = SpanKind.SERVER;
} else if (operation === "execute_tool") {
spanName = `${operation} ${run.name}`;
kind = SpanKind.CLIENT;
} else if (operation === "chat") {
spanName = `${operation} ${Utils.getModel(run) || run.name}`.trim();
kind = SpanKind.CLIENT;
}

if (this.runs.size >= LangChainTracer.MAX_RUNS) {
Expand All @@ -111,7 +118,7 @@ export class LangChainTracer extends BaseTracer {
const span = this.tracer.startSpan(
spanName,
{
kind: SpanKind.INTERNAL,
kind,
startTime,
attributes: { [ATTR_GEN_AI_PROVIDER_NAME]: "langchain" },
},
Expand All @@ -123,8 +130,7 @@ export class LangChainTracer extends BaseTracer {

/**
* Called by LangChain when a run finishes. Sets status, enriches the span
* with GenAI attributes, and ends it. If content recording is enabled,
* message bodies, tool arguments, and system instructions are also attached.
* with GenAI attributes, and ends it.
*/
protected async _endTrace(run: Run) {
if (isTracingSuppressed(context.active())) {
Expand Down Expand Up @@ -162,25 +168,35 @@ export class LangChainTracer extends BaseTracer {
if (run.error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.setAttribute(ATTR_ERROR_MESSAGE, String(run.error));
const errorType =
(run.error as { name?: string })?.name ??
(run.error as { constructor?: { name?: string } })?.constructor?.name;
if (typeof errorType === "string" && errorType.length > 0) {
span.setAttribute(ATTR_ERROR_TYPE, errorType);
}
} else {
span.setStatus({ code: SpanStatusCode.OK });
}

// Always-on attributes: operation type, agent info, model, provider, session, tokens
Utils.setOperationTypeAttribute(operation, span);
Utils.setAgentAttributes(run, span);
if (operation === "invoke_agent") {
const callerName = this.findCallerAgentName(run);
if (callerName) {
span.setAttribute(ATTR_GEN_AI_CALLER_AGENT_NAME, callerName);
}
}
Utils.setModelAttribute(run, span);
Utils.setProviderNameAttribute(run, span);
Utils.setSessionIdAttribute(run, span);
Utils.setTokenAttributes(run, span);

// Opt-in content attributes (may contain PII / large payloads)
if (this.isContentRecordingEnabled) {
Utils.setToolAttributes(run, span);
Utils.setInputMessagesAttribute(run, span);
Utils.setOutputMessagesAttribute(run, span);
Utils.setSystemInstructionsAttribute(run, span);
}
// Content attributes — always recorded (aligned with Python/.NET SDKs)
Utils.setToolAttributes(run, span);
Utils.setInputMessagesAttribute(run, span);
Utils.setOutputMessagesAttribute(run, span);
Utils.setSystemInstructionsAttribute(run, span);
} catch (error) {
diag.error(
`[LangChainTracer] Error setting span attributes for run ${run.name}: ${error instanceof Error ? error.message : String(error)}`,
Expand Down Expand Up @@ -209,4 +225,20 @@ export class LangChainTracer extends BaseTracer {
}
return undefined;
}

/**
* Walk up the parent run chain to find the nearest ancestor that is an
* invoke_agent run, returning its name as the caller agent name.
*/
private findCallerAgentName(run: Run): string | undefined {
let pid = run.parent_run_id;
while (pid) {
const entry = this.runs.get(pid);
if (entry && Utils.getOperationType(entry.run) === "invoke_agent") {
return entry.run.name;
}
pid = this.parentByRunId.get(pid);
}
return undefined;
}
}
Loading
Loading