← Back
Home › Systems › Agentic AI

agent-observability

Records and displays what LLM agents do, including model calls, tool calls and handoffs, so developers can debug and evaluate them.

LangSmith

Open the agent-observability pageagent-observability Agent tracingagentic-ai: Zstandard (zstd)compression: LZ77 plus Huffman and FSE entropy coding, with levels from LZ4-like speed to xz-like ratio, all decoding at about 1-2 GB/s.›HyperLogLogdata-structures: Counts distinct elements in kilobytes with about 1 % error by tracking the longest run of leading zero bits in hashed values per bucket.›Quantile sketchesdata-structures: Mergeable quantile sketches: t-digest, DDSketch, HDR histogramsPartitioned commit logdistributed: A topic split into replicated append-only partitions: key hash keeps per-key order, offsets give position, and a high watermark limits readers to fully replicated records.›Record replaydistributed: Record every nondeterministic input (time, randomness, I/O, model and tool outputs, per-tick player input) and re-execute deterministically for debugging, replays and state rebuildWindowed stream aggregationdistributed: Group events by event-time window per key, keep one partial aggregate per slice, and emit a window when the watermark passes its end.›Usage meteringfinance: Counting tokens/requests per user and key, aggregating for quotas and billing with idempotent eventsLLM-as-judgemachine-learning: Grade open-ended outputs with a language model under a rubric, then correct its known biases by order swapping, voting and calibration against human labels.›Named entity recognitionmachine-learning: Find the character spans of entities in text and give each a type, by labelling every token and decoding a valid label sequence.›Server-sent eventsnetworking: One long-lived HTTP response carries text events one way; each event may carry an id, so a reconnecting client names its last one and the server resumes.›Data maskingsecurity: Hide sensitive field values from people and systems that don't need them, by masked copies or role-based masking at read time, while keeping joins and formats working.›Secret scanningsecurity: Detect committed credentials with provider-specific patterns, entropy checks and live verification, then revokeColumnar storagestorage: Store each column contiguously so analytic scans read only the columns they need, compress well and run as tight SIMD loops.›Inverted indexstorage: Map each term to a sorted, compressed list of the documents containing it; the core of full-text search and other fast set queries.›LSM-treestorage: Buffer writes in memory, flush them as immutable sorted files and merge in the background; turns random writes into sequential I/O.›Rollup aggregationstorage: Fold raw events into one mergeable partial aggregate per time bucket and dimension key, then merge fine buckets into coarser ones, so long ranges read few rows and stay exact.›Distributed tracingsystems: Give each request a 128-bit id, record timed spans linked by parent ids, pass the ids in a header across every hop, and join the spans back into a tree afterwards.›Telemetry samplingsystems: Head, tail and dynamic sampling of traces, events and logs to a budgetFast hash functionsdata-structures: Non-cryptographic hashes built from 64×64→128-bit multiplies and xor-shifts; pick by key size, trust level and whether the hash is persisted.›Consumer offsetsdistributed: Each consumer group stores one integer per partition, the next position to read, so a restarted consumer resumes where it left off with at-least-once delivery.›Primary-backup replicationdistributed: One node orders all writes and ships them to standbys; the acknowledgement rule (none, one, all) sets what a crash loses, and an external arbiter decides who takes over.›Segmented logdistributed: Split an append-only log into files named by their first offset, write only the newest, and find any record by binary search plus a sparse index.›Zero-copy I/Onetworking: Move bytes between files, sockets and NICs by passing page references instead of copying through user space: sendfile, splice, MSG_ZEROCOPY, kTLS.›Watermarksdistributed: Event-time progress markers that bound lateness and trigger window resultsHierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›Sharded countersconcurrency: Split one hot counter into per-core or per-thread cells on separate cache lines; increments stay local and reads sum the cells.›Event deduplication windowdistributed: Keep the ids of recently accepted events for a time window and drop repeats; memory stays bounded by rotating time buckets instead of per-entry timers.›Idempotency keysfinance: Client-chosen unique key per logical operation, stored with a request fingerprint and the final response, so retries replay the first result instead of acting twice.›Token samplingml-training: Turn a vector of scores into one chosen item with temperature, top-k, top-p, min-p, repetition penalties and a seed.›Annotator agreementstatistics: Inter-annotator agreement measures: Cohen and Fleiss kappa, Krippendorff alpha, gold-question accuracy+ 22 moreFencing tokensdistributed: A number that grows with every lock grant and is checked by the protected resource, so a paused ex-holder's late write is rejected.›Message sequence numbersdistributed: A per-stream counter on every message lets the receiver detect loss, duplicates and reordering, ack cheaply and resume after reconnect with "send from N".›Write-ahead logstorage: Append each change to a checksummed sequential log and fsync it before touching data pages; the basis of crash durability in nearly every database.›Health checkingsystems: Decide per backend whether to route to it using probes and real-traffic errors, with consecutive-count hysteresis, capped ejection and a panic fallback.›d-ary heapdata-structures: Array heap with d children per node (d = 4 typical): half the binary heap's depth, siblings in one cache line, faster priority queues.›Leasesdistributed: A lock that expires on its own: bounded-time ownership that survives crashes and partitions, given bounded clock drift and fencing tokens on writes.›Slab allocatorsystems: Pre-carve pages into equal-size slots per size class with an in-place free list: O(1) alloc and free, no external fragmentation, cache-warm reuse.›False sharingconcurrency: Independent variables written by different cores on one cache line force the line to bounce between cores; pad or regroup hot data to fix it.›Memory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›Cryptographic hashes (SHA-2,BLAKE3)security: Collision-resistant fingerprints of data; SHA-256 for interoperability, BLAKE3 for speed, HMAC/HKDF for keyed uses.›Exponential backoff with jittersystems: Retry after delays that grow geometrically and are randomized, so failed clients spread out instead of hammering a recovering service in synchronized waves.›+ 9 moreCRC32C checksumshardware-os: The Castagnoli CRC-32 with dedicated instructions on x86 (SSE4.2) and ARMv8. It is the standard fast integrity check for storage blocks, logs and network frames.›fsync, O_DSYNC and O_DIRECThardware-os: What fsync, fdatasync, O_DSYNC, O_DIRECT and F_FULLFSYNC really guarantee on Linux and macOS, and the exact call sequences for crash-safe files.›CPU caches and the memoryhierarchyhardware-os: Sizes, line widths and latencies of L1/L2/L3/DRAM on modern x86 and Apple M-series, and the data-layout rules that follow from them.›PRNGs (PCG, xoshiro)numerics: Small, fast, statistically strong non-cryptographic generators: PCG32 and xoshiro256**, with seeding, parallel streams and unbiased ranges.›

