Recipe: LangGraph / LangChain JS
LangGraph and LangChain JS report runs through LangChain's callback system. The OpenInference LangChain instrumentation turns those callbacks into OpenTelemetry spans; export them over OTLP to ModelRig.
Mint a key in the org you want the data in. Sign in at https://app.modelrig.ai, check the organization name in the header badge, and create a key on the API keys page. The rig_sk_ secret is shown once. One key = one org: every row the key exports lands in the organization it was minted in, whichever organization you happen to be viewing in the console. Set it on every service, worker and cron that creates a rig; your provider keys stay where they are.
# Wherever your app reads configuration: .env.local, your platform's env vars, a secret manager. Never in code.
MODELRIG_API_KEY=rig_sk_…
Point your OTLP exporter at ModelRig. ModelRig accepts OpenTelemetry traces at POST /v1/otlp/v1/traces on the ingest host (https://api.modelrig.ai unless you were given another), over OTLP/HTTP as protobuf or JSON, optionally gzipped. Authenticate with a rig_sk_ key that carries the ingest scope; every run and step lands in that key's organization. Each trace becomes one run with one attested step per span: model, provider, operation, timing, status and token usage. Prompts, completions and tool arguments are not read.
# Standard OpenTelemetry exporter variables — most OTel SDKs read them as-is.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.modelrig.ai/v1/otlp/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=Bearer%20rig_sk_…
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
Minimal config
Untested config — verify against your LangChain and instrumentation versions.
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { LangChainInstrumentation } from "@arizeai/openinference-instrumentation-langchain";
import * as CallbackManagerModule from "@langchain/core/callbacks/manager";
const sdk = new NodeSDK({ serviceName: "support-bot", traceExporter: new OTLPTraceExporter() }); // reads OTEL_EXPORTER_OTLP_TRACES_*
sdk.start();
new LangChainInstrumentation().manuallyInstrument(CallbackManagerModule);
OpenInference marks graph and chain spans with its own span kinds, not invoke_agent, so wrap each user message in the turn() helper below to number turns. Retriever and embedding spans are classified as such and kept as steps.
Set gen_ai.conversation.id to your conversation id. A trace becomes a conversation when one of its spans carries gen_ai.conversation.id and it has invoke_agent spans. Each top-level invoke_agent span is a turn, in start order, and every model call under it becomes a step turn:<n>.<k>; the run is marked as a conversation and its episode key is your conversation id. Turns are numbered within a trace: when each user message arrives as its own trace, each shows as its own one-turn conversation run, and the shared conversation id groups them. If your framework does not emit an invoke_agent span itself, wrap each user message in one.
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("support-bot");
// One span per user message. The model calls your framework makes inside it
// become this conversation's turn steps; the conversation id joins the runs.
export function turn<T>(conversationId: string, fn: () => Promise<T>): Promise<T> {
return tracer.startActiveSpan(
"invoke_agent support-bot",
{ attributes: { "gen_ai.operation.name": "invoke_agent", "gen_ai.conversation.id": conversationId } },
async (span) => {
try {
return await fn();
} finally {
span.end();
}
},
);
}
What shows up
- Runs. Each trace is a run on Runs, with one attested step per span — shown as Reported by your code (attested with Developer details on) — carrying its model, provider, operation, timing, status and the token usage your spans report.
- Conversations. A trace with
gen_ai.conversation.idandinvoke_agentspans also appears on Conversations: its turns numbered, its model calls asturn:<n>.<k>steps, and the agent span counting the tool calls in its turn.
What does not show up
- Content. Prompts, completions, chat messages and tool arguments are not read. OTLP content extraction exists behind a server gate that is off, and stays off until ModelRig rules on storing per-call text for traces; until then AI graders have no transcript to read for these runs.
- Cost. Attested steps are not priced: you see the token usage your spans report, not a dollar figure. Calls you want priced go through a ModelRig lane or
rig.recordCall. - A span tree. Parent links are kept only to number turns and count tool calls; there is no waterfall view.
More on the OTLP door: https://modelrig.dev/otlp-ingest.html.