Tracing: Langfuse and OpenTelemetry

Every run becomes a trace: workflow nodes are spans, model calls are generations (model, input, output, tokens), tool calls are tool observations, failures are marked as errors. Export goes over OTLP/HTTP to Langfuse or any OpenTelemetry collector.

from criogenai import Agent, Telemetry

# LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
langfuse = Telemetry.langfuse()
# Or any OpenTelemetry collector:
otel = Telemetry.otlp("http://collector:4318/v1/traces")
# Prompts and answers stay out unless you ask: capture_content=True.

agent = Agent("openai:gpt-5-mini", telemetry=langfuse)  # every run is a trace
await agent.run("Where is order A-1001?", user_id="u-42")
await langfuse.flush()  # before the script ends
import { Agent, Telemetry } from "criogenai";

// LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
const langfuse = Telemetry.langfuse();
// Or any OpenTelemetry collector:
const otel = Telemetry.otlp("http://collector:4318/v1/traces");
// Prompts and answers stay out unless you ask: { captureContent: true }.

const agent = new Agent({ model: "openai:gpt-5-mini", telemetry: langfuse }); // every run is a trace
await agent.run("Where is order A-1001?", { state: { user_id: "u-42" } });
await langfuse.flush(); // before the script ends
use criogenai_telemetry::{Langfuse, OtlpConfig, OtlpExporter};
use serde_json::json;

// LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL
let langfuse = OtlpExporter::start(Langfuse::from_env()?)?;
// Or any OpenTelemetry collector, without message content:
let otel =
    OtlpExporter::start(OtlpConfig::new("http://collector:4318").capture_content(false))?;

let options = agent
    .run_options()
    .observe(langfuse.observer())
    .metadata(json!({"user_id": "u-42", "session_id": "chat-7"}));
agent.run_with("Where is order A-1001?", options).await?;
langfuse.flush().await;

See runs on your own computer#

The local trace viewer shows every run on a page, with no account and nothing leaving the computer: model calls, tool calls and steps on a time axis, durations, tokens, cost (from your prices), failures, and where the time went (model, tools, CriogenAI and your code).

# In a terminal: python -m criogenai.trace   (then open http://127.0.0.1:4318)
from criogenai import Agent, Telemetry

traces = Telemetry.local()  # capture_content=True to see prompts and outputs too
agent = Agent("openai:gpt-5-mini", telemetry=traces)
await agent.run("Where is order A-1001?")
await traces.flush()
// In a terminal: npx criogenai trace   (then open http://127.0.0.1:4318)
const traces = Telemetry.local(); // { captureContent: true } to see prompts and outputs too
const agent = new Agent({ model: "openai:gpt-5-mini", telemetry: traces });
await agent.run("Where is order A-1001?");
await traces.flush();
// In a terminal: criogenai-trace   (then open http://127.0.0.1:4318)
use criogenai_telemetry::{OtlpConfig, OtlpExporter};

let traces = OtlpExporter::start(OtlpConfig::local())?; // .capture_content(true) for prompts
agent
    .run_with(
        "Where is order A-1001?",
        agent.run_options().observe(traces.observer()),
    )
    .await?;
traces.flush().await;
  • Start it with python -m criogenai.trace, npx criogenai trace or the criogenai-trace binary, and open http://127.0.0.1:4318. Runs are kept in ~/.criogen/traces.db (--memory for none): the newest 10 000, for 14 days.

  • Prompts and outputs appear only with content capture on; otherwise you see structure, timing, tokens and error codes.

  • Cost: pass --prices prices.json (USD per million tokens by provider:model or model name, e.g. {"gpt-5-mini": {"input": 0.25, "output": 2}}). CriogenAI ships no price list: a stale price is worse than none.

  • It listens on 127.0.0.1 only. To share one on a network, pass --allow-remote --token-env NAME; clients then send that token.

  • It is an ordinary OTLP/HTTP (JSON) receiver, so the same runs can also go to Langfuse: give an agent one exporter, or run the viewer while you develop and Langfuse in production.

  • Never slows or breaks a run: the observer only queues events; a background task batches and sends them, and retries on transient errors.

  • Sensitive data: in every language, prompts, answers, tool data, error messages (they echo what failed) and the run's user_id, session_id and custom metadata stay out of traces unless you set capture_content=True / captureContent: true / .capture_content(true). Structure, timing, usage, status and error codes always go. A mask function redacts what is exported when capture is on.

  • Metadata: name, release, environment and tags are always attached to the trace; user_id, session_id and any other JSON only with content capture on.