How it is built

Architecture overview

An LLM agent is a loop: the model reasons, calls a tool, reads the result, calls the model again, sometimes hands work to a sub-agent. When it fails (wrong answer, runaway cost, a tool called ten times) the cause sits somewhere in a long chain of prompts, outputs and tool results that no single log line explains. Agent observability records that chain as a trace, a tree of timed steps with their full inputs and outputs. It stores traces cheaply, makes them searchable, rolls them up into cost, latency and quality numbers, scores them automatically with another model, and lets an engineer replay a recorded run, either to reproduce a bug or to try a new prompt or model on real inputs.

The idea that makes it work is that a trace is both a log and a dataset. As a log it answers "what happened in this run". As a dataset it is the set of real inputs, intermediate states and outputs that evaluation, regression tests and replay need. The costs are large and sensitive: prompts and tool outputs are big, repeat a lot, and contain user data and sometimes secrets, so sampling, compression and masking are part of the design from the start.

Parts:

 agent app ──spans (model call, tool call, step, parent id)──► mask / scan ──► ingest API
  (SDK, trace context propagated to tools and sub-agents)                          │
                                                                      commit log (partition by trace id)
                                                                                   │
                                          tail sampler: keep errors, slow, costly, low-scoring, a % of the rest
                                                                                   │
                       ┌──────────── trace store ───────────┐              rollup jobs
                       │ span rows (columnar, recent in LSM) │◄──────────  per (project, model, user, hour):
                       │ big payloads (zstd blobs)           │              tokens, cost, latency sketches
                       │ text index over prompts/outputs     │
                       └───────┬───────────┬────────────────┘
          UI: trace tree ◄─SSE─┘           │
          judge workers: score sampled traces ──► scores written back to the trace
          replay runner: stored inputs + recorded step outputs ──► new run ──► compare with the original

