Skip to content

Logging

Teff ships an operational log stream on top of the standard logging module. At INFO it shows the whole run skeleton — which nodes ran, how they routed, which tools fired, and the LLM calls (model + token counts, no text). Prompt/response content is an opt-in DEBUG detail, redacted and truncated, so you can inspect what the model saw without leaking secrets into your console.

Quick start

from teff import configure_logging
from teff.flow import Flow
from teff.node import LLM
from teff.provider import ProviderRegistry
import asyncio

configure_logging()  # INFO -> stderr, text

flow = Flow(
    "chat",
    providers=ProviderRegistry.from_presets("ollama"),
    default_provider="ollama",
    default_model="llama3.1:8b",
)
flow.step(LLM(prompt="Скажи привет", output_key="answer"))
graph = flow.compile()

asyncio.run(graph.run({}, checkpoint_id="thread-42"))
2026-08-03 23:06:13,249 INFO  teff.graph.execution [run=bde23c10 session=thread-42] run_start
2026-08-03 23:06:13,251 INFO  teff.graph.execution [run=bde23c10 session=thread-42 node=start type=log_smoke_hi] node_start
2026-08-03 23:06:13,252 INFO  teff.graph.execution [run=bde23c10 session=thread-42 node=start type=log_smoke_hi] node_end duration_ms=5.3
2026-08-03 23:06:13,252 INFO  teff.graph.execution [run=bde23c10 session=thread-42] run_end status=ok

Levels

Level Events
INFO run_start, run_end, node_start, node_end, edge, llm_call (no text), tool_call
DEBUG Everything above plus llm_request / llm_response content, checkpoint activity
WARNING retry, structured-output validation failures, interrupt pauses
ERROR node_error (and a failing run_end)

So INFO is a readable "skeleton" of the chain — you see every node, every routing decision and every tool invocation — while prompts and answers stay out of the picture. Flip to DEBUG to add the content.

Configuring

from teff import configure_logging

configure_logging()  # INFO, text
configure_logging("debug")  # + prompt/answer content
configure_logging("INFO", format="json")  # single-line JSON per record
configure_logging("debug", format="json")  # JSON with content

format="json" prints one JSON object per line to stdout (text goes to stderr), each carrying run_id, session_id, node_id, node_type, logger, level, event, and the per-event fields:

{"timestamp": "2026-08-03T23:06:13", "level": "INFO", "logger": "teff.graph.execution",
 "event": "node_start", "run_id": "bde23c10", "session_id": "thread-42",
 "node_id": "start", "node_type": "log_smoke_hi"}

When level is omitted it is read from the TEFF_LOG_LEVEL environment variable (default INFO). The LLM content cap is set with TEFF_LOG_LLM_CHARS (default 2000 characters, 0 disables truncation). configure_logging is idempotent — calling it again just switches the level/formatter of the existing handler.

Correlation

Every record is tagged with the enclosing run/session/node through contextvars, so concurrent runs never bleed into one another. The ids come from the run itself: graph.run()/graph.stream() generate a run_id and reuse your checkpoint_id as the session_id.

Log your own events inside a run and they inherit the same ids:

from teff import get_logger

log = get_logger(__name__)


@node("check_stock")
async def check_stock(ctx, state):
    log.info("stock for %s", state.get("sku"))
    return {"stock": 12}

get_logger never attaches handlers — that is solely the job of configure_logging. It simply prefixes teff. to your name so the default filters pick the record up.

Versus RunTracer

RunTracer remains the per-run telemetry: a structured event log you can fold into a RunSummary and persist. The logger is the ops stream: what is happening right now, correlated by run, filterable by level, and greppable in a terminal or log aggregator. Use the tracer when you need a machine-readable report of a finished run; use logging when you are watching (or debugging) a live one.

Entry points

The scaffolded apps wire logging up for you:

uv run python main.py --log-level debug --log-format json
uv run python daemon.py --log-level info --log-format text
uv run python cli.py run "Hello" --log-level debug

Without --log-level the TEFF_LOG_LEVEL env var (or INFO) applies.