Skip to main content

How to send traces to Logfire and LangSmith

Pydantic Logfire and LangSmith are both OpenTelemetry backends. Agentflow already reconstructs a full span tree (graph → node → LLM → tool) with GenAI semantic-convention attributes (gen_ai.usage.input_tokens, gen_ai.request.model, session.id, …) through its OtelPublisher. So sending traces to either backend means configuring the right OpenTelemetry TracerProvider/exporter — there is no per-vendor event plumbing.

You have three ways to wire it up:

  • Python helperssetup_logfire, setup_langsmith, or the unified setup_observability.
  • Dedicated publishersLogfirePublisher / LangsmithPublisher, if you prefer a publisher object to assign or compose.
  • Declarative config — an observability block in agentflow.json (auto-wired by the API server; see below).

Install

pip install '10xscale-agentflow[logfire]'        # Logfire
pip install '10xscale-agentflow[langsmith]' # LangSmith (OTLP HTTP exporter)
pip install '10xscale-agentflow[observability]' # both + otel

The langsmith extra pulls only the OpenTelemetry OTLP HTTP exporter — not the LangSmith SDK — because spans are sent over OTLP, not RunTree.


Secrets stay in the environment

Never put tokens in code or agentflow.json. Set them as environment variables:

# Logfire
export LOGFIRE_TOKEN="your-logfire-write-token"

# LangSmith
export LANGSMITH_API_KEY="your-langsmith-api-key"

Both helpers fall back to these variables when you do not pass token= / api_key= explicitly.


Logfire

Call setup_logfire(graph, ...) before graph.compile(). It runs logfire.configure(...) to install the global TracerProvider, then attaches the OtelPublisher.

from agentflow.core.graph import StateGraph, Agent
from agentflow.runtime.publisher import setup_logfire, ObservabilityLevel
from agentflow.utils import END

graph = StateGraph()
graph.add_node("MAIN", Agent(model="gpt-4o"))
graph.set_entry_point("MAIN")
graph.add_edge("MAIN", END)

# Configure Logfire and instrument the graph — before compile()
setup_logfire(
graph,
service_name="my-agent",
level=ObservabilityLevel.STANDARD,
)

app = graph.compile()

setup_logfire accepts token, service_name, send_to_logfire (default True), console (pass False to silence local console output), level, and any extra keyword arguments forwarded verbatim to logfire.configure() (e.g. environment="staging").


LangSmith

Call setup_langsmith(graph, ...) before graph.compile(). It builds an OTLP HTTP exporter pointing at LangSmith, wraps it in a BatchSpanProcessor, and attaches the OtelPublisher.

from agentflow.runtime.publisher import setup_langsmith, ObservabilityLevel

setup_langsmith(
graph,
project="my-agent", # sent as the Langsmith-Project header
level=ObservabilityLevel.STANDARD,
)

app = graph.compile()

For a regional deployment, pass the full base endpoint (Agentflow appends /v1/traces):

setup_langsmith(graph, project="my-agent", endpoint="https://eu.api.smith.langchain.com/otel")

Both at once

setup_observability reads a config dict and enables Logfire and/or LangSmith. When both are on, they share a single TracerProvider (the LangSmith processor is passed to Logfire via additional_span_processors):

from agentflow.runtime.publisher import setup_observability

setup_observability(graph, {
"level": "standard",
"logfire": {"enabled": True, "service_name": "my-agent"},
"langsmith": {"enabled": True, "project": "my-agent"},
})

app = graph.compile()

Dedicated publishers

If you prefer a publisher object — for example to fan out with CompositePublisher — use LogfirePublisher or LangsmithPublisher. They subclass OtelPublisher and configure the provider on construction, so assign them before compile():

from agentflow.runtime.publisher import LangsmithPublisher, ObservabilityLevel

publisher = LangsmithPublisher(project="my-agent", level=ObservabilityLevel.STANDARD)
graph = StateGraph(publisher=publisher)
# ... add nodes, edges
app = graph.compile()

LogfirePublisher takes the same options as setup_logfire; LangsmithPublisher takes the same options as setup_langsmith.


Observability levels and PII

The level controls how much data lands on each span. It reuses ObservabilityLevel:

LevelWhat it emitsPII risk
SPANSTiming and structure onlyNone
STANDARD (default)+ token counts, model, request params. No message content.Low
FULL+ prompt and completion contentHigh — opt in deliberately

FULL puts prompt/response text on spans. The framework's log redaction (install_secret_redaction()) does not scrub span content, so treat FULL traces as sensitive and restrict who can view them in Logfire/LangSmith.


Declarative config in agentflow.json

When you serve a graph with agentflow api, you do not call the helpers yourself. Add an observability block to agentflow.json and the server wires it up during startup:

{
"agent": "graph.react:app",
"observability": {
"level": "standard",
"logfire": { "enabled": true, "service_name": "my-agent", "send_to_logfire": true, "console": false },
"langsmith": { "enabled": true, "project": "my-agent", "endpoint": null }
}
}

Keep LOGFIRE_TOKEN / LANGSMITH_API_KEY in your .env — never in agentflow.json. If a backend is enabled but its package or key is missing, the server logs a warning and starts without that exporter rather than failing.