How the parts fit together

Instrumentation

agent-tracing (no page yet) turns an agent run into spans: one root span for the run, a child for each model call, tool call, retrieval and sub-agent, each with start and end time, status, and attributes. It builds on Distributed tracing: a trace id and parent span id travel with the work, including into tools that run as separate services and into sub-agents, so the tree is complete. The OpenTelemetry GenAI semantic conventions give the attributes a shared name: for model calls gen_ai.operation.name, gen_ai.request.model and gen_ai.usage.input_tokens, plus span types for creating and invoking an agent and executing a tool; they are marked "Development" status and have moved to their own repository (see Sources). Using them means any backend can read the data.

What differs from ordinary service tracing is the payload. A span for an LLM call carries the full prompt and response, which can be thousands of tokens, and a span can last minutes while a tool or a human is in the loop. So the SDK must not block the agent (batch and send in the background), must be able to send a span at start and again at end, and should flush on process exit so the last, usually most interesting, steps are not lost.

Privacy filter

The trace is a copy of the user's data in a second system, so it is filtered before leaving the process. Data masking replaces fields known to be sensitive (email, card number, phone) with tokens or hashes. Free text needs a detector: Named entity recognition finds names, places and organisations that a regular expression cannot. Agents also handle credentials, so Secret scanning (no page yet) (no page yet) looks for API keys and tokens in prompts and tool outputs, which agents routinely print. The filter has a trade-off to decide up front: masking in the application protects privacy but changes what replay and judges see; masking in the ingest tier is easier to update but means raw data crosses a boundary. Keep the original out of the store unless a use case needs it, and then store it separately with a short retention.

Ingest and sampling

Traces arrive in bursts and out of order, so ingestion first lands in a durable buffer: a Partitioned commit log partitioned by trace id, so that all spans of one run reach the same consumer in order. This decouples the agent from slow storage and makes reprocessing possible. Langfuse's self-hosted design shows a variant of the same idea: events are kept in blob storage, a Redis queue hands jobs to a worker, and the worker writes to the analytical database (Langfuse self-hosting docs, see Sources).

Telemetry sampling (no page yet) decides what to keep. Head sampling decides when a trace starts, which is cheap but random: it drops the rare failure along with the boring runs. Tail sampling decides after the trace is complete, so it can keep every trace that errored, was slow, was expensive or got a low score, and only a fraction of the healthy rest (OpenTelemetry sampling docs). For agents tail sampling is the better fit, at a price: spans of a trace must be buffered until it ends, which is why the commit log is keyed by trace id, and a run that lasts minutes holds its buffer that long. Metrics (counts, tokens, cost) are computed on all traffic before sampling, so sampling does not bias them.

Trace store

The store has four access patterns, which is why it is usually several structures in one system:

  • Writes at high rate with updates. Spans are created, then completed, and may be amended with a score, so the ingest path favours an append-friendly structure: an LSM-tree (sorted in-memory writes flushed to immutable files, merged later). Analytical stores built on merge trees behave this way.
  • Analytics over a few columns. "p95 latency by model last week" touches a handful of columns across millions of spans: Columnar storage, which compresses each column and scans only what the query names.
  • Big payloads. Prompts and outputs repeat large shared prefixes (system prompt, tool schemas, history), so they compress well. Zstandard (zstd) with a trained dictionary or a high level suits this; payloads often live as compressed blobs in object storage, referenced from the span row.
  • Find the needle. "Show runs where the user said refund and the tool errored" is full-text search: an Inverted index over prompt and output text, maintained alongside the rows.

Langfuse documents this split directly: PostgreSQL for transactional data, ClickHouse for observability data, Redis or Valkey for cache and queue, and S3 or blob storage for events and attachments (see Sources).

Aggregation

Dashboards and alerts must not scan raw spans. Windowed stream aggregation groups spans by event-time window and by dimensions (project, model, user, tool); Rollup aggregation keeps those windows as pre-aggregated rows at coarser grains (hour, day), so a month of data reads a few thousand rows. Each rollup holds sums and counts, plus mergeable sketches for what is not summable: Quantile sketches (no page yet) (no page yet) for latency and cost percentiles, HyperLogLog for distinct users and sessions.

Cost is the number agent teams ask for first. The Usage metering pipeline turns token counts from spans into money: tokens by model and kind (input, output, cached) times a versioned price table, grouped by customer or feature, with the same rule as metering elsewhere: the raw spans are the source of truth and the totals can be rebuilt. Price tables change, so the table version used is stored with the result.

Evaluation

Traces say what happened, not whether it was good. LLM-as-judge scores traces with a second model against a rubric (helpful, grounded, followed the instruction, used the right tool). Two modes: online, scoring a sample of live traces so quality shows up on the same dashboard as latency; offline, scoring replays of a fixed dataset before a release. The judge is a measuring instrument with its own errors: Zheng et al. (2023) document position bias, verbosity bias and self-enhancement bias, so scores are validated against a small human-labelled set and the judge prompt is versioned with the scores. Scores are written back to the trace, which is what lets the tail sampler keep low-scoring runs.

Replay

Record and replay (no page yet) uses stored traces as executable material. Two uses:

  • Reproduce a failure. Re-run the agent with every model and tool result served from the recording, so the same path is taken without calling anything; the step order, prompts and arguments can be stepped through and compared. The contract is determinism by recording: the only sources of non-determinism (model sampling, tool results, clock, randomness) are captured and replayed. The same principle drives durable execution engines such as Temporal, which recover a workflow by replaying its stored event history (Temporal docs).
  • What if. Take the recorded inputs (and optionally recorded tool results), call a new prompt or model live, and compare outputs and judge scores with the original. Tool results come from the recording because re-calling real tools may have side effects.

The seam to watch is divergence. As soon as the new run makes a call the recording does not contain (a different tool or different arguments), the recording cannot answer; the replay must either stop and flag it or fall back to a live call, and a replay that touches live side-effecting tools must be sandboxed.

Live view

A running agent can take minutes, so the UI should show the trace as it grows. Server-sent events is a fit: one long-lived HTTP response per viewer, the server pushes each new span or token as an event, the browser reconnects on its own with Last-Event-ID. The server side reads the same commit log (or a live buffer) as the store, so the live view and the stored view cannot disagree.

Data and control flow

One agent run.

  1. The SDK opens a root span and propagates the trace id to every model call, tool call and sub-agent.
  2. Each step builds a span with its inputs and outputs; the privacy filter masks and scans it; the SDK sends it in a background batch. Request latency impact: a queue append (order of microseconds), not a network call.
  3. The ingest API validates and appends the span to the commit log partition of its trace id and also forwards it to the live-view channel.
  4. When the root span ends, the tail sampler sees the whole trace with its status, cost and any score, and keeps or drops it. Counters and sketches were already updated for every trace.
  5. Kept traces are written to the store: rows into columns, payloads into zstd blobs, text into the index.
  6. A judge worker picks a sample, scores it and writes the score back, which may rescue a trace the sampler would have dropped.

One replay.

  1. The user selects a stored trace and a change (new prompt, new model, or none).
  2. The runner loads the original inputs and the recorded results of every step.
  3. It runs the agent with calls answered from the recording where the call matches, live where the change makes them differ.
  4. It stores the new trace linked to the original, runs the judge on both, and shows the diff.

Scaling

In the order the limits show up:

  1. Payload volume. Prompts dominate bytes. Levers: compress with a dictionary, store shared prefixes once, deduplicate repeated system prompts, move bodies to object storage, expire bodies earlier than metadata.
  2. Write rate. Spans per second grow with agents times steps. Levers: batch in the SDK, partition the commit log by trace id, scale ingest consumers horizontally.
  3. Query cost. Dashboards on raw spans fall over. Levers: rollups per window and dimension, sketches for percentiles, materialised views per common question.
  4. Judge cost. Every judged trace is another model call. Levers: score a sample, score only the tails (errors, long runs), use a cheaper judge for a first pass.
  5. Retention and privacy. Longer retention raises both cost and risk. Levers: tiered retention (rollups long, bodies short), masking at the source, deletion by user id.

Failure modes

FailureSymptomCauseFix
Broken trace treespans orphaned, sub-agent steps missingtrace context not propagated into tools or sub-agents, or into threads and background taskspropagate context explicitly, test with a nested example
Lost tail of a runlast steps absentprocess exited before the SDK flushedflush on shutdown, send a start event early
Sampling biasdashboards disagree with realitymetrics computed after sampling, or only errors keptcount before sampling, keep a uniform sample alongside the biased one
PII or secret in the storeprivacy or security incidentmasking only on known fields, free text unscannedNER and secret scanning before send, short retention for raw payloads
Cost surprisebill higher than the dashboardprice table stale, cached or reasoning tokens counted wronglyversion the table, record token kinds separately, reconcile with the provider's invoice
Replay divergesreplay differs from the original without a changean unrecorded source of non-determinism (time, random, a live tool)record it, or mark the step live and sandbox it
Replay hits real systemsemails sent, tickets created during a "what if"tools called liveserve tool results from the recording, stub write tools
Judge driftquality score moves with no change in the agentjudge model or prompt changed, or judge biasespin the judge version, track agreement with human labels
Slow trace UItree takes long to openvery long runs with thousands of spans and inline payloadslazy-load payloads, collapse repeated steps, paginate

Real implementations

namewhat it isrepolicencenotes
LangSmithhosted tracing, evaluation and prompt tooling for LLM appsnone public for the platform; docsproprietarytraces, datasets, online evaluators
Langfuseopen-source LLM engineering platform: tracing, evaluation, promptslangfuse/langfuseMIT, except the ee directories under a separate licenceself-hosting: web, worker, Postgres, ClickHouse, Redis, blob store (docs)
Arize Phoenixopen-source tracing and evaluation, OpenTelemetry basedArize-ai/phoenixElastic License 2.0source-available, not OSI open source
OpenTelemetry GenAI conventionsshared span, metric and event names for model, agent and tool callsopen-telemetry/semantic-conventions-genaiApache-2.0status "Development" in the repo
OpenLLMetryOpenTelemetry instrumentation for LLM librariestraceloop/openllmetryApache-2.0emits standard spans to any backend
MLflow Tracingtracing and evaluation inside MLflowmlflow/mlflowApache-2.0part of the broader MLflow platform
W&B Weavetracing and evaluation toolkitwandb/weaveApache-2.0Python and TypeScript SDKs
Heliconeproxy-based LLM observabilityHelicone/heliconeApache-2.0records calls at a gateway, no code change
Temporaldurable execution by event-history replaytemporalio/temporalMITnot an observability tool; the proven replay mechanism (docs)

Sources

Similar systems

Share the most building blocks with this one.