<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ai-Agents on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/ai-agents/</link><description>Recent content in Ai-Agents on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Tue, 09 Jun 2026 16:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/ai-agents/index.xml" rel="self" type="application/rss+xml"/><item><title>The contractor with the master key: isolating AI agents from workstation to cluster</title><link>https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/</link><pubDate>Tue, 09 Jun 2026 16:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/</guid><description>&lt;blockquote>
&lt;p>First post of a pair on &lt;strong>AI agent isolation&lt;/strong>. This one fixes the &lt;em>what&lt;/em> and the &lt;em>where&lt;/em>: the complete map of isolation primitives and which domain each belongs to. The &lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">sibling runbook&lt;/a> fixes the &lt;em>how&lt;/em>, with commands: &lt;code>ai-jail&lt;/code> and bubblewrap on the client, Tetragon &lt;code>TracingPolicy&lt;/code> and &lt;code>RuntimeClass&lt;/code> on the cluster. If you are only going to read one, this one gives you the mental model; the other one gives you the files to copy and paste.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An AI agent that runs code needs access to your filesystem and your tools: compiler, linter, &lt;code>grep&lt;/code>, &lt;code>make&lt;/code>, &lt;code>cargo&lt;/code>, &lt;code>npm&lt;/code>. That is the minimum to be useful. The problem is that alongside that access travels the ability to read &lt;code>~/.aws/credentials&lt;/code>, exfiltrate your SSH keys or fire an &lt;code>rm -rf&lt;/code> outside the project directory. And no malicious model is needed: a compromised dependency in an &lt;code>npm install&lt;/code> is enough, because the well-meaning agent and the poisoned &lt;em>post-install script&lt;/em> &lt;strong>run with the same permissions&lt;/strong>. The answer is not to trust the LLM&amp;rsquo;s good intentions; it is to &lt;strong>isolate in order to bound the blast radius&lt;/strong>. This post walks the five isolation families of 2026, from the process sandbox to the full VM, and sorts them into two columns: what applies on the &lt;strong>client&lt;/strong> (the developer&amp;rsquo;s workstation: bubblewrap, &lt;code>ai-jail&lt;/code>, sandbox-exec, Landlock, the native Claude Code and Codex sandboxes) and what applies on the &lt;strong>cluster&lt;/strong> (where the agent or the inference runs on Kubernetes: namespaces+seccomp, gVisor, Firecracker/Kata microVMs and &lt;strong>eBPF/Tetragon&lt;/strong> as the layer for live observation and enforcement). The thesis: the threat model is the same in both places; the tools are not. Policy extrapolates; the primitive gets rewritten.&lt;/p>
&lt;h2 id="the-analogy-the-contractor-with-the-master-key">The analogy: the contractor with the master key&lt;/h2>
&lt;p>You hire a builder for a renovation. He is competent and acts in good faith. But two things happen that you do not control. The first: he may misread the instruction and knock down the wrong partition wall. The second, worse: his toolbox may have been tampered with before he came through your door — somebody put something inside — and when he opens it in your living room, that something activates.&lt;/p>
&lt;p>Nobody sensible gives him the &lt;strong>master key&lt;/strong> to the whole building. You open the room he works in, you leave him the tools he needs, and you keep the study with the safe and the server room locked. If the renovation goes wrong, by error or by sabotage, the damage stays in that room.&lt;/p>
&lt;p>An AI agent is that contractor. The sandbox is the key policy: &lt;strong>you give it the project room and the tools, not the master key to the system.&lt;/strong> And here is the twist that justifies two posts: the builder works in two different buildings. One is &lt;strong>your flat&lt;/strong>, the developer&amp;rsquo;s workstation, with your credentials, your &lt;code>~/.ssh&lt;/code>, the browser&amp;rsquo;s password vault. The other is &lt;strong>the datacenter&lt;/strong>, the cluster where inference and autonomous agents serve customers, with data from several tenants at once. The key policy is identical in both: least privilege, bound the radius. But the lock on your flat&amp;rsquo;s door is not the same as the one at the datacenter. In the flat you fit a cylinder lock (&lt;code>bubblewrap&lt;/code>). At the datacenter you put a guard watching every door and a separate wing of the building (Tetragon + microVM). Same principle, different hardware. That is &lt;strong>extrapolating the technology&lt;/strong>, not copying it.&lt;/p>
&lt;h2 id="the-threat-model-what-a-runaway-agent-can-do">The threat model: what a runaway agent can do&lt;/h2>
&lt;p>Before choosing a lock it helps to enumerate the burglar. The attack surface of an agent that executes arbitrary bash breaks down into five concrete threats. Not all are defended by the same layer and, an uncomfortable fact that the security documentation itself acknowledges, &lt;strong>no single layer covers them all&lt;/strong>.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Threat&lt;/th>
&lt;th>What the agent does&lt;/th>
&lt;th>Minimum layer that cuts it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Filesystem out of scope&lt;/strong>&lt;/td>
&lt;td>Reads &lt;code>.env&lt;/code>, &lt;code>~/.ssh/id_rsa&lt;/code>, system secrets; modifies sources outside the project&lt;/td>
&lt;td>Process sandbox (path allowlist)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Arbitrary network egress&lt;/strong>&lt;/td>
&lt;td>Exfiltrates data, receives instructions from a remote C2, calls unauthorised APIs&lt;/td>
&lt;td>Network blocking / NetworkPolicy / microVM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Kernel syscall surface&lt;/strong>&lt;/td>
&lt;td>A kernel exploit from inside the container escalates to the host (shared kernel)&lt;/td>
&lt;td>gVisor or microVM (dedicated kernel)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Cross-tenant leakage&lt;/strong>&lt;/td>
&lt;td>One customer&amp;rsquo;s workload reads another&amp;rsquo;s data on a multi-tenant platform&lt;/td>
&lt;td>microVM (de facto standard)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Secret exfiltration&lt;/strong>&lt;/td>
&lt;td>Extracts tokens and environment variables via &lt;code>/proc&lt;/code> or the environment&lt;/td>
&lt;td>&lt;code>--clearenv&lt;/code> / tmpfs &lt;code>$HOME&lt;/code> / secrets outside the pod&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>There is a sixth threat that &lt;strong>no sandbox solves&lt;/strong>: &lt;em>prompt injection&lt;/em>. If an attacker manages to slip instructions into the agent&amp;rsquo;s context — a poisoned comment in the code, a malicious file the agent reads, an adversarial response from a tool — the agent will execute those instructions with whatever permissions the sandbox grants it. Isolation &lt;strong>shrinks the impact radius&lt;/strong> of a successful injection; it does not prevent the injection. That is why the sandbox is a layer, not the solution: on top go input validation, tool-call allowlists and output auditing. The sentence to internalise: &lt;em>isolation does not make the agent trustworthy; it bounds what an untrustworthy agent can break.&lt;/em>&lt;/p>
&lt;h2 id="two-domains-one-policy">Two domains, one policy&lt;/h2>
&lt;p>The builder works in two buildings. The distribution of tools reads better in two columns:&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two-column map: client domain (workstation) and cluster domain, with their isolation layers and the shared policy bridge">
&lt;defs>&lt;marker id="am" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="26" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="currentColor">Same least-privilege policy · different primitive&lt;/text>
&lt;!-- client column -->
&lt;rect x="24" y="46" width="360" height="350" rx="10" fill="#eef4fb" stroke="#1f5fa8" stroke-width="1.6"/>
&lt;text x="204" y="72" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="#0d3a66">CLIENT · dev workstation&lt;/text>
&lt;text x="204" y="90" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#0d3a66">one process, one user, personal data&lt;/text>
&lt;rect x="48" y="106" width="312" height="50" rx="6" fill="#d4ecff" stroke="#1f5fa8" stroke-width="1.2"/>
&lt;text x="204" y="126" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Process sandbox&lt;/text>
&lt;text x="204" y="143" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">bubblewrap (Linux) · sandbox-exec (macOS)&lt;/text>
&lt;rect x="48" y="166" width="312" height="42" rx="6" fill="#dff0ff" stroke="#1f5fa8" stroke-width="1.1"/>
&lt;text x="204" y="183" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Landlock (LSM, 2nd VFS barrier)&lt;/text>
&lt;text x="204" y="199" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">defence in depth, kernel ≥5.13&lt;/text>
&lt;rect x="48" y="218" width="312" height="42" rx="6" fill="#eaf4ff" stroke="#1f5fa8" stroke-width="1.1"/>
&lt;text x="204" y="235" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#0d3a66">Dev container (optional)&lt;/text>
&lt;text x="204" y="251" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#0d3a66">reproducibility + reset, shared kernel&lt;/text>
&lt;text x="204" y="284" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#0d3a66">What wraps it:&lt;/text>
&lt;text x="204" y="304" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">ai-jail · Claude Code /sandbox&lt;/text>
&lt;text x="204" y="320" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">Codex --sandbox · Cursor /worktree&lt;/text>
&lt;text x="204" y="352" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#0d3a66">Safety net: git remote with no push&lt;/text>
&lt;text x="204" y="376" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#555">startup ~0 ms · no daemon&lt;/text>
&lt;!-- cluster column -->
&lt;rect x="436" y="46" width="360" height="350" rx="10" fill="#f3eefb" stroke="#5a2db0" stroke-width="1.6"/>
&lt;text x="616" y="72" text-anchor="middle" font-family="sans-serif" font-size="14" font-weight="700" fill="#42208a">CLUSTER · inference / agents in prod&lt;/text>
&lt;text x="616" y="90" text-anchor="middle" font-family="sans-serif" font-size="11" fill="#42208a">multi-pod, multi-tenant, RKE2&lt;/text>
&lt;rect x="460" y="106" width="312" height="50" rx="6" fill="#e6d9f2" stroke="#5a2db0" stroke-width="1.2"/>
&lt;text x="616" y="126" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">Pod baseline&lt;/text>
&lt;text x="616" y="143" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">namespaces + seccomp + cgroups + NetworkPolicy&lt;/text>
&lt;rect x="460" y="166" width="312" height="42" rx="6" fill="#ede1f7" stroke="#5a2db0" stroke-width="1.1"/>
&lt;text x="616" y="183" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">gVisor (runsc) — user-space kernel&lt;/text>
&lt;text x="616" y="199" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">cuts the syscall surface to the host&lt;/text>
&lt;rect x="460" y="218" width="312" height="42" rx="6" fill="#f0e6fb" stroke="#5a2db0" stroke-width="1.1"/>
&lt;text x="616" y="235" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="600" fill="#42208a">Firecracker / Kata microVM — own kernel&lt;/text>
&lt;text x="616" y="251" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#42208a">isolation by construction for untrusted code&lt;/text>
&lt;rect x="460" y="270" width="312" height="62" rx="6" fill="#fde9d6" stroke="#a85a00" stroke-width="1.6"/>
&lt;text x="616" y="290" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#8a4a00">eBPF / Tetragon (what we already have)&lt;/text>
&lt;text x="616" y="307" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#8a4a00">watches every exec · connect · open in the kernel&lt;/text>
&lt;text x="616" y="322" text-anchor="middle" font-family="sans-serif" font-size="10" fill="#8a4a00">and kills (Sigkill) whatever strays from the script&lt;/text>
&lt;text x="616" y="356" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#42208a">Safety net: GitOps + PR review&lt;/text>
&lt;text x="616" y="376" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#555">startup &amp;lt;1 s (microVM) · runtime layer always on&lt;/text>
&lt;!-- bridge -->
&lt;path d="M384,221 L436,221" stroke="#666" stroke-width="1.6" fill="none" marker-end="url(#am)"/>
&lt;path d="M436,241 L384,241" stroke="#666" stroke-width="1.6" fill="none" marker-end="url(#am)"/>
&lt;text x="410" y="415" text-anchor="middle" font-family="sans-serif" font-size="11" font-style="italic" fill="#555">extrapolate: each client control has its cluster analogue (equivalence table in the runbook)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-client-isolating-the-agent-on-the-workstation">The client: isolating the agent on the workstation&lt;/h2>
&lt;p>Here the agent is a coding assistant — Claude Code, Codex, OpenCode, Cursor — that a developer launches on their machine. One process, one user, and right next to it the juiciest assets that exist: &lt;code>~/.aws/credentials&lt;/code>, &lt;code>~/.ssh&lt;/code>, &lt;code>~/.gnupg&lt;/code>, the browser&amp;rsquo;s password store. The matching tier is the lightest one: the &lt;strong>process sandbox&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>bubblewrap (Linux) and sandbox-exec (macOS).&lt;/strong> &lt;code>bubblewrap&lt;/code> (&lt;code>bwrap&lt;/code>) is the same sandbox Flatpak uses to isolate each desktop app: a ~50 KB binary, ~4,000 lines of C, maintained by the GNOME team and, the key property, it runs &lt;strong>without root&lt;/strong> via &lt;code>CLONE_NEWUSER&lt;/code>, creating namespaces with no elevated privileges. It mounts &lt;code>$HOME&lt;/code> as an ephemeral tmpfs and only exposes the project directory, writable; the rest of the system becomes invisible. On macOS the equivalent is &lt;code>sandbox-exec&lt;/code> with SBPL profiles: a legacy Apple API, officially deprecated and with no public replacement, but working today. Parity between the two is not exact — on macOS the GPU (Metal) and the display (Cocoa) are system services and &lt;code>sandbox-exec&lt;/code> does not restrict them — but both protect what matters: access to the sensitive areas of the filesystem.&lt;/p>
&lt;p>&lt;strong>Landlock as a second barrier.&lt;/strong> &lt;code>bubblewrap&lt;/code> isolates via &lt;em>namespaces and mounts&lt;/em>; Landlock, a Linux Security Module available since kernel 5.13, restricts access at the &lt;strong>VFS&lt;/strong> level, independently of namespaces. It does not replace &lt;code>bwrap&lt;/code>: it complements it. It closes vectors that mount-based isolation does not cover on its own (escape routes via &lt;code>/proc&lt;/code>, symlink tricks inside allowed mounts) and acts as a safety net should the namespace machinery have a bug. It is defence in depth inside the client itself, and it degrades cleanly to a no-op on kernels that do not support it.&lt;/p>
&lt;p>&lt;strong>Dev containers, when reproducibility is needed.&lt;/strong> A dev container (&lt;code>devcontainer.json&lt;/code>, what Codespaces and Cursor use) is a Docker container with a configuration layer on top. It gives reasonable filesystem isolation and easy &lt;em>reset&lt;/em> (destroy and recreate), but it &lt;strong>shares the host kernel&lt;/strong>, the same limitation as any Docker, and it tends to be long-lived, accumulating state. For an agent running your own team&amp;rsquo;s code on your own machine it is a good repeatability story; it is not, on its own, the isolation layer for untrusted code.&lt;/p>
&lt;p>&lt;strong>What wraps all of this.&lt;/strong> A hand-written bash script works, but it does not scale to a team. The tools that package it:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>ai-jail&lt;/code>&lt;/strong> (Rust, GPL-3.0): wraps &lt;code>bwrap&lt;/code>/&lt;code>sandbox-exec&lt;/code> with per-project config in a &lt;code>.ai-jail&lt;/code> file (TOML, committable to the repo, so the whole team inherits the same policy), auto-detection of GPU/Docker/display, a &lt;code>--lockdown&lt;/code> mode (project read-only, network cut with &lt;code>--unshare-net&lt;/code>, &lt;code>--clearenv&lt;/code>), &lt;code>--dry-run&lt;/code> for auditing, and &lt;code>--bootstrap&lt;/code> to generate each agent&amp;rsquo;s permission allowlists. It is tool-agnostic: the same binary serves Claude, Codex, OpenCode or Crush. It also applies Landlock automatically on 5.13+ kernels as defence in depth.&lt;/li>
&lt;li>&lt;strong>The Claude Code &lt;code>/sandbox&lt;/code>&lt;/strong>: since October 2025 Claude Code ships its own sandbox that uses, exactly, &lt;code>bubblewrap&lt;/code> on Linux and &lt;code>sandbox-exec&lt;/code> on macOS. Its &lt;em>Sandboxed Bash&lt;/em> isolates shell commands, but &lt;strong>not&lt;/strong> the file tools, the MCP servers or the hooks, which run with the process&amp;rsquo;s full permissions unless you enable the beta &lt;code>sandbox-runtime&lt;/code> package, which wraps the whole process. There is one nuance worth knowing: if a command fails because of a restriction, the agent can retry with &lt;code>dangerouslyDisableSandbox&lt;/code> — it is &lt;em>opt-out&lt;/em>, not &lt;em>opt-in&lt;/em>.&lt;/li>
&lt;li>&lt;strong>Codex CLI&lt;/strong>: three modes via &lt;code>--sandbox&lt;/code> (&lt;code>read-only&lt;/code>, &lt;code>workspace-write&lt;/code>, &lt;code>danger-full-access&lt;/code>); the recommended default is &lt;code>workspace-write&lt;/code>. The philosophy is deliberate: Codex does not provide the isolation, it delegates it to the environment that wraps it. &lt;code>danger-full-access&lt;/code> only makes sense &lt;strong>inside&lt;/strong> a microVM.&lt;/li>
&lt;li>&lt;strong>Cursor&lt;/strong>: its cloud agents run in isolated VMs; &lt;code>/worktree&lt;/code> creates a single-use isolated worktree per task, and &lt;code>/best-of-n&lt;/code> launches several attempts in parallel in separate worktrees.&lt;/li>
&lt;/ul>
&lt;h2 id="the-cluster-isolating-the-agent-in-production">The cluster: isolating the agent in production&lt;/h2>
&lt;p>The second building is the datacenter. Here the &amp;ldquo;agent&amp;rdquo; can be an autonomous agent running with no human in front of it, or the inference service itself executing generated code, or a multi-tenant workload where one customer&amp;rsquo;s pod must not touch another&amp;rsquo;s data. The process is no longer one: they are pods in a Kubernetes cluster (RKE2/RKE3 on a typical sovereign platform). The primitives change in nature.&lt;/p>
&lt;p>&lt;strong>The pod baseline.&lt;/strong> First of all, the standard kit: Linux namespaces, &lt;code>seccomp&lt;/code> (&lt;code>RuntimeDefault&lt;/code>) to trim the syscall surface, cgroups for resource limits, an unprivileged &lt;code>securityContext&lt;/code> (&lt;code>runAsNonRoot&lt;/code>, &lt;code>readOnlyRootFilesystem&lt;/code>, dropping all capabilities) and &lt;strong>NetworkPolicy&lt;/strong> to cut egress. It is the cluster equivalent of the process sandbox allowlist. Necessary, but it shares the kernel with the host: insufficient for genuinely untrusted code.&lt;/p>
&lt;p>&lt;strong>gVisor (&lt;code>runsc&lt;/code>).&lt;/strong> Google&amp;rsquo;s user-space kernel: it intercepts the workload&amp;rsquo;s syscalls before they reach the host kernel and serves them inside a Linux kernel reimplemented in Go (the &lt;em>Sentry&lt;/em>). The surface exposed to host kernel vulnerabilities drops sharply, while keeping fast startup and a low footprint. It is the middle ground when the risk of a kernel escape is real but the overhead of a microVM is not acceptable.&lt;/p>
&lt;p>&lt;strong>Firecracker / Kata microVMs.&lt;/strong> The de facto standard for untrusted code in 2026. Firecracker (AWS&amp;rsquo;s VMM written in Rust, on top of KVM) gives each sandbox a &lt;strong>dedicated Linux kernel&lt;/strong>: a kernel exploit inside the microVM does not reach the host &lt;em>by construction&lt;/em>. It is what sits underneath Vercel Sandbox (GA January 2026) and E2B. On Kubernetes, &lt;strong>Kata Containers&lt;/strong> brings that model to a &lt;code>RuntimeClass&lt;/code>: you mark the untrusted agent&amp;rsquo;s pod with &lt;code>runtimeClassName: kata&lt;/code> and it runs in its own microVM instead of sharing the node&amp;rsquo;s kernel. For multi-tenant with generated code, this is the baseline, not the luxury.&lt;/p>
&lt;p>&lt;strong>eBPF / Tetragon: the layer we already have.&lt;/strong> Here is the piece that separates a platform with runtime observability from one that only trusts its configuration. The previous layers are &lt;em>static&lt;/em>: they define what the pod can do before it starts. &lt;strong>Tetragon&lt;/strong>, Cilium&amp;rsquo;s runtime security component, based on eBPF, is &lt;em>dynamic&lt;/em>: it observes, in the kernel and at minimal cost, &lt;strong>every&lt;/strong> process execution, &lt;strong>every&lt;/strong> network connection and &lt;strong>every&lt;/strong> file open in every pod, and it can act inline. It does not replace the sandbox; it watches it from inside the kernel. Where &lt;code>bubblewrap&lt;/code> on the client blocks &lt;code>curl&lt;/code> with a command blocklist, Tetragon on the cluster hooks &lt;code>tcp_connect&lt;/code> in the kernel and, if the destination is not allowed, &lt;strong>kills the process with &lt;code>Sigkill&lt;/code>&lt;/strong> before the packet leaves. Where the client hides &lt;code>~/.ssh&lt;/code> behind a tmpfs, Tetragon hooks &lt;code>security_file_open&lt;/code> and reports, or kills, any attempt to read a sensitive mounted path. It is the guard patrolling the corridors while the microVMs are the walls. And it is exactly the kind of control that materialises the ENS monitoring and traceability measures (&lt;code>op.mon&lt;/code>, &lt;code>op.exp&lt;/code>) without instrumenting the application: visibility lives in the kernel, not in the agent&amp;rsquo;s code.&lt;/p>
&lt;h2 id="the-landscape-table">The landscape table&lt;/h2>
&lt;p>The five families, their relative isolation strength, their startup cost and the domain where they live:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tier&lt;/th>
&lt;th>Primitive&lt;/th>
&lt;th style="text-align:center">Isolation&lt;/th>
&lt;th style="text-align:center">Startup&lt;/th>
&lt;th>Natural domain&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Process sandbox&lt;/td>
&lt;td>Seatbelt · bubblewrap&lt;/td>
&lt;td style="text-align:center">Baseline&lt;/td>
&lt;td style="text-align:center">~0 ms&lt;/td>
&lt;td>&lt;strong>Client&lt;/strong> (Claude Code default)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dev container&lt;/td>
&lt;td>Docker + seccomp&lt;/td>
&lt;td style="text-align:center">Moderate&lt;/td>
&lt;td style="text-align:center">seconds&lt;/td>
&lt;td>Client / cluster (repeatability)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>User-space kernel&lt;/td>
&lt;td>gVisor (&lt;code>runsc&lt;/code>)&lt;/td>
&lt;td style="text-align:center">Strong&lt;/td>
&lt;td style="text-align:center">ms&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (medium multi-tenant)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>microVM&lt;/td>
&lt;td>Firecracker · Kata&lt;/td>
&lt;td style="text-align:center">Strongest (practical)&lt;/td>
&lt;td style="text-align:center">&amp;lt;1 s&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (untrusted code)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Full VM&lt;/td>
&lt;td>KVM · EC2&lt;/td>
&lt;td style="text-align:center">Maximum&lt;/td>
&lt;td style="text-align:center">30 s+&lt;/td>
&lt;td>Cluster (external boundary, compliance)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;em>Runtime enforcement&lt;/em>&lt;/td>
&lt;td>&lt;strong>eBPF · Tetragon&lt;/strong>&lt;/td>
&lt;td style="text-align:center">&lt;em>Cross-cutting&lt;/em>&lt;/td>
&lt;td style="text-align:center">&lt;em>always on&lt;/em>&lt;/td>
&lt;td>&lt;strong>Cluster&lt;/strong> (watches+kills on top of any tier)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tetragon gets its own row on purpose: it is not a rung on the ladder, it is a &lt;strong>cross-cutting layer&lt;/strong> that operates &lt;em>on top of&lt;/em> any of the others. It stacks with all of them.&lt;/p>
&lt;p>A numerical note on why the &amp;ldquo;startup&amp;rdquo; column decides as much as the &amp;ldquo;isolation&amp;rdquo; column. A full VM wins on raw isolation but takes tens of seconds to provision; for an agent that needs a fresh environment &lt;em>per request or per session&lt;/em>, that cost is prohibitive. A Firecracker microVM starts in &lt;strong>under 1 second&lt;/strong> and a process sandbox in &lt;strong>~0 ms&lt;/strong>. That is why the dominant pattern in 2026 is not &amp;ldquo;the most isolated VM&amp;rdquo;, but &lt;strong>a full VM as the external boundary plus a microVM as the per-request execution unit inside&lt;/strong>, the architecture of Vercel, AWS Lambda and E2B. On the client the arithmetic is the opposite: the developer launches the agent dozens of times a day interactively, and a startup measured in seconds would break the flow; hence the process sandbox, with its microsecond overhead, is the correct default.&lt;/p>
&lt;h2 id="extrapolate-do-not-copy">Extrapolate, do not copy&lt;/h2>
&lt;p>The thesis of this pair of posts fits in one sentence: &lt;strong>the threat model is invariant across domains; the primitive that implements it is not.&lt;/strong> The client and the cluster defend exactly the same five vectors — filesystem, network, kernel, multi-tenant, secrets — but with toolboxes that do not overlap. Every control has its twin on the other side:&lt;/p>
&lt;ul>
&lt;li>&lt;code>$HOME&lt;/code> as an ephemeral tmpfs (client) ↔ &lt;code>readOnlyRootFilesystem&lt;/code> + &lt;code>emptyDir&lt;/code> (cluster).&lt;/li>
&lt;li>A &lt;code>curl&lt;/code>/&lt;code>wget&lt;/code> blocklist in &lt;code>bwrap&lt;/code> (client) ↔ a &lt;code>TracingPolicy&lt;/code> on &lt;code>tcp_connect&lt;/code> in Tetragon plus NetworkPolicy (cluster).&lt;/li>
&lt;li>&lt;code>--unshare-net&lt;/code> in lockdown (client) ↔ default-deny NetworkPolicy (cluster).&lt;/li>
&lt;li>No &lt;em>escape hatch&lt;/em>, the process lives inside &lt;code>bwrap&lt;/code> (client) ↔ no &lt;code>privileged&lt;/code>, no &lt;code>hostPath&lt;/code>, &lt;code>RuntimeClass&lt;/code> kata (cluster).&lt;/li>
&lt;li>&lt;code>~/.ssh&lt;/code> and &lt;code>~/.aws&lt;/code> never mounted (client) ↔ secrets outside the pod plus Tetragon watching &lt;code>security_file_open&lt;/code> (cluster).&lt;/li>
&lt;/ul>
&lt;p>The sibling runbook turns each of these equivalences into concrete files. What matters here is the method: when someone shows you an agent sandbox, be it the Claude Code &lt;code>/sandbox&lt;/code> on a laptop or a microVM on a PaaS, the useful question is not &amp;ldquo;which tool does it use?&amp;rdquo;, but &amp;ldquo;which of the five vectors does it close, and which does it leave open?&amp;rdquo;. The tool gets replaced; the threat map stays.&lt;/p>
&lt;h2 id="what-no-sandbox-solves">What no sandbox solves&lt;/h2>
&lt;p>Three limits that Anthropic&amp;rsquo;s own documentation states, and which are worth keeping in view so as not to sell smoke:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Egress remains a risk&lt;/strong> in any sandbox that allows outbound connections. If the agent can open a connection, it can exfiltrate. That is why the client lockdown &lt;em>cuts&lt;/em> the network and the cluster uses default-deny NetworkPolicy plus Tetragon: you do not trust &amp;ldquo;filtering well&amp;rdquo;, you trust &amp;ldquo;not letting anything out&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Code modification remains possible&lt;/strong> in any sandbox with the project directory mounted writable. The remedy is not a sandbox-technical one, it is &lt;strong>git&lt;/strong>: with the remote untouched and no &lt;code>push&lt;/code> permission, the worst case is corrupting the local working copy — &lt;code>git checkout .&lt;/code> and start again. The damage never reaches the remote.&lt;/li>
&lt;li>&lt;strong>No sandbox prevents a compromised prompt from reaching the API.&lt;/strong> Isolation bounds the impact of an injection; it does not prevent it. The complementary defences — input validation, tool-call allowlists, output auditing — are mandatory &lt;em>alongside&lt;/em> isolation, not instead of it.&lt;/li>
&lt;/ol>
&lt;p>The operational conclusion: &lt;strong>isolation shrinks the blast radius; defence in depth is what closes the circle.&lt;/strong> A process sandbox for trusted code on a known machine is appropriate and practically free. For an agent acting on user prompts, executing generated code or running multi-tenant, the acceptable minimum in 2026 is a microVM, with Tetragon watching from above. Pick the tier that matches your real threat, check which vector it leaves open, and stack complementary controls on top.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets in the sovereign LLM stack: defence in depth&lt;/a> — the hardening of the stack that hosts those agents: network, secrets, runtime.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">Runbook: caging the AI agent — bubblewrap on the client, Tetragon on the cluster&lt;/a> — the operational companion to this post: the &lt;code>.ai-jail&lt;/code> files, the permissions &lt;code>--bootstrap&lt;/code> and the Tetragon &lt;code>TracingPolicy&lt;/code> files to copy and paste. With commands.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/network-numa-cilium-ebpf-dranet-fourth-leg-pinning/">The kitchen door the maître never checked: network NUMA, Cilium eBPF and DRANET&lt;/a> — the Cilium eBPF datapath Tetragon leans on in the cluster; the same kernel layer, a different use.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — mitigation on the &lt;em>content&lt;/em> plane (what the model says and what is said to it); this post is mitigation on the &lt;em>execution&lt;/em> plane (what the agent&amp;rsquo;s process can do).&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">Technical controls: ENS × ISO 42001 × EU AI Act&lt;/a> — the compliance framework that runtime isolation materialises: Tetragon as technical evidence for &lt;code>op.mon&lt;/code>/&lt;code>op.exp&lt;/code>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">Catalogue of OSS tools for LLMOps&lt;/a> — where the runtime security layer fits in the complete open stack.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>bubblewrap: &lt;a href="https://github.com/containers/bubblewrap">https://github.com/containers/bubblewrap&lt;/a>&lt;/li>
&lt;li>Landlock LSM: &lt;a href="https://landlock.io">https://landlock.io&lt;/a> · &lt;a href="https://docs.rs/landlock">https://docs.rs/landlock&lt;/a>&lt;/li>
&lt;li>ai-jail (Fabio Akita): &lt;a href="https://github.com/akitaonrails/ai-jail">https://github.com/akitaonrails/ai-jail&lt;/a>&lt;/li>
&lt;li>gVisor: &lt;a href="https://gvisor.dev">https://gvisor.dev&lt;/a>&lt;/li>
&lt;li>Firecracker: &lt;a href="https://firecracker-microvm.github.io">https://firecracker-microvm.github.io&lt;/a>&lt;/li>
&lt;li>Kata Containers: &lt;a href="https://katacontainers.io">https://katacontainers.io&lt;/a>&lt;/li>
&lt;li>Tetragon (Cilium): &lt;a href="https://tetragon.io">https://tetragon.io&lt;/a>&lt;/li>
&lt;li>Vercel Sandbox — concepts: &lt;a href="https://vercel.com/docs/vercel-sandbox/concepts">https://vercel.com/docs/vercel-sandbox/concepts&lt;/a>&lt;/li>
&lt;li>E2B: &lt;a href="https://github.com/e2b-dev/E2B">https://github.com/e2b-dev/E2B&lt;/a>&lt;/li>
&lt;li>Claude Code sandboxing: &lt;a href="https://docs.claude.com/en/docs/claude-code">https://docs.claude.com/en/docs/claude-code&lt;/a>&lt;/li>
&lt;li>Codex CLI: &lt;a href="https://github.com/openai/codex">https://github.com/openai/codex&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>MLOps specific to LLMs in 2026: the landscape of three modalities, six stages and ten tools that make them work</title><link>https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/</link><pubDate>Thu, 21 May 2026 05:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>This is the blog&amp;rsquo;s fourth series and it is called &lt;strong>MLOps specific to LLMs&lt;/strong>. It takes the operational craft of traditional MLOps (reproducible pipelines, model registries, dataset versioning, eval gates, controlled deployments) and redraws it for a world where the model is probabilistic, the outputs are subjective, the dependencies include external vendors that update weights without warning, and the &amp;ldquo;application&amp;rdquo; is not a model but an orchestration of models, embeddings, retrievers, guardrails and routers. Gartner predicts that more than 50% of enterprise GenAI deployments will fail before the end of 2026, and the main cause is not the model: it is that deterministic software assumptions were applied to probabilistic systems. This post opens the series with the frame: the seven structural differences between LLMOps and classic MLOps; the six-stage pipeline (data → tune → eval → deploy → observe → retrain); the three modalities of preparing a model (continuous fine-tuning, RAG over datalakes, agent training) with their decision matrix, where 60% of 2025-2026 deployments use hybrid because each modality solves a different problem: &amp;ldquo;fine-tune for behaviour, RAG for volatile knowledge&amp;rdquo;; and the 2026 tool landscape, which already forms reasonably stable layers: MLflow 3.10 (March 2026) as a GenAI-aware registry, W&amp;amp;B Weave and ZenML for tracing and pipelines, Kubeflow + KServe vLLM 0.8.1+ for serving, BentoML for flexibility, DVC + lakeFS (joined since November 2025) for data, Langfuse for prompts and observability. The three following posts will go down into the detail of the most critical pieces.&lt;/p>
&lt;blockquote>
&lt;p>This is the opening of series 4: MLOps for LLMs. It continues the tradition of the previous series: &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">LLM inference&lt;/a> (the first), &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF&lt;/a> (the second) and &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">post-tracing&lt;/a> (the third). Here we enter the discipline that ties all the pieces together: how an LLM system is operated in production over months, not just deployed once.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-analogy-the-sres-craft-redrawn">The analogy: the SRE&amp;rsquo;s craft redrawn&lt;/h2>
&lt;p>Anyone who has worked for years as an SRE or a platform engineer recognises the classic pillars: reproducibility (same code + same data + same config = same result), observability (what happens can be measured), safe rollback (if something goes wrong, I go back within minutes), gradual rollout (the new thing goes to 1% before it goes to 100%). These pillars are not negotiable. The question is whether they hold up when the central component is an LLM.&lt;/p>
&lt;p>The answer is: &lt;strong>same pillars, radically different mechanics&lt;/strong>. Reproducibility: versioning code and data is no longer enough; you have to version prompts, retrieval configurations, snapshots of the vendor&amp;rsquo;s model (which change without warning). Observability: error and latency metrics are no longer enough; you have to measure subjective quality via LLM-as-judge and embedding drift. Rollback: dropping the binary&amp;rsquo;s version is no longer enough; you have to keep the old model cached because loading a new one takes minutes. Gradual rollout: a % of traffic is no longer enough; you have to decide what % of what kind of queries by segment.&lt;/p>
&lt;p>It is the same craft, practised with partly new tools and reflexes. MLOps specific to LLMs, or &amp;ldquo;LLMOps&amp;rdquo;, as the field has christened itself, is the discipline that codifies those reflexes.&lt;/p>
&lt;h2 id="the-seven-structural-differences-between-llmops-and-traditional-mlops">The seven structural differences between LLMOps and traditional MLOps&lt;/h2>
&lt;p>Before going down to the pipeline, let us fix the differences that make this territory new rather than a mere continuation. Each one has concrete practical consequences.&lt;/p>
&lt;h3 id="1-non-deterministic-outputs">1. Non-deterministic outputs&lt;/h3>
&lt;p>Traditional MLOps: the model receives structured input and returns a bounded, reproducible prediction. Same input → same output. Unit tests work.&lt;/p>
&lt;p>LLMOps: same input → different output every time (through sampling, through temperature, through the order of tools invoked, through the retrieval context that changed). The idea of a &amp;ldquo;unit test&amp;rdquo; breaks.&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: tests over properties (was the tone maintained?, does it mention the source?, does it respect the JSON schema?), not over equality. Statistical evals over a distribution, not over samples.&lt;/p>
&lt;h3 id="2-behaviour-metrics-not-statistical-accuracy">2. Behaviour metrics, not statistical accuracy&lt;/h3>
&lt;p>Traditional MLOps: F1, accuracy, AUC, RMSE. Metrics with a clear number.&lt;/p>
&lt;p>LLMOps: subjective rubric scores (G-Eval, faithfulness, helpfulness, toxicity), judge LLMs, human feedback. The &amp;ldquo;number&amp;rdquo; depends on who judges.&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: platforms have to treat evals as versioned artifacts, not just &amp;ldquo;model v3 scored 0.87&amp;rdquo;, but &amp;ldquo;model v3 evaluated with the judge claude-3-5-sonnet-20251022 over the dataset gold-rag-v7 with the prompt judge-v2 scored 0.87&amp;rdquo;. Versioning the judge is as important as versioning the model being evaluated.&lt;/p>
&lt;h3 id="3-the-model-is-an-external-dependency-not-an-internal-asset">3. The model is an external dependency, not an internal asset&lt;/h3>
&lt;p>Traditional MLOps: you train the model, it lives in your registry, it does not change until you change it.&lt;/p>
&lt;p>LLMOps: the base model belongs to Anthropic, OpenAI, Google, Meta. They change it on you without warning. The &lt;code>claude-3-5-sonnet&lt;/code> version that answered well yesterday answers somewhat differently today.&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: drift detection becomes far more critical (&lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">previous post&lt;/a>). Pinning to specific snapshots (&lt;code>claude-3-5-sonnet-20251022&lt;/code>) when the vendor allows it. For high-commitment apps, self-hosting the base model to guarantee reproducibility.&lt;/p>
&lt;h3 id="4-the-application-is-an-orchestration-not-a-model">4. The application is an orchestration, not a model&lt;/h3>
&lt;p>Traditional MLOps: an app calls a model and consumes its output.&lt;/p>
&lt;p>LLMOps 2026: an app connects foundation model + LoRA adapters + retrievers + vector stores + guardrails + routers + tool servers (MCP) + evaluators, all composing the final behaviour. Any component can degrade the result.&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: cross-component debugging requires distributed tracing with OTel (covered in previous posts). The registry does not only store &amp;ldquo;the model&amp;rdquo; but the composition: which prompt version + which adapter + which vector store + which retriever config.&lt;/p>
&lt;h3 id="5-cost-per-inference-not-per-training">5. Cost per inference, not per training&lt;/h3>
&lt;p>Traditional MLOps: the high cost is training; serving is cheap. You optimise training.&lt;/p>
&lt;p>LLMOps: the high cost is serving (every token costs, every call to the vendor is paid for, the GPUs doing the serving are switched on 24/7). You optimise inference.&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: cost accountability per tenant, per agent, per tool. Metrics such as &lt;code>gen_ai.usage.input_tokens&lt;/code> aggregated at customer and product level. Model decisions based on cost per query, not only on quality.&lt;/p>
&lt;h3 id="6-gpu-heavy-infra-with-specific-primitives">6. GPU-heavy infra with specific primitives&lt;/h3>
&lt;p>Traditional MLOps: CPU + some GPU for training. Standard Kubernetes.&lt;/p>
&lt;p>LLMOps: Hopper/Blackwell SXM GPUs, NVLink/NVSwitch, tensor parallel, paged attention, KV cache. Infra that only fits into Kubernetes with primitives such as LeaderWorkerSet, GPU Operator, KEDA with LLM metrics (covered in &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a>).&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: the orchestration stack includes specialised operators (OME, vLLM Production Stack, NVIDIA Dynamo, llm-d, see &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s Operators&lt;/a>) that traditional MLOps does not contemplate.&lt;/p>
&lt;h3 id="7-rlhf-and-human-feedback-as-a-first-class-citizen">7. RLHF and human feedback as a first-class citizen&lt;/h3>
&lt;p>Traditional MLOps: human feedback means labelling data before training.&lt;/p>
&lt;p>LLMOps: human feedback lives inside the model in production, whether through RLHF of the foundation models (Anthropic, OpenAI), through RLAIF, through DPO, or through explicit user feedback that is fed back into fine-tuning.&lt;/p>
&lt;p>&lt;strong>Operational consequence&lt;/strong>: bidirectional production → training pipelines. Datasets grow with real incidents. Model decisions are taken with continuous feedback, not in a training project every N months.&lt;/p>
&lt;h2 id="why-gartner-predicts-50-failures">Why Gartner predicts 50%+ failures&lt;/h2>
&lt;p>&lt;a href="https://medium.com/@sanjeebmeister/the-complete-mlops-llmops-roadmap-for-2026-building-production-grade-ai-systems-bdcca5ed2771">Gartner published that more than 50% of enterprise GenAI deployments will fail before 2026&lt;/a>. The causes are not technical ones about the model but about the system:&lt;/p>
&lt;ul>
&lt;li>Hallucinated outputs from poor grounding: badly designed RAG, poor retrieval, insufficient context.&lt;/li>
&lt;li>Data architectures that are not ready: companies have data in silos, with no stable schemas, with no controlled freshness. Connecting an LLM to this data without a serious pipeline produces erratic answers.&lt;/li>
&lt;li>Lack of structured workflows for prompt-driven systems: teams that treat prompts as code in hardcoded strings, with no versioning, no tests, no gates.&lt;/li>
&lt;/ul>
&lt;p>The conclusion the field draws: &lt;strong>LLMOps is not optional&lt;/strong>. Companies that deploy GenAI without operational discipline fall into one of the three failure modes. Those that apply it (MLflow/W&amp;amp;B for tracking, DVC/lakeFS for data, Langfuse for prompts and evals, KServe or vLLM Production Stack for serving, drift detection in production) are the ones that keep the system working six months after the first release.&lt;/p>
&lt;h2 id="the-six-stage-llmops-pipeline">The six-stage LLMOps pipeline&lt;/h2>
&lt;p>On to the pipeline. The six stages that any serious LLM system goes through, in order:&lt;/p>
&lt;pre tabindex="0">&lt;code>[1. Data] → [2. Tune] → [3. Eval] → [4. Deploy] → [5. Observe] → [6. Retrain]
│
└─→ back to 1
&lt;/code>&lt;/pre>&lt;p>Each stage is an operational domain of its own, with its own tools and traps:&lt;/p>
&lt;h3 id="stage-1--data">Stage 1 — Data&lt;/h3>
&lt;p>&lt;strong>What happens&lt;/strong>: ingestion, cleaning, curation, versioning, indexing of the corpus. It is where real projects suffer most, because companies have data in heterogeneous silos.&lt;/p>
&lt;p>&lt;strong>Typical sub-tasks&lt;/strong>: extraction from source (CDC over Kafka, batch from data lakes, scraping), cleaning (PII removal, dedup, format), curation (labelling for fine-tuning, golden datasets for eval), versioning (DVC + lakeFS), indexing (embeddings + vector store for RAG).&lt;/p>
&lt;p>&lt;strong>Traps&lt;/strong>: schema drift at the source, undetected PII, poor dedup that injects redundancy into training, a vector store that is not updated.&lt;/p>
&lt;h3 id="stage-2--tune">Stage 2 — Tune&lt;/h3>
&lt;p>&lt;strong>What happens&lt;/strong>: preparing the model for your use case. Three modalities (we go deeper into them shortly): fine-tuning, RAG, agent training.&lt;/p>
&lt;p>&lt;strong>Typical sub-tasks&lt;/strong>: base model selection, adapter preparation (LoRA, QLoRA), training loop with continuous eval, hyperparameter sweep (Optuna, W&amp;amp;B Sweeps), saving the checkpoint.&lt;/p>
&lt;p>&lt;strong>Traps&lt;/strong>: catastrophic forgetting if the fine-tuning is very aggressive, overfitting to the golden dataset, no independent validation set.&lt;/p>
&lt;h3 id="stage-3--eval">Stage 3 — Eval&lt;/h3>
&lt;p>&lt;strong>What happens&lt;/strong>: validating that the model + adapters + RAG configuration is acceptable before promoting. Covered in &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Typical sub-tasks&lt;/strong>: running an eval framework (DeepEval, Promptfoo, Ragas) against the golden dataset, judge LLM evaluations, human review over a sample, gates with thresholds.&lt;/p>
&lt;p>&lt;strong>Traps&lt;/strong>: a golden dataset that ages, an uncalibrated judge, evals that pass in CI but fail in production because of distribution shift.&lt;/p>
&lt;h3 id="stage-4--deploy">Stage 4 — Deploy&lt;/h3>
&lt;p>&lt;strong>What happens&lt;/strong>: going from &amp;ldquo;the model evaluated well&amp;rdquo; to &amp;ldquo;the model serves real traffic&amp;rdquo;. Covered in &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s Operators&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Typical sub-tasks&lt;/strong>: serving with vLLM/SGLang/TRT-LLM, runtime configuration, gradual rollout (canary, shadow, blue-green), routing between models (LiteLLM, OpenRouter, LangChain routers).&lt;/p>
&lt;p>&lt;strong>Traps&lt;/strong>: a naive rolling update that cuts sessions, autoscaling by CPU% that does not respond to LLM metrics (covered), a new model that performs worse in production than in eval.&lt;/p>
&lt;h3 id="stage-5--observe">Stage 5 — Observe&lt;/h3>
&lt;p>&lt;strong>What happens&lt;/strong>: seeing what is happening in real time. Covered in the whole post-tracing series and the eBPF series.&lt;/p>
&lt;p>&lt;strong>Typical sub-tasks&lt;/strong>: tracing (Langfuse, LangSmith, Phoenix, OpenLLMetry), metrics (TTFT, TPOT, queue depth, cost per query), active guardrails (NeMo, Llama Guard), drift detection (Evidently, NannyML, WhyLabs).&lt;/p>
&lt;p>&lt;strong>Traps&lt;/strong>: cardinality explosion in metrics, batch evals with no tail-sampling over real traces, drift that is ignored until an incident makes it real.&lt;/p>
&lt;h3 id="stage-6--retrain">Stage 6 — Retrain&lt;/h3>
&lt;p>&lt;strong>What happens&lt;/strong>: closing the loop. Production feedback (incidents, worst-scoring cases, detected drift) generates new data to go back to stage 1.&lt;/p>
&lt;p>&lt;strong>Typical sub-tasks&lt;/strong>: extraction of problematic logs, human labelling of the sample, incorporation into the golden dataset, re-fine-tuning if applicable, decision on a new release.&lt;/p>
&lt;p>&lt;strong>Traps&lt;/strong>: an &amp;ldquo;open&amp;rdquo; loop where production never informs the dataset, human feedback that gets lost, lack of a clear retrain cadence.&lt;/p>
&lt;h2 id="the-three-modalities-of-preparing-the-model">The three modalities of &amp;ldquo;preparing the model&amp;rdquo;&lt;/h2>
&lt;p>Stage 2 (Tune) is where there is most confusion. In 2026 three modalities coexist, each solving a different problem:&lt;/p>
&lt;h3 id="fine-tuning">Fine-tuning&lt;/h3>
&lt;p>&lt;strong>What it does&lt;/strong>: modifying the model&amp;rsquo;s weights (or those of a LoRA/QLoRA adapter on top) so that it learns specific behaviour patterns: tone, output structure, idiomatic decisions of the domain.&lt;/p>
&lt;p>&lt;strong>When&lt;/strong>: when your main failure is behavioural inconsistency between calls. The model sometimes answers formally, sometimes not; sometimes it structures the JSON, sometimes not; sometimes it follows the company&amp;rsquo;s conventions, sometimes it invents them. Fine-tuning stabilises it.&lt;/p>
&lt;p>&lt;strong>When NOT&lt;/strong>: when what you need is up-to-date knowledge. Fine-tuning fixes knowledge in frozen weights; the day after the fine-tuning, the model knows nothing new.&lt;/p>
&lt;h3 id="rag-retrieval-augmented-generation">RAG (Retrieval-Augmented Generation)&lt;/h3>
&lt;p>&lt;strong>What it does&lt;/strong>: leaving the model intact and, on each call, retrieving fresh context from a knowledge base (vector store + lexical search, typically) and passing it to the model so that it answers on that basis.&lt;/p>
&lt;p>&lt;strong>When&lt;/strong>: when the knowledge you need is dynamic or very large. Documentation that changes, a product catalogue that is updated, an internal knowledge base that grows.&lt;/p>
&lt;p>&lt;strong>When NOT&lt;/strong>: when the problem is behavioural (RAG does not teach the model how to behave, it only gives it information). Or when retrieval is so noisy that the context arriving is worse than nothing.&lt;/p>
&lt;h3 id="agent-training">Agent training&lt;/h3>
&lt;p>&lt;strong>What it does&lt;/strong>: going beyond conventional fine-tuning with Reinforcement Learning techniques. RFT (OpenAI&amp;rsquo;s Reinforcement Fine-Tuning), classic RLHF, RLAIF (with AI feedback), DPO (Direct Preference Optimization) over datasets of (good, bad) pairs.&lt;/p>
&lt;p>&lt;strong>When&lt;/strong>: when the model needs to learn complex multistep trajectories: when to choose each tool, how to decompose a task, when to ask the user for confirmation. It is what is turning Claude, Gemini and GPT into agents capable of long tasks.&lt;/p>
&lt;p>&lt;strong>When NOT&lt;/strong>: when your case is simple chat or RAG. It is overkill, expensive and complicated for problems that the previous modalities solve.&lt;/p>
&lt;h3 id="decision-matrix">Decision matrix&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Observed problem&lt;/th>
&lt;th>Modality&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Inconsistent answers in tone/structure&lt;/td>
&lt;td>Fine-tuning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The model makes things up (hallucinates)&lt;/td>
&lt;td>RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Out-of-date knowledge (&amp;gt;1 year)&lt;/td>
&lt;td>RAG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>The model chooses tools badly&lt;/td>
&lt;td>Agent training (RLAIF/RFT)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mixed behaviour + knowledge&lt;/td>
&lt;td>Hybrid (fine-tune + RAG)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-step trajectory fails&lt;/td>
&lt;td>Agent training&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A specific language/regional style&lt;/td>
&lt;td>Fine-tuning&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="the-2026-verdict-hybrid-is-the-default">The 2026 verdict: hybrid is the default&lt;/h3>
&lt;p>&lt;a href="https://www.scalacode.com/blog/rag-vs-fine-tuning/">Multiple reports&lt;/a> agree that in 2025-2026, &lt;strong>around 60% of production projects use hybrid&lt;/strong>: fine-tuning for behaviour + RAG for knowledge. The key insight:&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Fine-tune for behaviour (brand voice, decision protocol, output structure); use RAG for volatile knowledge you need the model to cite. Do not force a single tool to do both jobs.&lt;/strong>&lt;/p>
&lt;/blockquote>
&lt;p>A practical observation: the biggest quality improvements of 2025-2026 come from better reranking in RAG (cross-encoders), not from better embeddings. Rerankers add 15-35% of quality with little complexity.&lt;/p>
&lt;p>On cost: combined fine-tuning + RAG tends to be 30-50% cheaper than pure RAG with frontier models at high volume, because the fine-tuned model can be smaller and cheaper while keeping equivalent quality.&lt;/p>
&lt;h2 id="the-2026-tool-landscape">The 2026 tool landscape&lt;/h2>
&lt;p>On to the concrete pieces, grouped by function. The field has matured enough for each piece to have 2-3 reasonable options and a couple of leaders.&lt;/p>
&lt;h3 id="experiment-tracking-and-model-registry">Experiment tracking and model registry&lt;/h3>
&lt;p>&lt;a href="https://mlflow.org/">MLflow&lt;/a> remains the de facto standard, now with LLM-specific traction. MLflow 3 was published in June 2025; version 3.10.1 (March 2026) added:&lt;/p>
&lt;ul>
&lt;li>A GenAI Overview dashboard with ready-made metrics for LLM apps.&lt;/li>
&lt;li>Multi-workspace support for large teams.&lt;/li>
&lt;li>Cost tracking in traces (gen_ai.usage.* aggregated per experiment).&lt;/li>
&lt;li>MemAlign: a new specific eval algorithm.&lt;/li>
&lt;li>Integrated native OpenTelemetry tracing.&lt;/li>
&lt;li>First-class support for LangChain, LlamaIndex, AutoGen as frameworks.&lt;/li>
&lt;/ul>
&lt;p>MLflow treats prompts and agents as first-class citizens alongside classic models. It is the biggest change relative to MLflow 2.x.&lt;/p>
&lt;p>&lt;a href="https://wandb.ai/">Weights &amp;amp; Biases (W&amp;amp;B)&lt;/a> with its LLM-specific product Weave offers tracing + eval + debug with a very polished UI. More commercial, less self-host friendly, but excellent UX.&lt;/p>
&lt;p>&lt;a href="https://www.zenml.io/">ZenML&lt;/a> is the piece that most cleanly integrates &amp;ldquo;classic MLOps + emerging LLMOps&amp;rdquo; into a single framework. Its automatic artifact versioning captures prompt templates, retrieval chunks and agent conversation histories with no extra work. Open-source. The most complete unification option that exists.&lt;/p>
&lt;h3 id="dataset-versioning">Dataset versioning&lt;/h3>
&lt;p>&lt;a href="https://dvc.org/">DVC&lt;/a> remains the OSS standard. It extends Git to large files and pipelines. Important news from November 2025: &lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS acquired DVC&lt;/a>, consolidating the two OSS data versioning projects under one organisation. The combined roadmap is oriented towards LLM training and RAG datalakes specifically.&lt;/p>
&lt;p>&lt;strong>Typical pattern&lt;/strong>: Git for code + DVC for data/models + MLflow or W&amp;amp;B for experiment tracking + registry. Few teams use just one; the combination is what covers the cycle. Detailed in the &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">dedicated post on data versioning for LLMOps&lt;/a> — the four artifacts to version separately, schema contracts, end-to-end lineage, and a golden eval set with a strict holdout.&lt;/p>
&lt;h3 id="prompt-versioning-and-observability">Prompt versioning and observability&lt;/h3>
&lt;p>Covered in depth in the &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight post&lt;/a> where we go deeper into Langfuse as the OSS reference. Summary here:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>: MIT, self-host, prompt management with v1/v2/v3 versioning + labels + cache + linkage with traces.&lt;/li>
&lt;li>&lt;a href="https://www.langchain.com/langsmith">LangSmith&lt;/a>: if your stack is LangChain.&lt;/li>
&lt;li>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a>: ELv2, OTel-native.&lt;/li>
&lt;/ul>
&lt;h3 id="pipeline-orchestration">Pipeline orchestration&lt;/h3>
&lt;p>For the LLMOps pipeline steps, the dominant options:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.kubeflow.org/docs/components/pipelines/">Kubeflow Pipelines&lt;/a>: the K8s-native standard. KServe (the serving part of Kubeflow) has its vLLM runtime upgraded to v0.8.1+ with support for reasoning models, tool calling, embeddings, reranking, Llama 4 and Qwen 3.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/">ZenML&lt;/a>: already mentioned; also a pipeline orchestrator.&lt;/li>
&lt;li>&lt;a href="https://metaflow.org/">Metaflow&lt;/a> (Netflix-originated): Python-first pipelines, less LLM-specific but workable.&lt;/li>
&lt;li>&lt;a href="https://argoproj.github.io/workflows/">Argo Workflows&lt;/a>: a pure K8s OSS alternative.&lt;/li>
&lt;li>&lt;a href="https://flyte.org/">Flyte&lt;/a>: Kubernetes-native, OSS.&lt;/li>
&lt;/ul>
&lt;h3 id="serving">Serving&lt;/h3>
&lt;p>Covered in depth in &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s Operators&lt;/a>. Summary:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/vllm-project/production-stack">vLLM Production Stack&lt;/a>: a curated Helm chart.&lt;/li>
&lt;li>&lt;a href="https://kserve.github.io/website/">KServe vLLM runtime&lt;/a>: K8s-native, vLLM 0.8.1+ with full agentic support.&lt;/li>
&lt;li>&lt;a href="https://www.bentoml.com/">BentoML&lt;/a>: flexible serving, popular in startups for its simplicity.&lt;/li>
&lt;li>&lt;a href="https://developer.nvidia.com/dynamo">NVIDIA Dynamo&lt;/a>: the successor to Triton.&lt;/li>
&lt;li>&lt;a href="https://github.com/llm-d/llm-d">llm-d&lt;/a>: CNCF Sandbox.&lt;/li>
&lt;li>&lt;a href="https://github.com/ome-projects/ome">OME&lt;/a>: an LMSYS operator with native SGLang.&lt;/li>
&lt;/ul>
&lt;h3 id="evals-and-guardrails">Evals and guardrails&lt;/h3>
&lt;p>Covered in &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails&lt;/a>. Ultra-short summary:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Evals CI&lt;/strong>: DeepEval, Promptfoo, Ragas.&lt;/li>
&lt;li>&lt;strong>Evals platform&lt;/strong>: Langfuse, LangSmith, Phoenix, Braintrust.&lt;/li>
&lt;li>&lt;strong>Guardrails&lt;/strong>: NeMo Guardrails, Llama Guard 4, Llama Prompt Guard 2, LLM Guard, Lakera.&lt;/li>
&lt;/ul>
&lt;h3 id="drift-detection-and-observability">Drift detection and observability&lt;/h3>
&lt;p>Covered in the &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">closing eBPF post&lt;/a>. Summary:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Drift&lt;/strong>: Evidently AI, NannyML, WhyLabs.&lt;/li>
&lt;li>&lt;strong>Tracing&lt;/strong>: Langfuse, OpenLLMetry, Phoenix.&lt;/li>
&lt;li>&lt;strong>eBPF&lt;/strong>: AgentSight, Hubble, Tetragon, ProfInfer.&lt;/li>
&lt;/ul>
&lt;h3 id="the-typical-2026-stack-table">The typical 2026 stack table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Stage&lt;/th>
&lt;th>Dominant piece&lt;/th>
&lt;th>Alternatives&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Data ingestion + versioning&lt;/td>
&lt;td>DVC + lakeFS (unified Nov 2025)&lt;/td>
&lt;td>Pachyderm, Quilt&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vector store / RAG index&lt;/td>
&lt;td>Milvus, Qdrant, pgvector, Weaviate&lt;/td>
&lt;td>LanceDB, Pinecone, Chroma&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Experiment tracking&lt;/td>
&lt;td>MLflow 3.10&lt;/td>
&lt;td>W&amp;amp;B Weave, Neptune&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pipeline orchestration&lt;/td>
&lt;td>Kubeflow + Argo&lt;/td>
&lt;td>ZenML, Metaflow, Flyte&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model registry&lt;/td>
&lt;td>MLflow registry&lt;/td>
&lt;td>W&amp;amp;B Models, KServe ModelMesh&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prompt versioning&lt;/td>
&lt;td>Langfuse&lt;/td>
&lt;td>LangSmith, MLflow Prompts&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Serving&lt;/td>
&lt;td>vLLM Production Stack&lt;/td>
&lt;td>KServe, BentoML, Dynamo, llm-d, OME&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Evals CI&lt;/td>
&lt;td>DeepEval, Ragas&lt;/td>
&lt;td>Promptfoo, OpenAI Evals&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Evals platform&lt;/td>
&lt;td>Langfuse, Phoenix&lt;/td>
&lt;td>LangSmith, Braintrust&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Guardrails&lt;/td>
&lt;td>NeMo + Llama Guard&lt;/td>
&lt;td>LLM Guard, Lakera&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tracing&lt;/td>
&lt;td>OpenLLMetry + Langfuse&lt;/td>
&lt;td>Phoenix, LangSmith&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift detection&lt;/td>
&lt;td>Evidently AI&lt;/td>
&lt;td>NannyML, WhyLabs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>eBPF observability&lt;/td>
&lt;td>AgentSight + Tetragon + Hubble&lt;/td>
&lt;td>(new territory, few alternatives)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>13 pieces. No org uses them all; any serious org uses at least six. &lt;strong>This is the current LLMOps stack&lt;/strong>.&lt;/p>
&lt;h2 id="the-operational-reality-nobody-uses-a-single-tool">The operational reality: nobody uses a single tool&lt;/h2>
&lt;p>&lt;a href="https://medium.com/@kanerika/mlflow-vs-kubeflow-vs-w-b-which-mlops-tool-fits-your-stack-b59007460b25">Multiple comparisons&lt;/a> agree on one thing: &lt;strong>the teams that win combine&lt;/strong>. Recurring patterns:&lt;/p>
&lt;ul>
&lt;li>ZenML to orchestrate + MLflow for tracking + KServe for serving: the most popular OSS stack in companies coming from classic MLOps.&lt;/li>
&lt;li>Kubeflow + W&amp;amp;B + BentoML: for teams with a research focus.&lt;/li>
&lt;li>Langfuse + DeepEval + Phoenix + LiteLLM: for pure-LLM teams with no classic MLOps background.&lt;/li>
&lt;li>MLflow + DVC + Argo + KServe: an idiomatic cloud-native stack without additional LLM-specifics (with its limitations).&lt;/li>
&lt;/ul>
&lt;p>The choice depends on the team&amp;rsquo;s background, on the licensing model they can afford, on the level of self-hosting they need, and on which frictions blocked them most in previous projects. &lt;strong>There is no &amp;ldquo;one right answer&amp;rdquo;&lt;/strong>; there is a stable meta-pattern of layers worth covering.&lt;/p>
&lt;h2 id="common-operational-traps">Common operational traps&lt;/h2>
&lt;h3 id="treating-the-prompt-as-text-in-code">Treating the prompt as text in code&lt;/h3>
&lt;p>Hardcoding prompts in strings in the repo. Changing them requires a PR + redeploy. Result: teams that do not iterate on prompts because every change costs hours of pipeline. &lt;strong>Solution&lt;/strong>: externalised prompt management (Langfuse, MLflow Prompts) with versioning, labels, hot-reload.&lt;/p>
&lt;h3 id="skipping-dataset-versioning">Skipping dataset versioning&lt;/h3>
&lt;p>&amp;ldquo;DVC is complicated, we will add it later&amp;rdquo;. Result: two months later, nobody knows which dataset trained which model. Impossible to reproduce incidents. &lt;strong>Solution&lt;/strong>: DVC + lakeFS from day 1, even if only with a small subset.&lt;/p>
&lt;h3 id="mixing-layers-in-the-same-pipeline">Mixing layers in the same pipeline&lt;/h3>
&lt;p>Teams that put ingestion, fine-tuning, eval and deploy into a single giant pipeline. When something fails, the whole pipeline fails. &lt;strong>Solution&lt;/strong>: independent pipelines per stage, with versioned artifacts as the interfaces between them.&lt;/p>
&lt;h3 id="tracking-with-no-structure">Tracking with no structure&lt;/h3>
&lt;p>Logging everything to stdout and &amp;ldquo;we will look at it in CloudWatch later&amp;rdquo;. Result: impossible to correlate, compare, debug. &lt;strong>Solution&lt;/strong>: OTel from day 1 with &lt;code>gen_ai.*&lt;/code> semantic conventions.&lt;/p>
&lt;h3 id="evals-that-block-nothing">Evals that block nothing&lt;/h3>
&lt;p>You have evals, you run them, you look at them, but they do not prevent the deploy if they drop. Eventually it drops gradually and nobody notices. &lt;strong>Solution&lt;/strong>: eval gates in CI/CD that block the merge if critical metrics regress by more than X%.&lt;/p>
&lt;h3 id="no-retrain-cadence">No retrain cadence&lt;/h3>
&lt;p>You launch v1 and never go back to the model. Six months later, drift has degraded it but the team is on other projects. &lt;strong>Solution&lt;/strong>: a formal retrain cadence (monthly, quarterly) tied to the production incident queue.&lt;/p>
&lt;h3 id="invisible-vendor-lock-in">Invisible vendor lock-in&lt;/h3>
&lt;p>You start with the OpenAI API + LangSmith + Pinecone. When you want to self-host, you discover that migrating is a 3-month project. &lt;strong>Solution&lt;/strong>: abstraction layers (LiteLLM, OpenLLMetry) and vendor-neutrality from the start.&lt;/p>
&lt;h2 id="what-is-coming-in-the-following-posts-of-the-series">What is coming in the following posts of the series&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Post 2 — &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over datalakes with Kafka: end-to-end technical architecture&lt;/a>&lt;/strong> — the most hands-on one. Kafka as source-of-truth, Flink CDC, embedding pipelines, continuous indexing in Milvus/Qdrant, a complete example with real numbers and manifests.&lt;/li>
&lt;li>&lt;strong>Post 3 — &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline: global architecture&lt;/a>&lt;/strong> — the master map of the complete system with a reusable &amp;ldquo;you are here&amp;rdquo; SVG for the following posts. A deep dive into each of the six stages (Data, Tune, Eval, Deploy, Observe, Retrain).&lt;/li>
&lt;li>&lt;strong>Post 4 — &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant in the ingestion stage&lt;/a>&lt;/strong> — synchronisation patterns (dual-write, outbox + CDC, event-driven), a complete microservices architecture, a Qdrant cluster manifest.&lt;/li>
&lt;li>&lt;strong>Upcoming posts&lt;/strong> — still to be decided: the cluster as a multi-tenant platform, Constitutional AI / alignment runtime, continuous fine-tuning in depth, edge LLMs.&lt;/li>
&lt;li>&lt;strong>Synthesis post&lt;/strong> — &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of an LLM request in production, May 2026&lt;/a> — the complete journey of a real request through the six stages and the two cross-cutting components, with cross-links to each dedicated post. It works as a mental map of the blog and as an integrator&amp;rsquo;s guide.&lt;/li>
&lt;li>&lt;strong>Parallel OSS vs hyperscalers catalogue&lt;/strong> — &lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: the six LLMOps stages in OSS and in AWS / GCP / Azure&lt;/a> — for each stage of the pipeline, which tools the blog&amp;rsquo;s reference OSS stack uses and what the cloud equivalents are, with summary tables, identification of real gaps and the multi-tenant chatbot ported to an AWS stack component by component. A neutral editorial stance.&lt;/li>
&lt;li>&lt;strong>OSS catalogue entry by entry&lt;/strong> — &lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages: entry by entry&lt;/a> — the zoom in on the open source side: ~150 words of description per core tool (vLLM, Langfuse, DVC, Qdrant, Airflow, NeMo Guardrails, Presidio…), licence and governance, a decision matrix per stage, a diagram of the connected OSS stack and a master table of licences and EE / SaaS offerings.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;p>LLMOps vs MLOps:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/@sanjeebmeister/the-complete-mlops-llmops-roadmap-for-2026-building-production-grade-ai-systems-bdcca5ed2771">The Complete MLOps/LLMOps Roadmap for 2026 (Sanjeeb Panda)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.iamraghuveer.com/posts/mlops-vs-llmops-what-changes/">MLOps vs LLMOps: What Changes (Raghuveer)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://hyscaler.com/insights/mlops-in-2026-guide/">MLOps in 2026: Architecture, Trends &amp;amp; Strategy (Hyscaler)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.ideas2it.com/blogs/llmops-vs-mlops-key-differences-and-evolution">LLMOps vs MLOps: Differences and Evolution (Ideas2IT)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dev.to/apprecode/llmops-vs-mlops-whats-different-whats-the-same-and-how-to-run-both-in-production-2o52">LLMOps vs MLOps in production (DEV/Apprecode)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Tools:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://mlflow.org/">MLflow&lt;/a> — registry + tracking + serving.&lt;/li>
&lt;li>&lt;a href="https://wandb.ai/site/weave">Weights &amp;amp; Biases Weave&lt;/a> — LLM tracing.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/">ZenML&lt;/a> — pipeline orchestration MLOps + LLMOps.&lt;/li>
&lt;li>&lt;a href="https://www.kubeflow.org/">Kubeflow&lt;/a> — K8s-native MLOps.&lt;/li>
&lt;li>&lt;a href="https://kserve.github.io/website/">KServe&lt;/a> — model serving K8s.&lt;/li>
&lt;li>&lt;a href="https://www.bentoml.com/">BentoML&lt;/a> — flexible serving.&lt;/li>
&lt;li>&lt;a href="https://metaflow.org/">Metaflow&lt;/a> — Netflix&amp;rsquo;s pipelines.&lt;/li>
&lt;li>&lt;a href="https://dvc.org/">DVC&lt;/a> — dataset versioning.&lt;/li>
&lt;li>&lt;a href="https://lakefs.io/">lakeFS&lt;/a> — enterprise data versioning, acquired DVC in Nov 2025.&lt;/li>
&lt;/ul>
&lt;p>2026 comparisons:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/@kanerika/mlflow-vs-kubeflow-vs-w-b-which-mlops-tool-fits-your-stack-b59007460b25">MLflow vs Kubeflow vs W&amp;amp;B (Kanerika)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/blog/mlflow-alternatives">9 MLflow alternatives tested (ZenML)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/blog/metaflow-vs-kubeflow">Metaflow vs Kubeflow vs ZenML (ZenML)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.zenml.io/blog/mlops-tools">12 Best MLOps Tools for Agentic AI (ZenML)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/mlops-pipeline-gpu-cloud-kubeflow-zenml-metaflow-2026/">MLOps Pipeline on GPU Cloud 2026 (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://northflank.com/blog/top-7-kubeflow-alternatives">Top 7 Kubeflow alternatives 2026 (Northflank)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.sganalytics.com/blog/mlops-tools/">Top 20 MLOps Tools 2026 (SG Analytics)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>RAG vs Fine-Tuning:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.scalacode.com/blog/rag-vs-fine-tuning/">RAG Vs Fine-Tuning In 2026 (ScalaCode)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.arxiv.org/pdf/2510.01375">Fine-Tuning with RAG (ICLR 2026, arxiv)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dev.to/tyson_cung/rag-vs-fine-tuning-what-actually-works-in-production-2026-20jg">RAG vs Fine-Tuning — What Actually Works in Production 2026 (DEV)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kapa.ai/blog/how-to-build-a-rag-pipeline-from-scratch-in-2026">How to Build a RAG Pipeline from Scratch in 2026 (kapa.ai)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references (the three previous series):&lt;/p>
&lt;ul>
&lt;li>LLM inference series: &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on K8s&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s Operators&lt;/a>.&lt;/li>
&lt;li>eBPF series: &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight&lt;/a>.&lt;/li>
&lt;li>Post-tracing series: &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF + drift&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>MCP from the inside and its deep observability: the LSP of AI agents and how to see everything with OpenTelemetry</title><link>https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/</link><pubDate>Wed, 20 May 2026 06:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>&lt;a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)&lt;/a> is the standard Anthropic published at the end of 2024 and which by 2026 has become &lt;strong>the dominant protocol for connecting AI agents to external tools and data&lt;/strong>. Its value, the reason the whole industry adopted it in under 18 months, is that it &lt;strong>solves a combinatorial problem&lt;/strong>: before MCP, integrating M AI apps with N tools required M×N ad-hoc integrations; with MCP, M + N. It is the same move the &lt;a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol&lt;/a> made in 2016 for code editors. The architecture is three well-defined roles, &lt;strong>Host&lt;/strong> (the AI app), &lt;strong>Client&lt;/strong> (the connection, one per server) and &lt;strong>Server&lt;/strong> (the piece that exposes capabilities); the primitives are six, three on the server side (Tools, Resources, Prompts) and three on the client side (Sampling, Roots, Elicitation); the protocol is JSON-RPC over two transports, stdio for local processes and Streamable HTTP for remote. The operational challenge appears when there are 10-20 MCP servers running simultaneously, each with several tools, connected to an agent that chains multistep calls: &lt;strong>observing what happens, where things fail, how much each tool costs, which tenant invokes what&lt;/strong> becomes critical. The ecosystem&amp;rsquo;s answer in 2026: the new &lt;strong>OpenTelemetry GenAI semantic conventions for MCP&lt;/strong> (already stable), trace context propagation via &lt;code>params._meta&lt;/code> (because JSON-RPC does not bring it natively), FastMCP with built-in OTel instrumentation, MCP Gateways as a centralised layer (Traefik Hub, MintMCP, OpenObserve), and MCP Inspector for interactive debugging. This article walks the architecture from the outside in, puts each concept in its exact place, and goes down to the detail of observability: traces, RED metrics, real use cases and pitfalls.&lt;/p>
&lt;blockquote>
&lt;p>This is the &lt;strong>third post in the post-tracing series&lt;/strong>. Previous posts: &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails&lt;/a>. Here we go down to the protocol that connects agents to tools, and how to see it in production.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-master-analogy-in-three-versions">The master analogy (in three versions)&lt;/h2>
&lt;p>MCP is a communication protocol. Like any protocol, it is best understood with the right analogy. I am going to give you three, because each one lights up a different facet and the combination leaves you understanding it better than any technical definition.&lt;/p>
&lt;h3 id="version-1--the-usb-c-of-ai-apps-the-official-one">Version 1 — The USB-C of AI apps (the official one)&lt;/h3>
&lt;p>This is the analogy Anthropic adopted when presenting it. Before USB-C, every electronic device had its own connector. Your phone had microUSB or Lightning, your laptop a proprietary power port, your headphones a 3.5mm jack, your external disk USB-A at one end and mini-USB at the other. The result: three boxes full of specific cables that got lost, none of them good for two things, and buying a new device meant buying new accessories.&lt;/p>
&lt;p>USB-C changed that. A single physical connector that many protocols cross: data (USB 3, USB 4, Thunderbolt), video (DisplayPort), power (Power Delivery), audio. You connect anything to anything and it works; the protocols negotiate above.&lt;/p>
&lt;p>MCP plays the same role for AI apps. Before MCP, every application that wanted to integrate tools with an LLM, Claude Desktop, Cursor, Continue, in-house custom agents, invented its own way of doing it. Every tool vendor had to write N different integrations, one per app. The result: massive fragmentation, a lot of duplicated code, integrations that broke when an app changed its internal API.&lt;/p>
&lt;p>With MCP, the connector is one: any app that speaks MCP can use any MCP tool. Just as your USB-C talks to printers, monitors and disks without the printer &amp;ldquo;knowing&amp;rdquo; whether the cable is connected to a Mac or to a Linux box.&lt;/p>
&lt;h3 id="version-2--the-lsp-of-code-editors-the-most-technically-precise">Version 2 — The LSP of code editors (the most technically precise)&lt;/h3>
&lt;p>This is my favourite because the analogy is structurally identical, not just metaphorical.&lt;/p>
&lt;p>Until 2016, if you wanted your code editor to support a new language, Rust, Go, TypeScript, somebody had to write a specific plugin for your particular editor. VSCode had its Rust plugin, IntelliJ a different one, Vim another, Emacs another. Every decent feature (go-to-definition, autocompletion, refactoring) was an implementation duplicated N times. &lt;strong>M editors × N languages = M·N integrations&lt;/strong>.&lt;/p>
&lt;p>In 2016 Microsoft proposed the &lt;strong>Language Server Protocol (LSP)&lt;/strong>: each language implements a single &amp;ldquo;language server&amp;rdquo; (a process that understands that language); each editor implements a single LSP client; when you work with Rust code in VSCode, VSCode launches rust-analyzer as a subprocess and talks LSP to it over stdio. Any LSP editor + any LSP server = it works. &lt;strong>M + N&lt;/strong>.&lt;/p>
&lt;p>MCP is literally this pattern, transferred from &amp;ldquo;editor + language server&amp;rdquo; to &amp;ldquo;AI app + tool provider&amp;rdquo;. And it shares even the technical detail: both pass &lt;strong>JSON-RPC over stdio&lt;/strong> (among other transports). When Anthropic designed MCP, they looked at LSP. Anyone coming from the world of editors and IDEs will find MCP familiar.&lt;/p>
&lt;h3 id="version-3--the-operating-system-driver-the-operational-one">Version 3 — The operating system driver (the operational one)&lt;/h3>
&lt;p>Finally, an analogy that helps you understand what a specific MCP server does.&lt;/p>
&lt;p>An operating system does not know directly how to talk to your particular HP LaserJet printer. What it knows is a generic interface: &amp;ldquo;print document&amp;rdquo;, &amp;ldquo;query status&amp;rdquo;, &amp;ldquo;cancel job&amp;rdquo;. The printer driver is the piece that translates that generic interface into the proprietary commands of your specific printer.&lt;/p>
&lt;p>An MCP server does exactly the same:&lt;/p>
&lt;ul>
&lt;li>Your AI agent knows a generic interface: invoke a tool with a defined schema, read a resource by URI, ask for a prompt template by name.&lt;/li>
&lt;li>The &lt;strong>MCP server&lt;/strong> is the driver: it translates those generic operations into the concrete APIs of the underlying system, your PostgreSQL database, your filesystem, your GitHub API, your Stripe.&lt;/li>
&lt;/ul>
&lt;p>This leaves the AI agent free from knowing how it authenticates with GitHub, what exact SQL PostgreSQL uses, what endpoints Stripe has. It speaks MCP; the server takes care of the details.&lt;/p>
&lt;p>With the three analogies combined: &lt;strong>MCP is the layer between the LLM and the world, a standard USB-C implemented like LSP in JSON-RPC, with each server acting as a driver for one concrete underlying system&lt;/strong>.&lt;/p>
&lt;h2 id="what-concrete-problem-mcp-solves">What concrete problem MCP solves&lt;/h2>
&lt;p>Before going down to the architecture, it is worth pinning down the specific problem MCP solves, because without that many design decisions look arbitrary.&lt;/p>
&lt;p>The problem is &lt;strong>the quadratic cost of integrations&lt;/strong>.&lt;/p>
&lt;p>Imagine you have M applications that use LLMs (Claude Desktop, Cursor, Continue, ChatGPT Desktop, your own custom agent, and so on) and N external tools those LLMs could use (filesystem, GitHub, Slack, PostgreSQL, Jira, Notion, and so on). Without a standard:&lt;/p>
&lt;ul>
&lt;li>Each (application, tool) pair requires a specific integration.&lt;/li>
&lt;li>Every time the application changes its internal API, N integrations have to be updated.&lt;/li>
&lt;li>Every time the tool changes its API, M have to be updated.&lt;/li>
&lt;li>For your new tool to be adopted, you have to write M integrations.&lt;/li>
&lt;li>For your new application to support the ecosystem, you have to write N.&lt;/li>
&lt;/ul>
&lt;p>The real result in 2023-2024: massive fragmentation. OpenAI&amp;rsquo;s function calling was not compatible with Anthropic&amp;rsquo;s tool use; every framework (LangChain, LlamaIndex, dspy) had its own wrapper; Claude Desktop plugins did not work in Cursor; and so on.&lt;/p>
&lt;p>MCP breaks the quadratic. Each application implements the protocol once; each tool implements the protocol once; any pair works. M + N.&lt;/p>
&lt;p>It is exactly what happened with USB-C, with LSP, with SQL (before there were proprietary APIs per database), with POSIX (before there were proprietary APIs per operating system). The pattern repeats because it always solves the same kind of problem.&lt;/p>
&lt;h2 id="the-architecture-three-roles-clearly-placed">The architecture: three roles, clearly placed&lt;/h2>
&lt;p>We are going to pin down where each thing lives, because mixing up the roles is the number one source of confusion in MCP.&lt;/p>
&lt;div class="diagram" style="max-width:720px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 720 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MCP architecture: Host, Client, Server">
&lt;style>.title{font:600 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.box{stroke:#444;stroke-width:1.4}.host{fill:#ffe9d6}.llm{fill:#ffd6d6}.client{fill:#d6eaff}.server{fill:#d9f5d6}.sys{fill:#eee;stroke-dasharray:4 2}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mh)}.bidi{stroke:#888;stroke-width:1.2;fill:none}&lt;/style>
&lt;defs>&lt;marker id="mh" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="360" y="20" text-anchor="middle" class="title">MCP architecture: where each piece lives&lt;/text>
&lt;rect x="30" y="40" width="280" height="280" rx="8" class="box host"/>
&lt;text x="170" y="60" text-anchor="middle" class="lbl">HOST&lt;/text>
&lt;text x="170" y="76" text-anchor="middle" class="sm">AI app: Claude Desktop, Cursor, own agent&lt;/text>
&lt;rect x="55" y="95" width="230" height="50" rx="6" class="box llm"/>
&lt;text x="170" y="116" text-anchor="middle" class="lbl">LLM (reasoning engine)&lt;/text>
&lt;text x="170" y="132" text-anchor="middle" class="sm">decides which tools to call, what to read&lt;/text>
&lt;rect x="55" y="160" width="100" height="36" rx="6" class="box client"/>
&lt;text x="105" y="183" text-anchor="middle" class="lbl">Client 1&lt;/text>
&lt;rect x="160" y="160" width="120" height="36" rx="6" class="box client"/>
&lt;text x="220" y="183" text-anchor="middle" class="lbl">Client 2&lt;/text>
&lt;rect x="55" y="210" width="100" height="36" rx="6" class="box client"/>
&lt;text x="105" y="233" text-anchor="middle" class="lbl">Client 3&lt;/text>
&lt;rect x="160" y="210" width="120" height="36" rx="6" class="box client"/>
&lt;text x="220" y="233" text-anchor="middle" class="lbl">Client N&lt;/text>
&lt;text x="170" y="275" text-anchor="middle" class="sm">one MCP client per connected server&lt;/text>
&lt;text x="170" y="295" text-anchor="middle" class="sm">each client is a 1:1 connection&lt;/text>
&lt;rect x="380" y="60" width="200" height="70" rx="6" class="box server"/>
&lt;text x="480" y="82" text-anchor="middle" class="lbl">Server: filesystem-mcp&lt;/text>
&lt;text x="480" y="100" text-anchor="middle" class="sm">stdio (local process)&lt;/text>
&lt;text x="480" y="116" text-anchor="middle" class="sm">tools: read, write, list, search&lt;/text>
&lt;rect x="380" y="140" width="200" height="70" rx="6" class="box server"/>
&lt;text x="480" y="162" text-anchor="middle" class="lbl">Server: github-mcp&lt;/text>
&lt;text x="480" y="180" text-anchor="middle" class="sm">Streamable HTTP (remote)&lt;/text>
&lt;text x="480" y="196" text-anchor="middle" class="sm">tools: create_issue, get_pr, ...&lt;/text>
&lt;rect x="380" y="220" width="200" height="70" rx="6" class="box server"/>
&lt;text x="480" y="242" text-anchor="middle" class="lbl">Server: postgres-mcp&lt;/text>
&lt;text x="480" y="260" text-anchor="middle" class="sm">stdio (local process)&lt;/text>
&lt;text x="480" y="276" text-anchor="middle" class="sm">tools: query, schema; resources: tables&lt;/text>
&lt;rect x="610" y="60" width="80" height="70" rx="6" class="box sys"/>
&lt;text x="650" y="92" text-anchor="middle" class="sm">local FS&lt;/text>
&lt;text x="650" y="108" text-anchor="middle" class="sm">↕&lt;/text>
&lt;rect x="610" y="140" width="80" height="70" rx="6" class="box sys"/>
&lt;text x="650" y="172" text-anchor="middle" class="sm">GitHub API&lt;/text>
&lt;text x="650" y="188" text-anchor="middle" class="sm">↕&lt;/text>
&lt;rect x="610" y="220" width="80" height="70" rx="6" class="box sys"/>
&lt;text x="650" y="252" text-anchor="middle" class="sm">PostgreSQL&lt;/text>
&lt;text x="650" y="268" text-anchor="middle" class="sm">↕&lt;/text>
&lt;path class="bidi" d="M155,178 L380,95"/>
&lt;path class="bidi" d="M280,178 L380,175"/>
&lt;path class="bidi" d="M155,228 L380,255"/>
&lt;path class="bidi" d="M580,95 L610,95"/>
&lt;path class="bidi" d="M580,175 L610,175"/>
&lt;path class="bidi" d="M580,255 L610,255"/>
&lt;text x="170" y="340" text-anchor="middle" class="sm">the clients inside the host speak MCP to the servers; the servers translate to the system&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Three roles. Let us pin down what each one does and where it physically lives.&lt;/p>
&lt;h3 id="host-the-ai-application">Host: the AI application&lt;/h3>
&lt;p>The &lt;strong>Host&lt;/strong> is the application the user opens. Claude Desktop, Cursor, Continue, ChatGPT Desktop, a custom agent your team builds, a VSCode extension. What the user perceives as &amp;ldquo;the product&amp;rdquo;.&lt;/p>
&lt;p>The Host is responsible for:&lt;/p>
&lt;ul>
&lt;li>Deciding which MCP servers to connect (configured by the user in a file or via the UI).&lt;/li>
&lt;li>Launching or connecting to each MCP server.&lt;/li>
&lt;li>Creating one MCP Client per server (it is 1:1, they are not shared).&lt;/li>
&lt;li>Embedding the &lt;strong>LLM&lt;/strong> (or calling it via API) that takes the decisions about which tools to use.&lt;/li>
&lt;li>Mediating the user&amp;rsquo;s authorisation for sensitive actions (showing the human &amp;ldquo;the agent wants to run tool X, do you allow it?&amp;rdquo;).&lt;/li>
&lt;/ul>
&lt;p>Important: &lt;strong>the LLM lives inside the Host&lt;/strong>, not in the servers. The servers are dumb; they execute operations when asked. The reasoning (&amp;ldquo;should I call this tool now?&amp;rdquo;) lives in the host&amp;rsquo;s LLM.&lt;/p>
&lt;h3 id="client-the-connection-one-per-server">Client: the connection, one per server&lt;/h3>
&lt;p>An &lt;strong>MCP Client&lt;/strong> is a specific connection between the Host and a Server. If your Host has 5 MCP servers configured, it has 5 clients, not one shared. Each client:&lt;/p>
&lt;ul>
&lt;li>Maintains its socket or stdio pipe with the server.&lt;/li>
&lt;li>Negotiates capabilities in the initial handshake (which protocol version, which primitives both support).&lt;/li>
&lt;li>Serialises JSON-RPC requests to the server and deserialises responses.&lt;/li>
&lt;li>Is the point where the Host invokes operations on the server.&lt;/li>
&lt;/ul>
&lt;p>The 1:1 client-server separation matters because it lets each server have its own session state, its specific permissions and its independent authenticated context. There is no multiplexing in the client.&lt;/p>
&lt;h3 id="server-the-piece-that-exposes-capabilities">Server: the piece that exposes capabilities&lt;/h3>
&lt;p>The &lt;strong>MCP Server&lt;/strong> is the piece that implements the tool-provider side of the protocol. It receives JSON-RPC from the client, processes it, executes the action against the underlying system and returns a response.&lt;/p>
&lt;p>There are two flavours physically:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Local server&lt;/strong>: it starts as a subprocess of the Host and communicates over stdio. Its lifecycle is the Host&amp;rsquo;s (when you close Claude Desktop, the local servers die). Typical model: your Host launches &lt;code>node filesystem-mcp-server.js&lt;/code> as a child.&lt;/li>
&lt;li>&lt;strong>Remote server&lt;/strong>: it runs as an independent service, reachable over HTTP. Multi-tenant, authenticated, scalable. Typical model: a company publishes &lt;code>https://mcp.acme.com/v1&lt;/code> and many hosts connect to it.&lt;/li>
&lt;/ul>
&lt;p>This difference has enormous consequences for observability (we will come back to it shortly).&lt;/p>
&lt;h3 id="summary-of-where-each-thing-lives">Summary of where each thing lives&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Lives in&lt;/th>
&lt;th>How many there are&lt;/th>
&lt;th>Speaks what with whom&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Host&lt;/td>
&lt;td>User&amp;rsquo;s machine&lt;/td>
&lt;td>1 (the open app)&lt;/td>
&lt;td>UI with the user; launches clients&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM&lt;/td>
&lt;td>Embedded in the Host (or cloud API)&lt;/td>
&lt;td>1 (the main one)&lt;/td>
&lt;td>Reasons; asks for tools&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Client&lt;/td>
&lt;td>Host&lt;/td>
&lt;td>1 per server&lt;/td>
&lt;td>JSON-RPC with its server&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Local server&lt;/td>
&lt;td>Subprocess of the Host&lt;/td>
&lt;td>1 per local integration&lt;/td>
&lt;td>stdio with its client&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Remote server&lt;/td>
&lt;td>External service&lt;/td>
&lt;td>1 per service&lt;/td>
&lt;td>HTTP/SSE with its clients&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Underlying system&lt;/td>
&lt;td>External&lt;/td>
&lt;td>Depends&lt;/td>
&lt;td>API/DB/FS, not MCP&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you get confused in a discussion, come back to this table. The number one source of errors in MCP is saying &amp;ldquo;the server&amp;rdquo; when you mean &amp;ldquo;the host&amp;rdquo;.&lt;/p>
&lt;h2 id="the-two-layers-of-the-protocol">The two layers of the protocol&lt;/h2>
&lt;p>MCP separates the &lt;strong>data layer&lt;/strong> and the &lt;strong>transport layer&lt;/strong>. This separation is what lets the protocol work over local stdio and over remote HTTP without changing anything in the primitives.&lt;/p>
&lt;h3 id="data-layer-json-rpc-with-mcp-extensions">Data Layer: JSON-RPC with MCP extensions&lt;/h3>
&lt;p>The data layer defines the vocabulary of the messages. It is &lt;strong>JSON-RPC 2.0&lt;/strong>. Each message is a JSON with &lt;code>jsonrpc: &amp;quot;2.0&amp;quot;&lt;/code>, a &lt;code>method&lt;/code> (eg &lt;code>tools/call&lt;/code>, &lt;code>resources/read&lt;/code>), &lt;code>params&lt;/code>, and an &lt;code>id&lt;/code> to correlate request with response.&lt;/p>
&lt;p>On top of JSON-RPC, MCP adds:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Lifecycle&lt;/strong>: the initial handshake (&lt;code>initialize&lt;/code>, &lt;code>initialized&lt;/code>) that negotiates capabilities.&lt;/li>
&lt;li>&lt;strong>The primitives&lt;/strong> (next section): &lt;code>tools/*&lt;/code>, &lt;code>resources/*&lt;/code>, &lt;code>prompts/*&lt;/code>, &lt;code>sampling/*&lt;/code> and so on.&lt;/li>
&lt;li>&lt;strong>Notifications&lt;/strong>: messages without a response (eg &lt;code>notifications/cancelled&lt;/code> to abort a tool in progress).&lt;/li>
&lt;li>&lt;strong>Meta-information&lt;/strong>: the &lt;code>params._meta&lt;/code> field carries cross-cutting metadata by convention (trace context, request IDs).&lt;/li>
&lt;/ul>
&lt;h3 id="transport-layer-how-the-messages-move">Transport Layer: how the messages move&lt;/h3>
&lt;p>The transport layer defines how the JSON-RPC messages travel. Two official transports:&lt;/p>
&lt;p>&lt;strong>stdio&lt;/strong>: the client launches the server as a subprocess and they communicate over its stdin/stdout/stderr with JSON-RPC. One message per line, separated by a newline. No network, no TLS handshake, no auth (trust is inherited from the operating system itself: if you launch the subprocess, you trust it). Minimal latency (~100 μs round-trip), maximum bandwidth (memcpy, not a socket).&lt;/p>
&lt;p>Use case: local servers that live on the same machine as the host. Most of the MCP servers you see in public directories are stdio.&lt;/p>
&lt;p>&lt;strong>Streamable HTTP&lt;/strong>: the client sends a POST to an HTTP endpoint on the server; the server responds with JSON, optionally opening a Server-Sent Events stream to send asynchronous notifications or long responses. Auth by bearer token, API key or custom headers.&lt;/p>
&lt;p>Introduced in the November 2025 spec, it replaces the pure SSE transport of earlier versions, which had bidirectionality limitations. Use case: remote servers serving many simultaneous clients, with authentication and multi-tenancy.&lt;/p>
&lt;p>Important: the primitives are the same on both transports. A &lt;code>tools/call&lt;/code> is identical over stdio and over HTTP. The transport is accidental, not fundamental.&lt;/p>
&lt;h2 id="the-six-primitives-placed-in-the-architecture">The six primitives: placed in the architecture&lt;/h2>
&lt;p>Here is the meat. There are six primitives in MCP. They are often confused because several look like they do similar things. The key classification: &lt;strong>three live on the server side&lt;/strong> (the server exposes, the client consumes) and &lt;strong>three on the client side&lt;/strong> (the client exposes, the server consumes).&lt;/p>
&lt;h3 id="server-side-what-the-server-gives-the-host">Server-side: what the server gives the host&lt;/h3>
&lt;p>&lt;strong>Tools&lt;/strong> are actions the server exposes. Each tool has a schema (typed parameters, description) and an implementation. When the host&amp;rsquo;s LLM decides to invoke a tool, the client sends &lt;code>tools/call&lt;/code> to the server, which executes it and returns a result.&lt;/p>
&lt;ul>
&lt;li>Example: the &lt;code>github-mcp&lt;/code> server exposes &lt;code>create_issue(repo, title, body)&lt;/code>. The host&amp;rsquo;s LLM decides &amp;ldquo;I am going to create an issue&amp;rdquo;, calls this tool, github-mcp talks to the GitHub API, returns the issue ID to the LLM.&lt;/li>
&lt;li>Architectural place: &lt;strong>the server exposes them, the LLM consumes them&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Resources&lt;/strong> are contextual data the server exposes, addressable by URI. They are not actions; they are content reads. A resource has a URI (&lt;code>file:///path/to/doc.md&lt;/code>, &lt;code>postgres://table/users&lt;/code>), metadata and an endpoint to read content.&lt;/p>
&lt;ul>
&lt;li>Example: the &lt;code>filesystem-mcp&lt;/code> server exposes the files in the authorised directories as resources. The LLM asks for &lt;code>resources/read&lt;/code> with the URI &lt;code>file:///docs/api.md&lt;/code> and gets the text.&lt;/li>
&lt;li>Architectural place: &lt;strong>the server exposes them, the host reads them (and optionally passes them to the LLM as context)&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Key difference between Tools and Resources: &lt;strong>Tools are verbs&lt;/strong> (they execute, they modify state, they have side effects); &lt;strong>Resources are nouns&lt;/strong> (they exist, they are read, they are idempotent). If you have something that is &amp;ldquo;search for text in files&amp;rdquo; → probably a Tool (an action). If it is &amp;ldquo;this specific file&amp;rdquo; → a Resource. The distinction matters for auditing and permissions: tools require more control.&lt;/p>
&lt;p>&lt;strong>Prompts&lt;/strong> are parameterised prompt templates the server exposes. The user or the host can invoke them to inject a conversational pattern into the model.&lt;/p>
&lt;ul>
&lt;li>Example: a &lt;code>code-review-mcp&lt;/code> server exposes a prompt &lt;code>review_diff(diff_text, style=&amp;quot;strict&amp;quot;)&lt;/code> that returns a complete, well-written prompt asking the LLM to review code.&lt;/li>
&lt;li>Architectural place: &lt;strong>the server exposes them, the user or the host invokes them, the LLM receives them as input&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Prompts are the least used of the three primitives; many servers do not even implement them. But they let a team publish good prompts as a reusable library, separate from the agent.&lt;/p>
&lt;h3 id="client-side-what-the-host-gives-the-server">Client-side: what the host gives the server&lt;/h3>
&lt;p>This is where MCP differs from protocols like HTTP REST: the server can also ask things of the host, it is not a one-way street. Three primitives travel in that direction.&lt;/p>
&lt;p>&lt;strong>Sampling&lt;/strong>: the server asks the host to run a generation with its LLM. That is, the server borrows the host&amp;rsquo;s LLM in order to reason.&lt;/p>
&lt;ul>
&lt;li>Example: the &lt;code>search-mcp&lt;/code> server receives a query from the agent, searches its corpus, finds 50 results and needs to summarise them before returning. Instead of having its own LLM, it sends a &lt;code>sampling/createMessage&lt;/code> to the client; the host passes this to its LLM, runs the generation with the user&amp;rsquo;s permissions, and returns the summary to the server.&lt;/li>
&lt;li>Architectural place: &lt;strong>the server asks for it, the host (with its LLM and the user&amp;rsquo;s authorisation) fulfils it&lt;/strong>.&lt;/li>
&lt;li>Why it matters: the user controls which model is used, what cost is paid, what permissions apply. The server does not need its own OpenAI API key.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Roots&lt;/strong>: the host tells the server where to look. Roots are URIs (directories, repositories, namespaces) the host authorises the server to explore.&lt;/p>
&lt;ul>
&lt;li>Example: your Claude Desktop starts &lt;code>filesystem-mcp&lt;/code> with roots &lt;code>[file:///Users/me/projects]&lt;/code>. The server knows it must only operate inside that folder, not in &lt;code>/etc/passwd&lt;/code>.&lt;/li>
&lt;li>Architectural place: &lt;strong>the host declares them in the handshake, the server respects them&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Elicitation&lt;/strong>: the server asks the host for additional information from the human user via a structured UI.&lt;/p>
&lt;ul>
&lt;li>Example: the &lt;code>stripe-mcp&lt;/code> server is about to process a refund of €5000. Before executing, it sends an &lt;code>elicitation/createMessage&lt;/code> to the client; the host shows the user &amp;ldquo;Confirm this €5000 refund&amp;rdquo; with a button; when the user confirms, it returns OK to the server, which then proceeds.&lt;/li>
&lt;li>Architectural place: &lt;strong>the server asks, the host shows the user, the user decides, the answer goes back to the server&lt;/strong>.&lt;/li>
&lt;li>It is the key primitive for human-in-the-loop on sensitive actions.&lt;/li>
&lt;/ul>
&lt;h3 id="visualising-the-flow-of-the-six-primitives">Visualising the flow of the six primitives&lt;/h3>
&lt;pre tabindex="0">&lt;code> HOST SERVER
│ │
Server-side ─────┼─────────────────────────────────────┤
│ │
tools/list ──────┼─────── asks what tools exist ──────▶│
│◀────── returns the list ────────────│
│ │
tools/call ──────┼────────── run this tool ───────────▶│
│◀────── result ──────────────────────│
│ │
resources/read ──┼────────── read this URI ───────────▶│
│◀────── content ─────────────────────│
│ │
prompts/get ─────┼─────── give me this prompt ────────▶│
│◀────── compiled prompt ─────────────│
│ │
Client-side ─────┼─────────────────────────────────────┤
│ │
sampling ────────│◀────── I need a generation ─────────│
│── use my LLM ───┐ │
│── returns ──────▼──────────────────▶│
│ │
roots ───────────┼───── declared in the handshake ────▶│
│ │
elicitation ─────│◀────── ask the user X ──────────────│
│── shows UI ────┐ │
│── confirms ────▼───────────────────▶│
&lt;/code>&lt;/pre>&lt;h2 id="json-rpc-in-action-a-concrete-example">JSON-RPC in action: a concrete example&lt;/h2>
&lt;p>To make the theory concrete, here is a real MCP conversation between a client and the &lt;code>filesystem-mcp&lt;/code> server:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-jsonc" data-lang="jsonc">// 1. Initial handshake (client → server)
{
&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: 1, &amp;#34;method&amp;#34;: &amp;#34;initialize&amp;#34;,
&amp;#34;params&amp;#34;: {
&amp;#34;protocolVersion&amp;#34;: &amp;#34;2026-03-01&amp;#34;,
&amp;#34;capabilities&amp;#34;: {
&amp;#34;sampling&amp;#34;: {}, // this client supports sampling
&amp;#34;roots&amp;#34;: { &amp;#34;listChanged&amp;#34;: true }
},
&amp;#34;clientInfo&amp;#34;: { &amp;#34;name&amp;#34;: &amp;#34;ClaudeDesktop&amp;#34;, &amp;#34;version&amp;#34;: &amp;#34;1.2.0&amp;#34; }
}
}
// 2. The server responds with its capabilities
{
&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: 1, &amp;#34;result&amp;#34;: {
&amp;#34;protocolVersion&amp;#34;: &amp;#34;2026-03-01&amp;#34;,
&amp;#34;capabilities&amp;#34;: {
&amp;#34;tools&amp;#34;: { &amp;#34;listChanged&amp;#34;: true },
&amp;#34;resources&amp;#34;: { &amp;#34;subscribe&amp;#34;: true, &amp;#34;listChanged&amp;#34;: true },
&amp;#34;prompts&amp;#34;: {}
},
&amp;#34;serverInfo&amp;#34;: { &amp;#34;name&amp;#34;: &amp;#34;filesystem-mcp&amp;#34;, &amp;#34;version&amp;#34;: &amp;#34;0.5.2&amp;#34; }
}
}
// 3. The client asks for the list of tools
{
&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: 2, &amp;#34;method&amp;#34;: &amp;#34;tools/list&amp;#34;
}
// 4. The server returns its tools with a schema
{
&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: 2, &amp;#34;result&amp;#34;: {
&amp;#34;tools&amp;#34;: [
{
&amp;#34;name&amp;#34;: &amp;#34;read_file&amp;#34;,
&amp;#34;description&amp;#34;: &amp;#34;Read a file from the filesystem&amp;#34;,
&amp;#34;inputSchema&amp;#34;: {
&amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,
&amp;#34;properties&amp;#34;: { &amp;#34;path&amp;#34;: { &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34; } },
&amp;#34;required&amp;#34;: [&amp;#34;path&amp;#34;]
}
},
{ &amp;#34;name&amp;#34;: &amp;#34;write_file&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;inputSchema&amp;#34;: {} },
{ &amp;#34;name&amp;#34;: &amp;#34;list_directory&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;...&amp;#34;, &amp;#34;inputSchema&amp;#34;: {} }
]
}
}
// 5. The LLM decides to call read_file; the client sends tools/call
{
&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: 3, &amp;#34;method&amp;#34;: &amp;#34;tools/call&amp;#34;,
&amp;#34;params&amp;#34;: {
&amp;#34;name&amp;#34;: &amp;#34;read_file&amp;#34;,
&amp;#34;arguments&amp;#34;: { &amp;#34;path&amp;#34;: &amp;#34;/Users/me/projects/notes.md&amp;#34; },
&amp;#34;_meta&amp;#34;: { // ← the extension where trace context will go
&amp;#34;traceparent&amp;#34;: &amp;#34;00-abc123...-def456-01&amp;#34;
}
}
}
// 6. The server returns the file content
{
&amp;#34;jsonrpc&amp;#34;: &amp;#34;2.0&amp;#34;, &amp;#34;id&amp;#34;: 3, &amp;#34;result&amp;#34;: {
&amp;#34;content&amp;#34;: [
{ &amp;#34;type&amp;#34;: &amp;#34;text&amp;#34;, &amp;#34;text&amp;#34;: &amp;#34;# My notes\n\n...&amp;#34; }
]
}
}
&lt;/code>&lt;/pre>&lt;p>The important thing to note: &lt;strong>&lt;code>params._meta&lt;/code>&lt;/strong>. That is the bag where MCP conventionally passes cross-cutting metadata, including trace context. We will come back to it shortly.&lt;/p>
&lt;h2 id="the-observability-problem-why-traditional-tracing-is-not-enough">The observability problem: why traditional tracing is not enough&lt;/h2>
&lt;p>So much for theory. Let us go down to the operational problem: in a 2026 production cluster, a typical agent has 5-15 MCP servers connected simultaneously, each with 5-20 tools, and each conversation with the agent can generate dozens of chained tool calls. Without observability, debugging incidents is impossible.&lt;/p>
&lt;p>Why generic tracing (Hubble, OTel without MCP conventions) is not enough:&lt;/p>
&lt;p>&lt;strong>Stdio is not visible on the network&lt;/strong>. Local servers talk over OS pipes. Your Hubble or your Datadog APM see nothing; there are no packets to capture. AgentSight (seen in the &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">previous post in the eBPF series&lt;/a>) with &lt;code>stdiocap&lt;/code> captures it but gives you the raw JSON-RPC, without semantic context (which tool it is, which resource, which prompt).&lt;/p>
&lt;p>&lt;strong>Generic HTTP does not understand MCP either&lt;/strong>. If you trace the HTTP to a remote MCP server without MCP conventions, you see a POST to &lt;code>/v1&lt;/code> with an opaque JSON-RPC body. You lose &amp;ldquo;which tool was invoked&amp;rdquo;, &amp;ldquo;what arguments&amp;rdquo;, &amp;ldquo;was it elicitation or sampling&amp;rdquo;. RED metrics per endpoint are no use to you; you need RED per tool.&lt;/p>
&lt;p>&lt;strong>JSON-RPC does not propagate trace context natively&lt;/strong>. Unlike HTTP (the W3C traceparent header) or gRPC (metadata), JSON-RPC has no standard field for trace context. If you do not propagate it, every call to the server starts a new trace disconnected from the agent&amp;rsquo;s trace.&lt;/p>
&lt;p>&lt;strong>Multistep multi-server is very hard to follow&lt;/strong>. A single user conversation can translate into: 1) a call to github-mcp &lt;code>get_pr&lt;/code>; 2) a call to filesystem-mcp &lt;code>read_file&lt;/code> for several files; 3) a call to the main LLM with all the context; 4) a call to postgres-mcp &lt;code>query&lt;/code>; 5) a call to slack-mcp &lt;code>send_message&lt;/code>. Without propagated trace context, those are five disconnected traces. With propagation, it is one tree.&lt;/p>
&lt;p>The solution: &lt;strong>OpenTelemetry semantic conventions for MCP&lt;/strong>, already stable in 2026.&lt;/p>
&lt;h2 id="opentelemetry-semantic-conventions-for-mcp">OpenTelemetry semantic conventions for MCP&lt;/h2>
&lt;p>The &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/">GenAI MCP semantic conventions&lt;/a> are the set of standardised attributes for MCP-related spans and metrics. They were published as part of the GenAI subgroup of the OpenTelemetry SIG and are the first part of the GenAI semantic conventions to reach stable.&lt;/p>
&lt;h3 id="why-mcp-specific-semantic-conventions">Why MCP-specific semantic conventions&lt;/h3>
&lt;p>Before they existed, teams instrumented MCP with the generic &lt;strong>RPC semantic conventions&lt;/strong> (the ones you would use for gRPC or XML-RPC). It half worked. The MCP-specific conventions add:&lt;/p>
&lt;ul>
&lt;li>Attributes to identify which primitive was executed (&lt;code>mcp.method.name = &amp;quot;tools/call&amp;quot;&lt;/code>).&lt;/li>
&lt;li>Attributes to identify which specific tool/resource/prompt was touched (&lt;code>mcp.tool.name&lt;/code>, &lt;code>mcp.resource.uri&lt;/code>, &lt;code>mcp.prompt.name&lt;/code>).&lt;/li>
&lt;li>Attributes for the bidirectional flow (sampling/elicitation requests from the server to the client).&lt;/li>
&lt;li>Attributes for the handshake (&lt;code>mcp.protocol.version&lt;/code>, &lt;code>mcp.client.name&lt;/code>, &lt;code>mcp.server.name&lt;/code>).&lt;/li>
&lt;li>Standardised RED metrics per tool (&lt;code>mcp.tool.call.duration&lt;/code>, &lt;code>mcp.tool.call.errors&lt;/code>).&lt;/li>
&lt;/ul>
&lt;h3 id="the-canonical-attributes">The canonical attributes&lt;/h3>
&lt;p>The attributes any MCP-aware instrumentation should emit:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Attribute&lt;/th>
&lt;th>Meaning&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>mcp.method.name&lt;/code>&lt;/td>
&lt;td>JSON-RPC method&lt;/td>
&lt;td>&lt;code>&amp;quot;tools/call&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.tool.name&lt;/code>&lt;/td>
&lt;td>Name of the tool&lt;/td>
&lt;td>&lt;code>&amp;quot;read_file&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.resource.uri&lt;/code>&lt;/td>
&lt;td>URI of the resource&lt;/td>
&lt;td>&lt;code>&amp;quot;file:///docs/api.md&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.prompt.name&lt;/code>&lt;/td>
&lt;td>Name of the prompt&lt;/td>
&lt;td>&lt;code>&amp;quot;code_review&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.session.id&lt;/code>&lt;/td>
&lt;td>MCP session ID&lt;/td>
&lt;td>&lt;code>&amp;quot;sess-abc123&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.protocol.version&lt;/code>&lt;/td>
&lt;td>Protocol version&lt;/td>
&lt;td>&lt;code>&amp;quot;2026-03-01&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.client.name&lt;/code>&lt;/td>
&lt;td>Client identity&lt;/td>
&lt;td>&lt;code>&amp;quot;ClaudeDesktop/1.2.0&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.server.name&lt;/code>&lt;/td>
&lt;td>Server identity&lt;/td>
&lt;td>&lt;code>&amp;quot;filesystem-mcp/0.5.2&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.transport&lt;/code>&lt;/td>
&lt;td>Transport used&lt;/td>
&lt;td>&lt;code>&amp;quot;stdio&amp;quot;&lt;/code> or &lt;code>&amp;quot;http&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>mcp.error.code&lt;/code>&lt;/td>
&lt;td>JSON-RPC error code&lt;/td>
&lt;td>&lt;code>-32602&lt;/code> (Invalid params)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.input_tokens&lt;/code>&lt;/td>
&lt;td>Tokens consumed (if sampling)&lt;/td>
&lt;td>&lt;code>1240&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.output_tokens&lt;/code>&lt;/td>
&lt;td>Tokens generated (if sampling)&lt;/td>
&lt;td>&lt;code>512&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The last two come from the generic GenAI semantic conventions and apply when the MCP call involves sampling (the server using the client&amp;rsquo;s LLM).&lt;/p>
&lt;h3 id="red-metrics-per-tool">RED metrics per tool&lt;/h3>
&lt;p>Beyond spans, the semantic conventions define three core metrics:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>mcp.tool.call.duration&lt;/code>&lt;/strong> (histogram): the latency of each invocation.&lt;/li>
&lt;li>&lt;strong>&lt;code>mcp.tool.call.count&lt;/code>&lt;/strong> (counter): the total number of invocations.&lt;/li>
&lt;li>&lt;strong>&lt;code>mcp.tool.call.errors&lt;/code>&lt;/strong> (counter): errors per tool.&lt;/li>
&lt;/ul>
&lt;p>Labelled with &lt;code>mcp.tool.name&lt;/code>, &lt;code>mcp.server.name&lt;/code>, &lt;code>mcp.client.name&lt;/code>. Pivotable in Grafana to answer &amp;ldquo;which tool is the slowest&amp;rdquo;, &amp;ldquo;which tool fails most&amp;rdquo;, &amp;ldquo;which client loads which server hardest&amp;rdquo;.&lt;/p>
&lt;h2 id="trace-context-propagation-the-params_meta-trick">Trace context propagation: the &lt;code>params._meta&lt;/code> trick&lt;/h2>
&lt;p>JSON-RPC has no headers like HTTP, so MCP cannot use the W3C &lt;code>traceparent&lt;/code> header directly. The solution the ecosystem has converged on: &lt;strong>propagate trace context in &lt;code>params._meta&lt;/code>&lt;/strong>.&lt;/p>
&lt;p>When the MCP client sends a &lt;code>tools/call&lt;/code>, its OTel instrumentation does:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">json&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry.propagate&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">inject&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">carrier&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">inject&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">carrier&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1"># fills it with traceparent/tracestate from the active span&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">params&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;read_file&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;arguments&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/notes.md&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;_meta&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">carrier&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># ← propagates trace context&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When the server receives it, it does the symmetrical thing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry.propagate&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">extract&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">extract&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;_meta&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{}))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">with&lt;/span> &lt;span class="n">tracer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">start_as_current_span&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;tools/call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># this span is a child of the client&amp;#39;s&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">execute_tool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The result: the server&amp;rsquo;s span is a child of the client&amp;rsquo;s span in the trace tree. When you look at the trace in Tempo or Phoenix, you see the whole chain: user → host → client → server → execution → response → client → host → response to the user.&lt;/p>
&lt;p>This requires both ends to instrument consistently. If the server does not extract the context, you see disconnected spans but at least you have traceability on the client side.&lt;/p>
&lt;h2 id="instrumentation-patterns">Instrumentation patterns&lt;/h2>
&lt;p>There are three routes to instrumenting MCP, in increasing order of effort:&lt;/p>
&lt;h3 id="1-fastmcp-with-built-in-opentelemetry">1. FastMCP with built-in OpenTelemetry&lt;/h3>
&lt;p>&lt;a href="https://gofastmcp.com/">FastMCP&lt;/a> is one of the most used Python frameworks for building MCP servers. It ships built-in OpenTelemetry instrumentation: every tool, resource template and prompt operation generates spans automatically with the correct MCP conventions.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">fastmcp&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">FastMCP&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry.sdk.trace.export&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OTLPSpanExporter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">mcp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FastMCP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;my-server&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">otel_endpoint&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;https://otel-collector:4318&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@mcp.tool&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">search_docs&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">query&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;Search the corpus for matching documents.&amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># this automatically generates a span with&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># mcp.tool.name=search_docs, mcp.method.name=tools/call, etc.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">run_search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">query&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Zero instrumentation code. Spans with the correct conventions. It is the recommended pattern if you are starting an MCP server in Python from scratch.&lt;/p>
&lt;h3 id="2-manual-opentelemetry-sdk">2. Manual OpenTelemetry SDK&lt;/h3>
&lt;p>For servers that already exist or are in other languages (TypeScript, Go), the option is to instrument manually with the standard OTel SDK and emit the conventional MCP attributes:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">opentelemetry&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">trace&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">tracer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">trace&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_tracer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="vm">__name__&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">handle_tools_call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">req&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">JSONRPCRequest&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">ctx&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">extract_trace_context&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">req&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">tracer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">start_as_current_span&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mcp.tools.call&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">ctx&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">span&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mcp.method.name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tools/call&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mcp.tool.name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">req&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mcp.server.name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;filesystem-mcp&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">execute_tool&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">req&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">Exception&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="n">e&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">set_attribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mcp.error.code&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mi">32603&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">span&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">record_exception&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>More boilerplate but it works with any existing server.&lt;/p>
&lt;h3 id="3-mcp-inspector-for-interactive-debugging">3. MCP Inspector for interactive debugging&lt;/h3>
&lt;p>&lt;a href="https://github.com/modelcontextprotocol/inspector">MCP Inspector&lt;/a> (official) is a tool for interactive protocol-level debugging. It launches a local proxy (port 6277) between your client and the server, and opens a web UI (port 6274) where you see every JSON-RPC message back and forth in real time.&lt;/p>
&lt;p>It is not production observability, it is development and debugging. But it is irreplaceable during the bring-up of a new server: you see exactly which requests arrive, which responses are returned, which errors occur. It saves hours of ad-hoc logging.&lt;/p>
&lt;h2 id="mcp-gateways-the-centralised-piece-for-enterprise">MCP Gateways: the centralised piece for enterprise&lt;/h2>
&lt;p>When your organisation has many agents connecting to many MCP servers, managing the connection matrix becomes operationally serious. The natural question, &amp;ldquo;can there be a proxy in front of all the MCP servers that centralises auth, rate limiting, logging and observability?&amp;rdquo;, already has an answer: &lt;strong>MCP Gateways&lt;/strong>.&lt;/p>
&lt;p>An MCP Gateway is a proxy that:&lt;/p>
&lt;ul>
&lt;li>Accepts MCP connections from the hosts/agents.&lt;/li>
&lt;li>Routes them to the corresponding backend MCP servers.&lt;/li>
&lt;li>Applies centralised &lt;strong>authentication and authorisation&lt;/strong> (which agent can call which tool).&lt;/li>
&lt;li>Applies &lt;strong>rate limiting&lt;/strong> per agent, per tool, per tenant.&lt;/li>
&lt;li>&lt;strong>Observes&lt;/strong>: it emits OTel metrics for every operation passing through.&lt;/li>
&lt;li>&lt;strong>Propagates the identity&lt;/strong> of the agent to the backend server (with several models: token forwarding, token exchange, impersonation).&lt;/li>
&lt;/ul>
&lt;p>The options that have established themselves in 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://doc.traefik.io/traefik-hub/mcp-gateway/">Traefik Hub MCP Gateway&lt;/a>&lt;/strong> — from the Traefik team. Declarative configuration, native integration with Traefik&amp;rsquo;s Kubernetes/Helm ecosystem.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.mintmcp.com/">MintMCP&lt;/a>&lt;/strong> — a gateway focused on observability and multi-tenancy. SaaS and self-host.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://openobserve.ai/blog/mcp-gateway-guide/">OpenObserve MCP Gateway&lt;/a>&lt;/strong> — integrated with the OpenObserve observability platform.&lt;/li>
&lt;/ul>
&lt;p>For small deployments (one team, few agents) a Gateway can be overkill. For enterprise (dozens of agents, dozens of servers, regulated compliance), it is practically mandatory.&lt;/p>
&lt;h2 id="real-use-cases-for-mcp-observability">Real use cases for MCP observability&lt;/h2>
&lt;p>Let us land this with five cases where properly instrumented MCP observability delivers immediate value:&lt;/p>
&lt;h3 id="1-audit-per-tool-per-tenant-per-agent">1. Audit per tool, per tenant, per agent&lt;/h3>
&lt;p>Question: &amp;ldquo;who ran the &lt;code>delete_repo&lt;/code> tool last month?&amp;rdquo;. Without MCP observability, impossible. With OTel conventions + identity propagation: a query in your trace backend filtering by &lt;code>mcp.tool.name=&amp;quot;delete_repo&amp;quot;&lt;/code>, grouping by &lt;code>mcp.client.name&lt;/code> or by the user_id propagated in &lt;code>_meta&lt;/code>. Compliance happy.&lt;/p>
&lt;h3 id="2-cost-per-tool-and-per-tenant">2. Cost per tool and per tenant&lt;/h3>
&lt;p>Question: &amp;ldquo;how much does each tool cost?&amp;rdquo;. If the tools invoke external APIs (Stripe, OpenAI sampling) or consume significant resources (a GPU for an inference tool), knowing their aggregated cost matters. With &lt;code>mcp.tool.call.duration&lt;/code> + &lt;code>gen_ai.usage.*&lt;/code> aggregated by tool and tenant, you build cost accountability dashboards without instrumenting anything extra.&lt;/p>
&lt;h3 id="3-debugging-multistep-chains-that-fail">3. Debugging multistep chains that fail&lt;/h3>
&lt;p>Question: &amp;ldquo;the agent failed to complete this task, where did it go wrong?&amp;rdquo;. The propagated trace connects: the user&amp;rsquo;s span → the LLM&amp;rsquo;s span with its CoT → the spans of each tool invoked → the final LLM span. If the chain broke at the third tool, in Tempo you see the red span with the specific error message. Reproducing the failure is trivial.&lt;/p>
&lt;h3 id="4-tool-latency-and-degradation">4. Tool latency and degradation&lt;/h3>
&lt;p>Question: &amp;ldquo;which tool is degrading?&amp;rdquo;. RED metrics per tool in Grafana show p95/p99 latency over time. When a tool starts climbing from 200ms to 800ms (because the underlying service is collapsing), you see it before the users complain.&lt;/p>
&lt;h3 id="5-detecting-loops-and-agentic-anomalies">5. Detecting loops and agentic anomalies&lt;/h3>
&lt;p>Question: &amp;ldquo;is any agent stuck in a loop?&amp;rdquo;. If an agent calls &lt;code>tools/call read_file&lt;/code> 80 times in 30 seconds for the same path, something is clearly wrong. An alert on &lt;code>mcp.tool.call.count&lt;/code> grouped by (session_id, tool_name) detects this. Combined with loop detection at the reasoning level, it closes the circle.&lt;/p>
&lt;h2 id="operational-pitfalls">Operational pitfalls&lt;/h2>
&lt;h3 id="lack-of-identity-propagation">Lack of identity propagation&lt;/h3>
&lt;p>Your Gateway authenticates the agent, but passes requests to the backend without propagating identity. The result: the backend&amp;rsquo;s logs say &amp;ldquo;service-account&amp;rdquo; for everything, and it is impossible to audit who invoked what. &lt;strong>Choose a propagation strategy early&lt;/strong>: token forwarding (simple, exposes tokens to the backend), token exchange (more secure), or impersonation with cross-logging.&lt;/p>
&lt;h3 id="stdio-servers-that-do-not-show-up-in-your-apm">Stdio servers that do not show up in your APM&lt;/h3>
&lt;p>This is the field&amp;rsquo;s number 1 pitfall. Your Cursor agent uses filesystem-mcp over stdio; you see nothing in Datadog because there is no network traffic. Solution: instrument the stdio server with an OTel SDK that exports over OTLP to your collector (via gRPC or HTTP; the OTel collector can receive even though the server talks stdio to its client). Or use AgentSight &lt;code>stdiocap&lt;/code> to capture the raw JSON-RPC and process it offline.&lt;/p>
&lt;h3 id="multiple-protocol-versions-in-production">Multiple protocol versions in production&lt;/h3>
&lt;p>Different clients use different MCP versions simultaneously. Your metrics dashboard mixes apples and oranges. ALWAYS label with &lt;code>mcp.protocol.version&lt;/code> and filter/group by it.&lt;/p>
&lt;h3 id="_meta-lost-when-passing-through-a-proxy">&lt;code>_meta&lt;/code> lost when passing through a proxy&lt;/h3>
&lt;p>Your Gateway accepts the client&amp;rsquo;s request, rewrites it for the backend, and forgets to copy &lt;code>params._meta&lt;/code>. The result: the trace is broken at the Gateway, two disconnected traces. Make sure your Gateway &lt;strong>preserves or re-injects&lt;/strong> trace context at each hop.&lt;/p>
&lt;h3 id="trace-volume-with-chatty-servers">Trace volume with chatty servers&lt;/h3>
&lt;p>Some MCP servers emit many small operations (filesystem listings, partial reads). Without sampling, they fill your backend with useless traces. Apply &lt;strong>tail-based sampling&lt;/strong> that keeps complete sessions or keeps only traces with errors or high latency.&lt;/p>
&lt;h3 id="cardinality-in-metrics">Cardinality in metrics&lt;/h3>
&lt;p>&lt;code>mcp.tool.call.duration&lt;/code> with &lt;code>mcp.session.id&lt;/code> as a label blows up cardinality. &lt;strong>Do not include unique per-session IDs in labels&lt;/strong>; keep cardinality under control with labels that take few discrete values (tool name, server name, client name, error code).&lt;/p>
&lt;h3 id="confusing-client-and-server-spans">Confusing client and server spans&lt;/h3>
&lt;p>When you look at the tree, distinguish: the client sees the total latency from its perspective (network included); the server sees only its own work. If you look only at the server span to debug the latency perceived by the user, you miss the RTT. Use both.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Experimental WebSocket MCP transport&lt;/strong>: an alternative to Streamable HTTP, not yet standard.&lt;/li>
&lt;li>&lt;strong>MCP servers in cloud-native deployments with sidecars&lt;/strong>: an emerging pattern of deploying MCP servers as pod sidecars.&lt;/li>
&lt;li>&lt;strong>MCP federation&lt;/strong>: composing several servers as one (similar to GraphQL federation).&lt;/li>
&lt;li>&lt;strong>eBPF + MCP&lt;/strong>: how AgentSight&amp;rsquo;s &lt;code>stdiocap&lt;/code> and Cilium&amp;rsquo;s hooks complement native instrumentation.&lt;/li>
&lt;li>&lt;strong>MCP testing and contract tests&lt;/strong>: how to validate that your server complies with the spec.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;p>Specification and concepts:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://modelcontextprotocol.io/">Model Context Protocol — sitio oficial&lt;/a> — entrada canónica.&lt;/li>
&lt;li>&lt;a href="https://modelcontextprotocol.io/docs/learn/architecture">MCP architecture overview&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://modelcontextprotocol.info/docs/concepts/transports/">Transports — MCP docs&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/modelcontextprotocol/inspector">MCP Inspector (GitHub)&lt;/a> — debugging interactivo.&lt;/li>
&lt;/ul>
&lt;p>OpenTelemetry GenAI MCP:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/">Semantic conventions for Model Context Protocol — OpenTelemetry&lt;/a> — referencia normativa.&lt;/li>
&lt;li>&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/269">Adding OpenTelemetry Trace Support to MCP (Discussion #269)&lt;/a> — historia de la propuesta.&lt;/li>
&lt;li>&lt;a href="https://oneuptime.com/blog/post/2026-03-26-how-to-instrument-mcp-servers-with-opentelemetry/view">How to Instrument MCP Servers with OpenTelemetry (OneUptime)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.elastic.co/observability-labs/blog/mcp-tracing-opentelemetry-elastic-apm">How to trace MCP server tool calls with OpenTelemetry and Elastic APM&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://signoz.io/blog/mcp-observability-with-otel/">MCP Observability with OpenTelemetry (SigNoz)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://developers.redhat.com/articles/2026/04/06/distributed-tracing-agentic-workflows-opentelemetry">Distributed tracing for agentic workflows (Red Hat Developer)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.mintmcp.com/blog/opentelemetry-ai-agents">OpenTelemetry for AI Agents in MCP Workflows (MintMCP)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Frameworks and gateways:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://gofastmcp.com/servers/telemetry">FastMCP OpenTelemetry&lt;/a> — instrumentación built-in.&lt;/li>
&lt;li>&lt;a href="https://doc.traefik.io/traefik-hub/mcp-gateway/">Traefik Hub MCP Gateway&lt;/a> — gateway de Traefik.&lt;/li>
&lt;li>&lt;a href="https://www.mintmcp.com/">MintMCP&lt;/a> — gateway con foco en observabilidad.&lt;/li>
&lt;li>&lt;a href="https://openobserve.ai/blog/mcp-gateway-guide/">OpenObserve MCP Gateway guide&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://dev.to/composiodev/what-is-an-mcp-gateway-and-why-do-enterprise-ai-teams-need-one-in-2026-1lie">What is an MCP Gateway (DEV Community)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/traceloop/opentelemetry-mcp-server">OpenTelemetry MCP Server (Traceloop)&lt;/a> — el patrón inverso: usar MCP para que agentes consulten traces OTel.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Post anterior: &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails y safety&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">AgentSight y el nuevo tracing de LLMs&lt;/a> — donde se introdujo &lt;code>stdiocap&lt;/code> para capturar stdio de servidores MCP locales.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: la capa después del tracing&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>AgentSight and the new LLM tracing: zero-instrumentation with eBPF against Langfuse, LangSmith, Phoenix and company</title><link>https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/</link><pubDate>Tue, 19 May 2026 18:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Observing an LLM agent in production in 2026 splits into two approaches with opposite philosophies. The &lt;strong>instrumented&lt;/strong> one, dominant until 2025, lives in tools such as &lt;a href="https://langfuse.com/">Langfuse&lt;/a>, &lt;a href="https://www.langchain.com/langsmith">LangSmith&lt;/a>, &lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a>, &lt;a href="https://www.helicone.ai/">Helicone&lt;/a>, &lt;a href="https://www.traceloop.com/openllmetry">OpenLLMetry/Traceloop&lt;/a> or &lt;a href="https://pydantic.dev/logfire">Pydantic Logfire&lt;/a>: you install an SDK, decorate your calls, emit spans with the &lt;strong>OpenTelemetry GenAI&lt;/strong> convention (&lt;code>gen_ai.request.model&lt;/code>, &lt;code>gen_ai.usage.input_tokens&lt;/code>, and so on) and export them to a backend. Enormous depth when you control the code; zero visibility when the agent is an opaque binary you run without instrumenting. The &lt;strong>zero-instrumentation&lt;/strong> one, which &lt;a href="https://github.com/eunomia-bpf/agentsight">AgentSight&lt;/a> popularised in the second half of 2025, turns the perspective 180º: it puts &lt;strong>eBPF hooks on the uprobes of the SSL/TLS libraries&lt;/strong> and captures the plaintext of every HTTPS request before encryption, &lt;strong>without touching the app&amp;rsquo;s code&lt;/strong>, with less than &lt;strong>3% overhead&lt;/strong> and the guarantee of being &lt;strong>tamper-proof&lt;/strong> (the agent cannot falsify what is seen in the kernel). Combined with BPF capture of stdio for local MCP servers, AgentSight gives you complete observability of any agent, including closed binaries such as Claude Code, Gemini CLI or Cursor, on a Kubernetes cluster. The two families are not enemies: the 2026 reference stack combines both (instrumented for your own apps with LangChain, eBPF for opaque binaries and tamper-proof compliance) over &lt;strong>OpenTelemetry GenAI semantic conventions&lt;/strong> as the common vocabulary the ecosystem is stabilising this year.&lt;/p>
&lt;blockquote>
&lt;p>This is the &lt;strong>fourth and last post of the eBPF series&lt;/strong>. Part 1: &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a>. Part 2: &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon: runtime security&lt;/a>. Part 3: &lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble: network observability&lt;/a>. Here we close the circle with the &lt;strong>semantic&lt;/strong> dimension: what an AI agent does, not only what network it opens or what syscalls it emits.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-analogy-traditional-apm-vs-network-sniffer">The analogy: traditional APM vs network sniffer&lt;/h2>
&lt;p>Anyone who has operated enterprise applications knows the two tribes of monitoring. The &lt;strong>APM&lt;/strong> tribe (New Relic, AppDynamics, Datadog APM): you install an agent or an SDK in each application, mark spans, collect traces with enormous depth inside each process, down to lines of code, SQL queries, Java methods. The &lt;strong>wire-level&lt;/strong> tribe (network sniffers, SolarWinds NPM-style tools, NetFlow): it does not touch the application; it observes the cable, sees protocols, latencies, retransmissions, identifies problems the app does not know it has.&lt;/p>
&lt;p>Each one sees different things and both are useful. Anyone who has lived through a serious incident where APM said &amp;ldquo;all green&amp;rdquo; while users suffered knows that wire-level would have caught the problem (a saturated middlebox, a badly configured MTU, a TCP timeout). Anyone who has tried to debug a memory leak with sniffers knows it was impossible without APM.&lt;/p>
&lt;p>LLM agent observability in 2026 is exactly at this point. The &lt;strong>APM-style&lt;/strong> has been in place for a couple of years: Langfuse, LangSmith, Phoenix, OpenLLMetry. Enormous depth, requires instrumenting the app. The &lt;strong>wire-level with eBPF&lt;/strong> has just arrived: AgentSight is the first project to take it to production. Less depth inside the agent, but it sees any agent without touching anything and it is &lt;strong>tamper-proof&lt;/strong>. Both are useful. The industry is in full coexistence.&lt;/p>
&lt;h2 id="why-observing-llm-agents-is-different">Why observing LLM agents is different&lt;/h2>
&lt;p>Before getting into tools, it is worth pausing on what makes LLM agents specific as subjects of observability:&lt;/p>
&lt;p>&lt;strong>Non-determinism.&lt;/strong> The same input can produce different outputs. Reproducing an incident requires capturing &lt;strong>exactly&lt;/strong> the conversation, the model, the parameters and, ideally, the seed. An aggregate &amp;ldquo;p95 latency&amp;rdquo; metric falls short; what you need is replay of the individual trace.&lt;/p>
&lt;p>&lt;strong>A chain of external invocations.&lt;/strong> A typical agent calls LLM → tools (tool calling) → MCP servers → other APIs → back to the LLM. A chat session can generate &lt;strong>dozens of chained calls&lt;/strong> that have to be correlated by trace_id to understand the decision.&lt;/p>
&lt;p>&lt;strong>Cost linear in tokens.&lt;/strong> Every call is paid for in tokens. Without tracing input/output tokens per request, you cannot assign cost to a tenant or a team, nor detect loops that eat your budget in an hour.&lt;/p>
&lt;p>&lt;strong>Semantic risk.&lt;/strong> Prompt injection (a user input containing instructions to manipulate the model), jailbreaks, secret leakage through tool calls. It is a class of problem that does not appear in traditional applications, and observability must see it.&lt;/p>
&lt;p>&lt;strong>Opaque binaries.&lt;/strong> In 2026, many teams deploy &lt;strong>third-party agents&lt;/strong> such as Claude Code, the Cursor agent, Aider, Gemini CLI or Codex CLI as internal tools. They are not your own applications; they are closed binaries that call the vendor&amp;rsquo;s API. Instrumenting them is impossible. Observing them requires something else.&lt;/p>
&lt;p>&lt;strong>Multi-agent and orchestration.&lt;/strong> More and more architectures have agents that invoke other agents (planner → executor → critic). Observability must understand the topology, not just the individual span.&lt;/p>
&lt;p>With these five points in mind, the tools we are about to see differ mainly in &lt;strong>which parts&lt;/strong> of the problem they cover well and &lt;strong>which parts&lt;/strong> they leave blind.&lt;/p>
&lt;h2 id="the-instrumented-approach-how-it-works">The instrumented approach: how it works&lt;/h2>
&lt;p>The model is direct and familiar:&lt;/p>
&lt;ol>
&lt;li>Your code calls the LLM or the tools using an official library: &lt;code>openai&lt;/code>, &lt;code>anthropic&lt;/code>, &lt;code>langchain&lt;/code>, &lt;code>llama_index&lt;/code>, &lt;code>dspy&lt;/code>.&lt;/li>
&lt;li>You install a tracer SDK (Langfuse, LangSmith, OpenLLMetry, Logfire) that &lt;strong>wraps&lt;/strong> or &lt;strong>monkey-patches&lt;/strong> those libraries.&lt;/li>
&lt;li>Each call emits an &lt;strong>OpenTelemetry span&lt;/strong> with standardised attributes: model used, input/output tokens, latency, parameters, messages, tool invoked, result.&lt;/li>
&lt;li>The spans are exported over OTLP to a backend that shows them as a tree of traces.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Typical example with OpenLLMetry + any SDK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">traceloop.sdk&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Traceloop&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">openai&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">Traceloop&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">init&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;my-agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">api_endpoint&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;https://otel-collector:4318&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpenAI&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># this call automatically emits a span with&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># gen_ai.request.model, gen_ai.usage.input_tokens, etc.&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">resp&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;gpt-4.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">messages&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[{&lt;/span>&lt;span class="s2">&amp;#34;role&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;content&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;...&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>What you see afterwards: a dashboard with each conversation as a trace, each call as a span, the complete prompts and completions (if you opt in), the computed cost, latencies per span, errors flagged.&lt;/p>
&lt;h3 id="opentelemetry-genai-semantic-conventions-the-common-vocabulary">OpenTelemetry GenAI semantic conventions: the common vocabulary&lt;/h3>
&lt;p>The fragmentation of the field is being mitigated with &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">&lt;strong>OpenTelemetry GenAI Semantic Conventions&lt;/strong>&lt;/a>. It is the CNCF&amp;rsquo;s effort to get &lt;strong>all&lt;/strong> tools to emit spans with the same attribute names:&lt;/p>
&lt;ul>
&lt;li>&lt;code>gen_ai.system&lt;/code> — the provider (openai, anthropic, vertex_ai, and so on).&lt;/li>
&lt;li>&lt;code>gen_ai.request.model&lt;/code> — the model requested (&lt;code>gpt-4.1&lt;/code>, &lt;code>claude-3-5-sonnet&lt;/code>).&lt;/li>
&lt;li>&lt;code>gen_ai.response.model&lt;/code> — the model actually used (sometimes it differs, e.g. fallbacks).&lt;/li>
&lt;li>&lt;code>gen_ai.usage.input_tokens&lt;/code> and &lt;code>gen_ai.usage.output_tokens&lt;/code> — counters.&lt;/li>
&lt;li>&lt;code>gen_ai.request.temperature&lt;/code>, &lt;code>gen_ai.request.top_p&lt;/code>, and so on — parameters.&lt;/li>
&lt;li>&lt;code>gen_ai.response.finish_reasons&lt;/code> — why it ended (stop, length, content_filter).&lt;/li>
&lt;li>&lt;code>gen_ai.operation.name&lt;/code> — the type of operation (chat, embedding, completion).&lt;/li>
&lt;/ul>
&lt;p>In early 2026, the &lt;strong>client spans&lt;/strong> moved from experimental to stable. The rest (server spans, multi-agent events) is still in development. The operational meaning: if your SDK emits these attributes, &lt;strong>any backend that understands OTel GenAI&lt;/strong> can consume them. Moving from Langfuse to Phoenix to Helicone does not imply re-instrumenting, only changing the exporter.&lt;/p>
&lt;p>The SIG is actively developing &lt;strong>conventions for multi-agent systems&lt;/strong>: agent teams, tasks, actions, memory, artifact tracking. This is what is missing for complex agent architectures to have a common vocabulary. In 2026 it is experimental; stabilisation is expected at the end of the year or the start of 2027.&lt;/p>
&lt;h3 id="instrumented-tools-the-2026-landscape">Instrumented tools: the 2026 landscape&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Licence&lt;/th>
&lt;th>Self-host&lt;/th>
&lt;th>Focus&lt;/th>
&lt;th>Where it shines&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Langfuse&lt;/strong>&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>LLM observability + evals + prompt mgmt&lt;/td>
&lt;td>Best OSS balance, complete suite&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LangSmith&lt;/strong>&lt;/td>
&lt;td>Commercial&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Native LangChain/LangGraph&lt;/td>
&lt;td>If you use LangChain, zero-config integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Arize Phoenix&lt;/strong>&lt;/td>
&lt;td>ELv2 (OSS)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>OTel-native, strong RAG&lt;/td>
&lt;td>Vector DBs, retrieval, embeddings&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Helicone&lt;/strong>&lt;/td>
&lt;td>Commercial + OSS lite&lt;/td>
&lt;td>Yes (lite)&lt;/td>
&lt;td>Simple proxy&lt;/td>
&lt;td>Setup in minutes, OpenAI-only&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>OpenLLMetry / Traceloop&lt;/strong>&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>OTel SDK for LLMs&lt;/td>
&lt;td>Vendor-neutral, exports to any OTel backend&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Pydantic Logfire&lt;/strong>&lt;/td>
&lt;td>Commercial&lt;/td>
&lt;td>No&lt;/td>
&lt;td>App + LLM unified&lt;/td>
&lt;td>If you use Pydantic AI, native integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Weights &amp;amp; Biases Weave&lt;/strong>&lt;/td>
&lt;td>Commercial&lt;/td>
&lt;td>Limited&lt;/td>
&lt;td>Experimentation + production&lt;/td>
&lt;td>If you already use W&amp;amp;B for training&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Laminar / Braintrust&lt;/strong>&lt;/td>
&lt;td>Commercial&lt;/td>
&lt;td>No / Yes&lt;/td>
&lt;td>Evals + tracing&lt;/td>
&lt;td>More recent, focused on evaluation&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="deep-dive-langfuse">Deep dive: Langfuse&lt;/h3>
&lt;p>It is worth pausing on &lt;a href="https://langfuse.com/">Langfuse&lt;/a> because in 2026 it is &lt;strong>the default choice among the open-source options&lt;/strong> and the one most teams have adopted this year. It is a &lt;a href="https://github.com/langfuse/langfuse">YC W23&lt;/a> project, &lt;strong>MIT&lt;/strong> licence, and keeps a sustained release cadence with serious architectural changes between versions.&lt;/p>
&lt;p>&lt;strong>Four declared pillars&lt;/strong>: observability (tracing), evaluations, prompt management, playground/datasets. Each one separately has complete commercial products behind it; Langfuse integrates them into a single platform with a single backend.&lt;/p>
&lt;h4 id="sdk-v4-otel-native-not-a-replacement">SDK v4: OTEL-native, not a replacement&lt;/h4>
&lt;p>The big recent operational change is the &lt;strong>SDK v4&lt;/strong>, a thin layer over the official OpenTelemetry client. The choice is deliberate: instead of maintaining a client of its own that would fall behind the OTel primitives, Langfuse uses the standard SDK and &lt;strong>enriches&lt;/strong> the spans with LLM-specific attributes and helpers. The consequence: any code already instrumented with vanilla OpenTelemetry (&lt;code>@opentelemetry/sdk-node&lt;/code>, &lt;code>opentelemetry-sdk&lt;/code> in Python) &lt;strong>can export to Langfuse without major changes&lt;/strong>, and the other way round, if tomorrow you want to migrate from Langfuse to another OTel backend, the spans are portable.&lt;/p>
&lt;p>In Python the idiomatic decorator is &lt;code>@observe&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langfuse&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">observe&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">get_client&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">langfuse&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">get_client&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@observe&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">buscar_documentos&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">query&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># any internal call is traced as well&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">vector_store&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">similarity_search&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">query&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">as_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;generation&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">llamar_llm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">prompt&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1"># marked as &amp;#34;generation&amp;#34; so it appears with LLM metadata&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">openai_client&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">chat&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">completions&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@observe&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">pipeline_rag&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pregunta&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">docs&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">buscar_documentos&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pregunta&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">llamar_llm&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">build_prompt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pregunta&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">docs&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The call tree is captured automatically: the trace shows &lt;code>pipeline_rag&lt;/code> as the root span, with &lt;code>buscar_documentos&lt;/code> and &lt;code>llamar_llm&lt;/code> as children, nested. Without writing a single &lt;code>with tracer.start_as_current_span(...)&lt;/code> by hand.&lt;/p>
&lt;p>In TypeScript the equivalent is modular: you install &lt;code>@langfuse/tracing&lt;/code>, &lt;code>@langfuse/otel&lt;/code> and &lt;code>@opentelemetry/sdk-node&lt;/code>, and you can use TS decorators, context managers or manual spans, the three models interoperate. The consequence: third-party libraries that emit OTel spans (&lt;code>openai&lt;/code>, &lt;code>@anthropic-ai/sdk&lt;/code>, Vercel AI SDK instrumentations) show up in Langfuse with no extra work.&lt;/p>
&lt;h4 id="self-host-architecture-designed-for-serious-production">Self-host architecture: designed for serious production&lt;/h4>
&lt;p>The Langfuse backend architecture has &lt;strong>two explicit decisions&lt;/strong> that set its self-host deployment apart:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Persistence in S3/Blob Storage first&lt;/strong>. When a tracing event arrives, &lt;strong>it is persisted in object storage before touching the database&lt;/strong>. Only when the later processing confirms OK is it inserted into Postgres/Clickhouse. If the DB goes down temporarily, the events &lt;strong>are not lost&lt;/strong>; they stay in S3 waiting to be reprocessed. For production where losing the traces of an incident is equivalent to losing evidence, this is load-bearing.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Long migrations as background jobs&lt;/strong>. Schema upgrades that on other platforms mean a downtime window run in the background on Langfuse while the application keeps serving. Upgrade downtime drops drastically.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>The officially supported deployment modes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Docker Compose&lt;/strong>: for development and POCs. One command, everything up.&lt;/li>
&lt;li>&lt;strong>VM&lt;/strong>: a single node, containers, no orchestration. For small environments.&lt;/li>
&lt;li>&lt;strong>Kubernetes with Helm&lt;/strong>: the recommended mode for production. Official chart maintained. Supports external Postgres, external Clickhouse, external S3, HPA.&lt;/li>
&lt;/ul>
&lt;p>The external dependencies in a typical production setup: &lt;strong>Postgres&lt;/strong> (metadata, prompts, configuration), &lt;strong>Clickhouse&lt;/strong> (tracing events, high-cardinality queries), &lt;strong>S3 or a compatible blob store&lt;/strong> (pending events), &lt;strong>Redis&lt;/strong> (the queue between components). Yes, that is several pieces; it is what holds up the durability and the scale.&lt;/p>
&lt;h4 id="prompt-management-as-a-first-class-citizen">Prompt management as a first-class citizen&lt;/h4>
&lt;p>What sets Langfuse apart from platforms focused only on tracing is that &lt;strong>the prompts live in Langfuse&lt;/strong>, not in the application repo or in spreadsheets. Each prompt has:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Name and version&lt;/strong> (v1, v2, v3&amp;hellip;). Changing the prompt does not require redeploying the app: the app asks the SDK for the prompt, and the SDK caches it and refreshes when there is a new version.&lt;/li>
&lt;li>&lt;strong>Typed variables&lt;/strong>: &lt;code>{{user_input}}&lt;/code>, &lt;code>{{context}}&lt;/code>. Rendering with validation.&lt;/li>
&lt;li>&lt;strong>Tags and labels&lt;/strong>: by environment (&lt;code>production&lt;/code>, &lt;code>staging&lt;/code>), by team, by experiment.&lt;/li>
&lt;li>&lt;strong>Client and server cache&lt;/strong>: the SDK caches locally with a configurable TTL, avoiding a roundtrip to Langfuse on every call.&lt;/li>
&lt;li>&lt;strong>Linkage with traces&lt;/strong>: each trace records which exact version of which prompt was used. Investigating &amp;ldquo;this answer came out wrong&amp;rdquo; leads to prompt version Y, not to &amp;ldquo;some version of the prompt at some point&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langfuse&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">get_client&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">langfuse&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">get_client&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">prompt&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">langfuse&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_prompt&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rag-system-prompt&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">version&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">3&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or by label: langfuse.get_prompt(&amp;#34;rag-system-prompt&amp;#34;, label=&amp;#34;production&amp;#34;)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">compiled&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">prompt&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">compile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">docs_text&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">user_input&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">question&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># &amp;#39;compiled&amp;#39; is the final string, ready to send to the LLM&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For teams iterating on prompts daily, this is what avoids the chaos of &amp;ldquo;which version of the prompt is actually running in production right now&amp;rdquo;.&lt;/p>
&lt;h4 id="evaluations-four-combinable-evaluation-models">Evaluations: four combinable evaluation models&lt;/h4>
&lt;p>Langfuse covers the four patterns of answer evaluation:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LLM-as-a-judge&lt;/strong>: you configure a model (typically GPT-4 or Claude) with a rubric and it evaluates each answer. Result: a numeric score (0-1) and a justification. Applicable to automatic tracing (all answers) or batch (a dataset selection).&lt;/li>
&lt;li>&lt;strong>User feedback&lt;/strong>: the app lets the user mark an answer as good/bad. The feedback is associated with the trace and the prompt version, which lets you see which versions have a worse rate.&lt;/li>
&lt;li>&lt;strong>Manual labeling&lt;/strong>: a UI where human labelers score answers. Useful for golden datasets and for evaluating the judge.&lt;/li>
&lt;li>&lt;strong>Custom evaluators through the API/SDK&lt;/strong>: your own evals (a unit test, a business metric) report a score through the API. They integrate with CI.&lt;/li>
&lt;/ul>
&lt;p>Combined, they give &lt;strong>regression testing&lt;/strong> of the prompt: you change from v3 to v4, evaluate the golden dataset with LLM-as-judge, compare; if v4 gets worse in any of the segments, the merge fails.&lt;/p>
&lt;h4 id="integrations">Integrations&lt;/h4>
&lt;p>Langfuse does not compete with OpenLLMetry, LangChain or LiteLLM: it &lt;strong>integrates&lt;/strong> them. The ones that are tested and documented:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenTelemetry&lt;/strong>: any OTel instrumentation emits to Langfuse over OTLP.&lt;/li>
&lt;li>&lt;strong>LangChain and LangGraph&lt;/strong>: a native callback that captures the whole chain.&lt;/li>
&lt;li>&lt;strong>LlamaIndex&lt;/strong>: native callback.&lt;/li>
&lt;li>&lt;strong>OpenAI SDK&lt;/strong> (Python and TS): a wrapper that adds tracing automatically.&lt;/li>
&lt;li>&lt;strong>LiteLLM&lt;/strong>: integration as a callback, which covers 100+ providers through LiteLLM.&lt;/li>
&lt;li>&lt;strong>OpenLLMetry / Traceloop&lt;/strong>: they emit to Langfuse like any OTel backend.&lt;/li>
&lt;li>&lt;strong>MLflow&lt;/strong>: through an OTel exporter from MLflow to Langfuse.&lt;/li>
&lt;li>&lt;strong>Vercel AI SDK&lt;/strong>: native instrumentation.&lt;/li>
&lt;/ul>
&lt;p>The strategy is clear: &lt;strong>Langfuse is a backend, not an SDK&lt;/strong>. Your team chooses how it instruments; Langfuse accepts any path. The operational consequence: moving from Langfuse to another OTel backend tomorrow is viable.&lt;/p>
&lt;h4 id="when-langfuse-is-not-the-answer">When Langfuse is not the answer&lt;/h4>
&lt;p>So as not to present it as a silver bullet:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>If you only use LangChain and have no resources to self-host&lt;/strong>: LangSmith will give you a smoother integration (it is the same team).&lt;/li>
&lt;li>&lt;strong>If your only need is a proxy with cost tracking and no evals&lt;/strong>: Helicone is simpler.&lt;/li>
&lt;li>&lt;strong>If you want an integrated commercial vendor solution&lt;/strong>: Datadog LLM Observability, New Relic AI Monitoring or Dynatrace AI are Enterprise alternatives with 24/7 support.&lt;/li>
&lt;li>&lt;strong>If your load is pure batch massive inference with no agents&lt;/strong>: you probably do not need semantic tracing; Prometheus + Grafana with OTel metrics is enough.&lt;/li>
&lt;/ul>
&lt;p>For everything else, your own apps with serious tracing, multi-tenant with quotas, teams iterating on prompts daily, RAG with continuous evaluation, Langfuse is the safe bet.&lt;/p>
&lt;p>&lt;strong>Quick choice summary&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>LangChain → LangSmith&lt;/strong> (zero effort, automatic instrumentation).&lt;/li>
&lt;li>&lt;strong>Your own multi-framework applications with OSS → Langfuse&lt;/strong> (MIT, self-host, complete).&lt;/li>
&lt;li>&lt;strong>RAG with vector stores → Arize Phoenix&lt;/strong> (better retrieval visibility).&lt;/li>
&lt;li>&lt;strong>Simple proxy, low budget → Helicone&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Strict vendor neutrality → OpenLLMetry/Traceloop&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Pydantic AI → Logfire&lt;/strong> (same team).&lt;/li>
&lt;/ul>
&lt;h3 id="strengths-and-weaknesses-of-the-instrumented-model">Strengths and weaknesses of the instrumented model&lt;/h3>
&lt;p>&lt;strong>Strengths&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Enormous depth&lt;/strong>: nested spans with all the context (chain steps, retrieval, embeddings, tool calls).&lt;/li>
&lt;li>&lt;strong>Semantic vocabulary&lt;/strong>: the SDK knows the domain (LLM, vector store, agent).&lt;/li>
&lt;li>&lt;strong>Maturity&lt;/strong>: three years of evolution, a rich ecosystem, ready-made dashboards.&lt;/li>
&lt;li>&lt;strong>Built-in evals&lt;/strong>: the top platforms combine tracing with evaluation (judge LLM, datasets, regression).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Weaknesses&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Requires control of the code&lt;/strong>: if you cannot instrument, it does not work.&lt;/li>
&lt;li>&lt;strong>Trust in the app&lt;/strong>: if the app reports badly or has a bug, so does the trace. It is not tamper-proof.&lt;/li>
&lt;li>&lt;strong>Coupling to the SDK&lt;/strong>: version changes in a library can break the instrumentation.&lt;/li>
&lt;li>&lt;strong>Uneven coverage&lt;/strong>: Python SDKs are mature; Go, Rust and JS are younger.&lt;/li>
&lt;/ul>
&lt;h2 id="the-zero-instrumentation-approach-agentsight">The zero-instrumentation approach: AgentSight&lt;/h2>
&lt;p>&lt;a href="https://github.com/eunomia-bpf/agentsight">AgentSight&lt;/a> is the project from the &lt;code>eunomia-bpf&lt;/code> group that champions the opposite approach. Its &lt;a href="https://arxiv.org/abs/2508.02736">paper on arxiv (2508.02736)&lt;/a>, presented at the &lt;em>Workshop on Practical Adoption Challenges of ML for Systems&lt;/em>, formalises the proposal. The premise is direct:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>Instead of instrumenting the agent, observe it at the system boundary.&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>And &amp;ldquo;system boundary&amp;rdquo; means &lt;strong>the kernel boundary&lt;/strong>: the last point before a piece of data leaves the process towards the network or the filesystem. There, with eBPF, things are seen as they are, without the application being able to cooperate in hiding them.&lt;/p>
&lt;h3 id="architecture-three-planes">Architecture: three planes&lt;/h3>
&lt;p>AgentSight assembles three layers:&lt;/p>
&lt;p>&lt;strong>Plane 1 — SSL/TLS uprobes&lt;/strong>. eBPF can attach programs to functions in &lt;strong>userspace libraries&lt;/strong> (uprobes). The target functions are the encryption ones: &lt;code>SSL_write&lt;/code>, &lt;code>SSL_read&lt;/code> from OpenSSL/BoringSSL, and their equivalents in Rustls. AgentSight puts hooks on them that &lt;strong>capture the arguments&lt;/strong>: the &lt;strong>plaintext&lt;/strong> buffer the app passes to be encrypted, just before TLS processes it. On reception it does the symmetric thing: a hook after &lt;code>SSL_read&lt;/code> with the freshly decrypted plaintext. Result: AgentSight sees the complete content of any HTTPS request the app makes &lt;strong>without needing a man-in-the-middle, certificates or traffic decryption&lt;/strong>. The payload is plaintext because it was captured &lt;strong>before&lt;/strong> being encrypted.&lt;/p>
&lt;p>This works because uprobes are cheap (around 100 ns per invocation) and because apps use common TLS libraries. The few apps that implement their own TLS (rare in production) escape this hook; for those you need a different kprobe or manual instrumentation.&lt;/p>
&lt;p>&lt;strong>Plane 2 — Kernel events&lt;/strong>. In parallel, AgentSight observes relevant syscalls through tracepoints: &lt;code>execve&lt;/code> (which processes start), &lt;code>connect&lt;/code>/&lt;code>accept&lt;/code> (network), &lt;code>read&lt;/code>/&lt;code>write&lt;/code> with file descriptors (filesystem and stdio), &lt;code>unlink&lt;/code>, &lt;code>clone&lt;/code>. Any action by the agent that has an effect outside the process passes through here. This covers, among other things, &lt;strong>shell commands executed by the agent&lt;/strong>: if a Claude Code agent decides to run &lt;code>rm -rf&lt;/code> to &amp;ldquo;clean up the project&amp;rdquo;, the &lt;code>execve&lt;/code> is seen even if the LLM API does not report it.&lt;/p>
&lt;p>&lt;strong>Plane 3 — Correlation engine&lt;/strong>. The two previous planes produce streams of asynchronous events. AgentSight has a userspace component that &lt;strong>correlates them causally cross-process&lt;/strong>: an outgoing HTTP request with &lt;code>bash -c rm -rf&lt;/code> can be correlated with the previous LLM response that suggested it, through PIDs, timings and heuristics. The paper mentions the optional use of &lt;strong>a secondary LLM&lt;/strong> (Anthropic Claude, for instance) that analyses the sequence of events and produces semantic alerts: &amp;ldquo;the agent responded with a tool call that was not in the whitelist&amp;rdquo;, &amp;ldquo;the reasoning chain has gone 47 iterations without converging&amp;rdquo;.&lt;/p>
&lt;h3 id="stdiocap-capturing-the-stdio-of-local-mcp-servers">&lt;code>stdiocap&lt;/code>: capturing the stdio of local MCP servers&lt;/h3>
&lt;p>One specific piece that deserves its own mention is &lt;code>stdiocap&lt;/code>, a separate BPF tool included in the repo. The &lt;strong>Model Context Protocol (MCP)&lt;/strong>, popularised by Anthropic in 2024 and mainstream in 2025-2026, has two transport modes: HTTP/SSE (network) and &lt;strong>stdio&lt;/strong> (between the client and the server it starts as a subprocess). Local MCP servers, the ones that run on the same machine and are started by the client as children over pipes, communicate through stdin/stdout/stderr with JSON-RPC.&lt;/p>
&lt;p>&lt;code>stdiocap&lt;/code> hooks &lt;code>read&lt;/code>/&lt;code>write&lt;/code>/&lt;code>dup&lt;/code> on the stdin/stdout/stderr file descriptors of a target process and &lt;strong>records all the JSON-RPC traffic&lt;/strong> between the MCP client and server. It is the same idea as the SSL capture but for stdio: you observe the conversation without either the client or the server knowing. Typical use case: seeing which tools of the &lt;code>filesystem-mcp&lt;/code> MCP server a Claude Code agent has invoked in the last hour, what arguments it passed, what errors it received. Impossible with classic instrumentation (MCP servers are usually third-party binaries).&lt;/p>
&lt;h3 id="guarantees-tamper-proof-kernel-safety-3-overhead">Guarantees: tamper-proof, kernel safety, &amp;lt;3% overhead&lt;/h3>
&lt;p>Three properties make AgentSight interesting for production:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Tamper-proof&lt;/strong>: the observation happens in the kernel (uprobes, syscalls). A malicious or compromised application cannot falsify what is seen. Compare with instrumentation: if the agent decides not to emit the span for its action, it does not appear in Langfuse. Here it has no choice.&lt;/li>
&lt;li>&lt;strong>Kernel safety&lt;/strong>: eBPF formally verifies that programs terminate and respect bounds checks. It cannot crash the kernel. The same as in the rest of the eBPF series.&lt;/li>
&lt;li>&lt;strong>&amp;lt;3% CPU overhead&lt;/strong> measured on real agent workloads (per the paper). The number compares favourably with SDK instrumentation, which typically adds 5-10% in intensive applications.&lt;/li>
&lt;/ul>
&lt;h3 id="what-it-detects-out-of-the-box">What it detects out of the box&lt;/h3>
&lt;p>The paper and the documentation highlight three classes of detection:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Real-time prompt injection&lt;/strong>: the correlation engine can apply rules or a detection model over the plaintext captured by the SSL uprobes. If the prompt contains suspicious patterns, &amp;ldquo;ignore all previous instructions&amp;rdquo;, a system prompt embedded in a user input, instructions to exfiltrate data, it raises an alert.&lt;/li>
&lt;li>&lt;strong>Reasoning loops that burn resources&lt;/strong>: agents that enter infinite loops calling tools without progressing. Detectable because the causal chain does not converge to a &amp;ldquo;final answer&amp;rdquo; and the tokens pile up. The correlation engine flags them.&lt;/li>
&lt;li>&lt;strong>Bottlenecks in multi-agent setups&lt;/strong>: when several agents coordinate, AgentSight sees the communication matrix between all of them and can detect agents that block while waiting, deadlocks, excessive fan-out.&lt;/li>
&lt;/ul>
&lt;h2 id="the-clash-and-the-coexistence">The clash and the coexistence&lt;/h2>
&lt;p>The two families look like competitors, but in reality they see different things and complement each other in production.&lt;/p>
&lt;h3 id="what-only-the-instrumented-one-sees">What only the instrumented one sees&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Internal agent variables&lt;/strong> that never reach the wire: the intermediate state of a LangChain chain, the values before passing them to a tool, how a prompt is built from a template with internal vars.&lt;/li>
&lt;li>&lt;strong>Deep semantic spans&lt;/strong>: &lt;code>retrieval &amp;gt; embed &amp;gt; vector_search &amp;gt; rerank &amp;gt; format_context &amp;gt; prompt_template &amp;gt; llm&lt;/code>. AgentSight sees only the final call to the LLM; the path taken to build it is invisible.&lt;/li>
&lt;li>&lt;strong>Evaluations&lt;/strong>: scoring of answers, judge LLMs, quality regression. This lives only in instrumented platforms.&lt;/li>
&lt;/ul>
&lt;h3 id="what-only-ebpf-sees">What only eBPF sees&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Opaque binaries&lt;/strong>: Claude Code, Cursor, Gemini CLI, third-party agents. You do not have the code; you cannot instrument them. Only eBPF sees them.&lt;/li>
&lt;li>&lt;strong>System-level actions&lt;/strong>: the agent decides to run &lt;code>git push --force&lt;/code> or &lt;code>kubectl delete&lt;/code>. The action is seen in the &lt;code>execve&lt;/code>. The agent&amp;rsquo;s instrumentation may not report it (especially if it was a command the agent generated as output without going through an explicit &amp;ldquo;tool&amp;rdquo;).&lt;/li>
&lt;li>&lt;strong>Tamper-proof audit&lt;/strong>: for regulatory compliance (HIPAA, SOC2, NIS2), having observation the app cannot dodge has formal value. eBPF provides it.&lt;/li>
&lt;li>&lt;strong>Local MCP servers over stdio&lt;/strong>: invisible to classic instrumentation unless each server emits its own spans (rare).&lt;/li>
&lt;/ul>
&lt;h3 id="what-both-see-complementarily">What both see, complementarily&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Prompts and completions&lt;/strong>: the instrumented approach emits them with rich metadata; eBPF captures them off the wire. A perfect cross-check for spotting discrepancies.&lt;/li>
&lt;li>&lt;strong>Calls to external APIs&lt;/strong>: APM flags it; eBPF confirms it at kernel level.&lt;/li>
&lt;li>&lt;strong>Latency&lt;/strong>: APM per span; eBPF measures RTT at TCP level and network connectivity.&lt;/li>
&lt;/ul>
&lt;h3 id="decision-matrix">Decision matrix&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Case&lt;/th>
&lt;th>Instrumented&lt;/th>
&lt;th>eBPF (AgentSight)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Your own app with LangChain&lt;/td>
&lt;td>&lt;strong>Yes, first&lt;/strong>&lt;/td>
&lt;td>Optional&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Your own multi-framework app&lt;/td>
&lt;td>&lt;strong>Yes&lt;/strong>&lt;/td>
&lt;td>Optional&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Third-party binary (Claude Code, Cursor)&lt;/td>
&lt;td>&lt;strong>Does not work&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Yes, the only path&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tamper-proof regulatory compliance&lt;/td>
&lt;td>Insufficient&lt;/td>
&lt;td>&lt;strong>Yes, required&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Zero-trust multi-tenant&lt;/td>
&lt;td>Insufficient&lt;/td>
&lt;td>&lt;strong>Yes, required&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Local MCP servers (stdio)&lt;/td>
&lt;td>Difficult&lt;/td>
&lt;td>&lt;strong>Yes, with stdiocap&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Answer quality evaluation&lt;/td>
&lt;td>&lt;strong>Yes, required&lt;/strong>&lt;/td>
&lt;td>No (out of scope)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Depth of the internal chain&lt;/td>
&lt;td>&lt;strong>Yes, required&lt;/strong>&lt;/td>
&lt;td>No (a black box for AgentSight)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reasoning loop detection&lt;/td>
&lt;td>Possible with plumbing&lt;/td>
&lt;td>&lt;strong>Yes, built in&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Real-time prompt injection&lt;/td>
&lt;td>Possible (post-processed)&lt;/td>
&lt;td>&lt;strong>Yes, in stream&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The natural conclusion: &lt;strong>for your own apps, instrumented; for opaque binaries or compliance, eBPF; for everything that matters, both&lt;/strong>.&lt;/p>
&lt;h2 id="2026-reference-architecture">2026 reference architecture&lt;/h2>
&lt;p>Four recipes that cover the bulk of real cases:&lt;/p>
&lt;h3 id="setup-a--your-own-application-with-langchain-or-similar">Setup A — Your own application with LangChain or similar&lt;/h3>
&lt;p>Needs: depth, evals, a team comfortable with SDKs.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Langfuse self-host&lt;/strong> or &lt;strong>LangSmith cloud&lt;/strong> as the backend.&lt;/li>
&lt;li>&lt;strong>OpenLLMetry SDK&lt;/strong> or &lt;strong>LangSmith SDK&lt;/strong> instrumenting the code.&lt;/li>
&lt;li>&lt;strong>OpenTelemetry Collector&lt;/strong> between the app and the backend for routing flexibility (to Langfuse + Tempo + Loki, for instance).&lt;/li>
&lt;li>&lt;strong>Hubble&lt;/strong> for the network layer in the cluster (inter-pod latency, drop attribution).&lt;/li>
&lt;/ul>
&lt;h3 id="setup-b--productionising-an-opaque-binary-claude-code-gemini-cli">Setup B — Productionising an opaque binary (Claude Code, Gemini CLI)&lt;/h3>
&lt;p>Needs: observe without touching, audit, control cost.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AgentSight&lt;/strong> deployed as a DaemonSet over the cluster (or standalone on the node).&lt;/li>
&lt;li>&lt;strong>Grafana with dashboards&lt;/strong> fed by AgentSight&amp;rsquo;s metrics.&lt;/li>
&lt;li>&lt;strong>An OTLP exporter&lt;/strong> from AgentSight to an OTel backend (Tempo, Jaeger). The spans will use the GenAI semantic conventions once they are fully standardised.&lt;/li>
&lt;li>&lt;strong>Tetragon&lt;/strong> optionally for policy over what the agent is allowed to run (Sigkill if it tries &lt;code>rm -rf&lt;/code> or similar).&lt;/li>
&lt;/ul>
&lt;h3 id="setup-c--zero-trust-multi-tenant-platform">Setup C — Zero-trust multi-tenant platform&lt;/h3>
&lt;p>Needs: agents from different clients running on the same cluster, mandatory audit, nobody trusts anybody.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AgentSight&lt;/strong> as the tamper-proof audit layer. Compliance requires it.&lt;/li>
&lt;li>&lt;strong>Langfuse multi-tenant&lt;/strong> for the clients that do instrument.&lt;/li>
&lt;li>&lt;strong>Tetragon&lt;/strong> with &lt;code>TracingPolicyNamespaced&lt;/code> per tenant (different policies per namespace).&lt;/li>
&lt;li>&lt;strong>Hubble&lt;/strong> with persistent flow logs for forensics.&lt;/li>
&lt;li>&lt;strong>Cilium NetworkPolicy&lt;/strong> to isolate tenants from each other on the network.&lt;/li>
&lt;/ul>
&lt;h3 id="setup-d--a-local-mcp-server-on-a-workstation">Setup D — A local MCP server on a workstation&lt;/h3>
&lt;p>Needs: seeing what an agent does with a stdio MCP server.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AgentSight stdiocap&lt;/strong> pointed at the PID of the client or the server.&lt;/li>
&lt;li>Complete JSON-RPC capture to a file or to an OTLP endpoint.&lt;/li>
&lt;li>Visualisation: Grafana, or simply &lt;code>jq&lt;/code> over the log.&lt;/li>
&lt;/ul>
&lt;p>A real use case: if you are integrating an MCP server of your own and want to see what tool calls a Claude Code or Cursor agent makes to your server, &lt;code>stdiocap&lt;/code> is the cleanest way. You need to modify neither client nor server.&lt;/p>
&lt;h2 id="operational-traps">Operational traps&lt;/h2>
&lt;h3 id="sensitive-data-in-prompts-instrumented">Sensitive data in prompts (instrumented)&lt;/h3>
&lt;p>By default, Langfuse, LangSmith and similar tools capture &lt;strong>the complete content&lt;/strong> of prompts and completions. If your app processes PII, secrets or medical data, &lt;strong>that goes to your observability backend&lt;/strong>. Configuring &lt;strong>redaction&lt;/strong> or &lt;strong>content opt-out&lt;/strong> before going to production is mandatory. OTel GenAI has specific flags (&lt;code>OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=false&lt;/code>) to avoid it.&lt;/p>
&lt;h3 id="sensitive-data-in-prompts-agentsight">Sensitive data in prompts (AgentSight)&lt;/h3>
&lt;p>The same problem, worse: AgentSight captures &lt;strong>literally what goes to the wire&lt;/strong>, in plaintext. If the agent talked to &lt;code>api.openai.com&lt;/code> with a prompt containing sensitive data, AgentSight has that plaintext. You have to encrypt or redact before storing.&lt;/p>
&lt;h3 id="pinned-certificates-or-non-standard-tls">Pinned certificates or non-standard TLS&lt;/h3>
&lt;p>Some high-security apps do certificate pinning or use unconventional TLS implementations (Go&amp;rsquo;s &lt;code>crypto/tls&lt;/code>, custom BoringSSL). In those cases, the uprobes on &lt;code>libssl&lt;/code> do not cover them. AgentSight detects when it cannot observe and reports the gap; you still have to add specific hooks for the alternative SDK.&lt;/p>
&lt;h3 id="token-volume-and-storage">Token volume and storage&lt;/h3>
&lt;p>An application with medium traffic can generate &lt;strong>millions of tokens a day&lt;/strong>. If you store them all in Langfuse or Phoenix with long retention, the database grows fast. Strategies: aggressive sampling, short retention for normal sessions and long retention only for errors/anomalies, content redaction and keeping only metadata.&lt;/p>
&lt;h3 id="tracing-with-sampling-and-consistency">Tracing with sampling and consistency&lt;/h3>
&lt;p>To cut cost, many installations sample: only 1 in every N traces is persisted. &lt;strong>Be careful with inconsistent sampling&lt;/strong>: a trace can carry several spans across multiple services, and if the sampling decision is taken per span, you end up with incomplete traces. OTel has &lt;strong>head sampling&lt;/strong> (in the SDK, at the start), which is consistent, and &lt;strong>tail sampling&lt;/strong> (in the collector, at the end), which allows finer rules. For LLM, tail sampling is ideal: sample everything, discard only the &amp;ldquo;normal&amp;rdquo; traces and keep the ones with errors, high latency or high cost.&lt;/p>
&lt;h3 id="multi-agent-and-trace-propagation">Multi-agent and trace propagation&lt;/h3>
&lt;p>When agent A calls agent B, you have to &lt;strong>propagate the trace context&lt;/strong> (W3C Trace Context headers) so it shows up as a single tree. If you do not, you see two disconnected traces. Modern platforms do it automatically with &lt;code>inject&lt;/code>/&lt;code>extract&lt;/code>, but if your transport between agents is custom (over Redis pub/sub, over a DB), you have to propagate by hand.&lt;/p>
&lt;h3 id="the-cost-of-uprobes-on-critical-libraries">The cost of uprobes on critical libraries&lt;/h3>
&lt;p>Hooking &lt;code>libssl&lt;/code> adds around 100 ns per invocation. On extreme TLS traffic loads (tens of thousands of connections/s per core), that adds up. AgentSight keeps it under 3% on typical agent workloads (which are chatty but not networking-intensive). If your use were sniffing all the HTTPS on the node, it could hurt more.&lt;/p>
&lt;h2 id="what-we-have-not-covered-next-series">What we have not covered (next series)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Evals&lt;/strong>: the next layer after tracing. Phoenix, Langfuse, LangSmith and company offer answer evaluation (judge LLM, datasets, regression). It is a world of its own.&lt;/li>
&lt;li>&lt;strong>Guardrails and safety&lt;/strong>: NeMo Guardrails, Llama Guard, Llama Prompt Guard, evaluators specific to prompt injection and jailbreaks.&lt;/li>
&lt;li>&lt;strong>Deep MCP server observability&lt;/strong>: how the OpenTelemetry GenAI conventions are being extended to MCP servers for trace-aware tools.&lt;/li>
&lt;li>&lt;strong>eBPF + on-device inference&lt;/strong>: when the LLM runs locally through vLLM or llama.cpp, the uprobes can see the output token queue BEFORE it goes to the client. New territory.&lt;/li>
&lt;li>&lt;strong>Statistical analysis of agent flows&lt;/strong>: detecting drift, outliers, patterns that indicate degradation.&lt;/li>
&lt;/ul>
&lt;h2 id="closing-the-ebpf-series">Closing the eBPF series&lt;/h2>
&lt;p>This series of four articles has travelled eBPF from first principles to the 2026 frontier:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a> — what eBPF is, networking hooks, how Cilium skips the TCP/IP stack, BGP Control Plane v2.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon: runtime security&lt;/a> — observability and enforcement of processes in the kernel.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble: network observability&lt;/a> — L3-L7 flow logs and the frontier with AI agents.&lt;/li>
&lt;li>&lt;strong>This one&lt;/strong> — AgentSight, LLM tracing, instrumented vs zero-instrumentation.&lt;/li>
&lt;/ol>
&lt;p>If you have got this far you have the map to sit down with a platform, security or AI team in 2026 and recognise what each piece does, what problem it solves and where to start. That whole stack, Cilium for CNI and BGP, Tetragon for runtime security, Hubble for network observability, AgentSight for AI agents, sharing eBPF as a common substrate, Cloud Native governance and OpenTelemetry vocabulary. It is the clean architecture the industry asked for a decade ago and that finally exists.&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;p>AgentSight:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/eunomia-bpf/agentsight">AgentSight GitHub (eunomia-bpf)&lt;/a> — the project.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2508.02736">AgentSight: System-Level Observability for AI Agents Using eBPF (arxiv 2508.02736)&lt;/a> — the formal paper.&lt;/li>
&lt;li>&lt;a href="https://dl.acm.org/doi/10.1145/3766882.3767169">AgentSight ACM workshop publication&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://eunomia.dev/blog/2025/08/26/agentsight-keeping-your-ai-agents-under-control-with-ebpf-powered-system-observability/">AgentSight blog post (eunomia.dev)&lt;/a> — accessible description.&lt;/li>
&lt;/ul>
&lt;p>OpenTelemetry GenAI semantic conventions:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">OpenTelemetry — Semantic conventions for generative AI systems&lt;/a> — official reference.&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/">Semantic conventions for generative client AI spans&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/">Semantic conventions for generative AI metrics&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://opentelemetry.io/blog/2026/genai-observability/">Inside the LLM Call: GenAI Observability with OpenTelemetry (OTel blog 2026)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/open-telemetry/semantic-conventions/issues/2664">Multi-agent Semantic Conventions (GitHub issue #2664)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Instrumented platforms:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://langfuse.com/">Langfuse&lt;/a> — MIT, self-host + cloud.&lt;/li>
&lt;li>&lt;a href="https://www.langchain.com/langsmith">LangSmith&lt;/a> — LangChain team.&lt;/li>
&lt;li>&lt;a href="https://phoenix.arize.com/">Arize Phoenix&lt;/a> — OSS, OTel-native.&lt;/li>
&lt;li>&lt;a href="https://www.helicone.ai/">Helicone&lt;/a> — simple proxy.&lt;/li>
&lt;li>&lt;a href="https://github.com/traceloop/openllmetry">OpenLLMetry (Traceloop)&lt;/a> — Apache 2.0, OTel SDK.&lt;/li>
&lt;li>&lt;a href="https://pydantic.dev/docs/logfire/get-started/ai-observability/">Pydantic Logfire — AI observability&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>2026 comparisons:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.braintrust.dev/articles/langfuse-alternatives-2026">Langfuse alternatives 2026 (Braintrust)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.braintrust.dev/articles/best-llm-tracing-tools-2026">7 best LLM tracing tools for multi-agent AI systems (2026)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://medium.com/@kanerika/llmops-observability-langsmith-vs-arize-vs-langfuse-vs-w-b-f1baeabd1bbf">LLMOps Observability: LangSmith vs Arize vs Langfuse vs W&amp;amp;B&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.firecrawl.dev/blog/best-llm-observability-tools">Best LLM Observability Tools in 2026 (Firecrawl)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/llm-observability-gpu-cloud-langfuse-arize-phoenix-helicone/">LLM Observability on GPU Cloud (Spheron 2026 guide)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references from the series:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon: runtime security&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/">Hubble: network observability&lt;/a>.&lt;/li>
&lt;li>LLM inference series: &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM Operators on K8s&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>Hubble: eBPF network observability, state of the art 2026 and the new frontier with AI agents</title><link>https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/</link><pubDate>Tue, 19 May 2026 06:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/hubble-ebpf-network-observability-ai-agents/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>&lt;a href="https://github.com/cilium/hubble">Hubble&lt;/a> is &lt;strong>Cilium&amp;rsquo;s native network observability&lt;/strong>, built on the same eBPF programs Cilium uses for enforcement. It does not duplicate the datapath or instrument the kernel its own way: it &lt;strong>listens&lt;/strong> to the hooks Cilium already has and produces structured flow logs with Kubernetes context included, covering pod, namespace, labels, service, policy verdict and L7 payload where applicable. It is what happens when someone decides that &lt;code>tcpdump&lt;/code> with &lt;code>grep&lt;/code> does not scale to 10,000 pods and builds a distributed system of their own (a Hubble server per node, plus Hubble Relay as the aggregator, plus a CLI and a UI) with practically zero overhead, because the capture was already happening. In 2026 it is at version 1.19.3 (April 2026), with Cilium 1.19 marking the project&amp;rsquo;s tenth anniversary; IP options tracing has arrived, along with filtering by encryption status, the drop event tagged with the exact NetworkPolicy that caused it (direct attribution), a stabilised field mask API, and the first wave of ML anomaly detection applied to flows for predictive security in IoT/5G clusters. And, most interesting of all for 2026: a new frontier appears where the same eBPF observes AI agents such as Claude Code, Gemini CLI and MCP agents, intercepting SSL/TLS and stdio without instrumenting the code, which turns the Cilium + Hubble + Tetragon + AgentSight stack into a complete set of tools for understanding what an agentic system does inside a cluster.&lt;/p>
&lt;blockquote>
&lt;p>This article is part 3 of the eBPF series. Part 1: &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium: how the kernel learned to skip its own TCP/IP stack&lt;/a>. Part 2: &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon: Cilium&amp;rsquo;s security cousin that sees every syscall in the kernel&lt;/a>. Here we complete the observability quadrant: network with Hubble, process with Tetragon, AI agent with AgentSight.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-analogy-tcpdump-that-speaks-kubernetes">The analogy: tcpdump that speaks Kubernetes&lt;/h2>
&lt;p>If you have administered networks over the last twenty years, &lt;code>tcpdump&lt;/code> and Wireshark have been your daily bread. They capture packets on an interface, parse them, and let you filter with &lt;code>tcp.port == 443 and host 10.0.0.5&lt;/code>. They work, they have been working since the 90s, and they are the first thing you open when something smells wrong.&lt;/p>
&lt;p>Now stick &lt;code>tcpdump&lt;/code> on a Kubernetes cluster with 10,000 pods. The problems show up in order:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>One &lt;code>tcpdump&lt;/code> session per node&lt;/strong>. You wanted to &amp;ldquo;see the traffic between the frontend and the API&amp;rdquo;; you need SSH to every node, tcpdump on every NIC, synchronised timestamps, manual aggregation.&lt;/li>
&lt;li>&lt;strong>There is no K8s context&lt;/strong>. You see a packet from &lt;code>10.244.5.7&lt;/code> to &lt;code>10.244.8.42&lt;/code>. Which pod was that? Which namespace? Which label? You have to correlate with &lt;code>kubectl get pod -A -o wide&lt;/code> every single time.&lt;/li>
&lt;li>&lt;strong>No L7 understanding&lt;/strong>. You see a POST to HTTPS, and you cannot know the method and path because it is encrypted on the wire. If there is mTLS between pods, worse.&lt;/li>
&lt;li>&lt;strong>High cost&lt;/strong>: full packet capture with a copy to userspace slows down the datapath. Under dense traffic, you notice it.&lt;/li>
&lt;/ol>
&lt;p>Hubble is &lt;strong>tcpdump redesigned for all of that&lt;/strong>. It reuses the eBPF programs that are already processing every packet (Cilium puts them there for enforcement) and, while they take their allow/deny decision, they emit a flow event with all the context: source and destination pod identity, namespace, labels, protocol, verdict, and, if Cilium has done L7 parsing via Envoy, the HTTP method, path, status code, DNS query, Kafka topic. That event travels through a ringbuffer to userspace, it is received by the Hubble server that lives inside the node&amp;rsquo;s Cilium agent, and it is exposed over gRPC. A separate service, Hubble Relay, aggregates the streams from all nodes and gives you a single cluster-wide API. On top of that: a CLI (&lt;code>hubble&lt;/code>) and a web UI with a real-time service graph.&lt;/p>
&lt;p>Zero additional copies. Zero duplicated parsing. And the result is understandable by anyone who knows what a Pod is.&lt;/p>
&lt;h2 id="architecture-four-pieces-visible-from-outside">Architecture: four pieces visible from outside&lt;/h2>
&lt;p>Hubble is made up of four logical components, all optional depending on what you want to do:&lt;/p>
&lt;h3 id="1-hubble-server-embedded-in-every-cilium-agent">1. Hubble Server (embedded in every Cilium agent)&lt;/h3>
&lt;p>It lives inside the Cilium agent process (it is not a separate binary). Each node exposes a gRPC endpoint locally on the Unix socket &lt;code>/var/run/cilium/hubble.sock&lt;/code>. The server listens to the events the eBPF programs emit to the ringbuffer, enriches them with Kubernetes metadata (which the agent already has in memory), and makes them available to consumers.&lt;/p>
&lt;p>Enabling it: &lt;code>--set hubble.enabled=true&lt;/code> in Cilium&amp;rsquo;s Helm chart. By default, the server is only accessible locally; if you want to consume it from another node, it has to be exposed (which is what Hubble Relay does).&lt;/p>
&lt;h3 id="2-hubble-relay-aggregator">2. Hubble Relay (aggregator)&lt;/h3>
&lt;p>It is a separate Deployment (typically 1 replica, scalable) that connects to every Hubble server in the cluster and aggregates their streams into a single API. When your CLI or UI asks for &amp;ldquo;the last 1000 flows in the cluster&amp;rdquo;, the Relay collects them in parallel from all nodes and returns the union.&lt;/p>
&lt;p>Enabling it: &lt;code>--set hubble.relay.enabled=true&lt;/code>. Without the Relay you only see the traffic of the node you are connected to, which is useful for local debugging but not for a cluster-wide view.&lt;/p>
&lt;h3 id="3-hubble-cli-hubble">3. Hubble CLI (&lt;code>hubble&lt;/code>)&lt;/h3>
&lt;p>A Go binary that speaks gRPC with the Relay (or with a local Hubble server). It supports two main modes:&lt;/p>
&lt;ul>
&lt;li>&lt;code>hubble observe&lt;/code>: a real-time stream of flows, with very expressive filters (by namespace, pod, port, verdict, protocol, label).&lt;/li>
&lt;li>&lt;code>hubble status&lt;/code>: the state of the Hubble cluster (how many nodes connected, lag, flow rate).&lt;/li>
&lt;/ul>
&lt;p>And the equivalent of &lt;code>tcpdump&lt;/code>&amp;rsquo;s pcap dump: &lt;code>hubble observe --output jsonpb &amp;gt; flows.json&lt;/code> to process afterwards with &lt;code>jq&lt;/code> or other tools.&lt;/p>
&lt;h3 id="4-hubble-ui">4. Hubble UI&lt;/h3>
&lt;p>A web frontend that connects to Hubble Relay and shows:&lt;/p>
&lt;ul>
&lt;li>A service graph in real time (which Pod talks to which Service, which protocols it uses, which verdict).&lt;/li>
&lt;li>A filterable list of flows.&lt;/li>
&lt;li>&lt;strong>L7 details&lt;/strong> where there are any (HTTP method/path/status, DNS query/response).&lt;/li>
&lt;/ul>
&lt;p>Enabling it: &lt;code>--set hubble.ui.enabled=true&lt;/code>. Useful for presentations to non-CLI teams; it does not replace the CLI for serious debugging.&lt;/p>
&lt;div class="diagram" style="max-width:720px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 720 290" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Hubble architecture">
&lt;style>.title{font:600 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.box{stroke:#444;stroke-width:1.4}.k{fill:#ffe9d6}.s{fill:#d6eaff}.r{fill:#d9f5d6}.c{fill:#e9d6f5}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#hh)}&lt;/style>
&lt;defs>&lt;marker id="hh" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="360" y="18" text-anchor="middle" class="title">Hubble architecture: 4 pieces, eBPF as the only data source&lt;/text>
&lt;rect x="30" y="40" width="200" height="50" rx="6" class="box k"/>
&lt;text x="130" y="60" text-anchor="middle" class="lbl">eBPF (kernel)&lt;/text>
&lt;text x="130" y="78" text-anchor="middle" class="sm">Cilium programs&lt;/text>
&lt;rect x="30" y="115" width="200" height="50" rx="6" class="box s"/>
&lt;text x="130" y="135" text-anchor="middle" class="lbl">Hubble Server (node)&lt;/text>
&lt;text x="130" y="153" text-anchor="middle" class="sm">local grpc, inside the agent&lt;/text>
&lt;rect x="270" y="115" width="180" height="50" rx="6" class="box s"/>
&lt;text x="360" y="135" text-anchor="middle" class="lbl">Hubble Server (node N)&lt;/text>
&lt;text x="360" y="153" text-anchor="middle" class="sm">one per node&lt;/text>
&lt;rect x="490" y="115" width="200" height="50" rx="6" class="box s"/>
&lt;text x="590" y="135" text-anchor="middle" class="lbl">Hubble Server (node …)&lt;/text>
&lt;text x="590" y="153" text-anchor="middle" class="sm">N agents = N servers&lt;/text>
&lt;rect x="220" y="190" width="280" height="50" rx="6" class="box r"/>
&lt;text x="360" y="210" text-anchor="middle" class="lbl">Hubble Relay (Deployment)&lt;/text>
&lt;text x="360" y="228" text-anchor="middle" class="sm">aggregates gRPC streams from all nodes&lt;/text>
&lt;rect x="80" y="245" width="160" height="35" rx="6" class="box c"/>
&lt;text x="160" y="266" text-anchor="middle" class="lbl">Hubble CLI&lt;/text>
&lt;rect x="290" y="245" width="160" height="35" rx="6" class="box c"/>
&lt;text x="370" y="266" text-anchor="middle" class="lbl">Hubble UI&lt;/text>
&lt;rect x="500" y="245" width="180" height="35" rx="6" class="box c"/>
&lt;text x="590" y="266" text-anchor="middle" class="lbl">Prometheus / OTLP&lt;/text>
&lt;path class="arr" d="M130,90 L130,115"/>
&lt;path class="arr" d="M130,165 L290,190"/>
&lt;path class="arr" d="M360,165 L360,190"/>
&lt;path class="arr" d="M590,165 L430,190"/>
&lt;path class="arr" d="M340,240 L200,245"/>
&lt;path class="arr" d="M360,240 L370,245"/>
&lt;path class="arr" d="M380,240 L560,245"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="what-you-see-the-hubble-flow-log-from-the-inside">What you see: the Hubble flow log from the inside&lt;/h2>
&lt;p>A Hubble flow in JSON format looks roughly like this (simplified):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;time&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-05-19T03:12:45.182Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;verdict&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;FORWARDED&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;source&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5482&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;identity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">24871&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;prod-api&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;labels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;app=checkout&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;team=payments&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pod_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;checkout-7c9f-x8j2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;workloads&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;checkout&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;kind&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Deployment&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;destination&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">12041&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;identity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">18356&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;namespace&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;prod-db&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;labels&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;app=postgres&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;tier=primary&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pod_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;postgres-0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;L3_L4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;l4&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;TCP&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;source_port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">41982&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;destination_port&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">5432&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;flags&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;SYN&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;node_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;rke2-worker-03&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;Summary&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;TCP Flags: SYN&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When L7 parsing is active (via embedded Envoy or Hubble&amp;rsquo;s lightweight parser), the same flow adds:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="s2">&amp;#34;l7&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;REQUEST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;http&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;code&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;url&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/api/v1/cart/items&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;protocol&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;HTTP/1.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;headers&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[{&lt;/span>&lt;span class="nt">&amp;#34;key&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user-agent&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;checkout/1.4.2&amp;#34;&lt;/span>&lt;span class="p">}]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The protocols natively supported for L7 parsing:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>HTTP/1.1 and HTTP/2&lt;/strong> (including gRPC over HTTP/2).&lt;/li>
&lt;li>&lt;strong>DNS&lt;/strong> (queries and responses, with domains, types, response codes).&lt;/li>
&lt;li>&lt;strong>Kafka&lt;/strong> (topics, API keys).&lt;/li>
&lt;li>&lt;strong>TLS handshake&lt;/strong> (SNI, not the encrypted payload by default).&lt;/li>
&lt;li>&lt;strong>MySQL, Cassandra&lt;/strong> (with optional modules).&lt;/li>
&lt;/ul>
&lt;p>For HTTP and gRPC, Cilium can enable the embedded Envoy proxy for the flows you want to inspect (not all of them; it is selective via &lt;code>CiliumNetworkPolicy&lt;/code> with L7 rules). Without Envoy there is lightweight parsing, but less detailed.&lt;/p>
&lt;h2 id="verdict-and-drop-attribution">Verdict and drop attribution&lt;/h2>
&lt;p>Every flow has a &lt;code>verdict&lt;/code>: &lt;code>FORWARDED&lt;/code>, &lt;code>DROPPED&lt;/code>, &lt;code>ERROR&lt;/code>, &lt;code>AUDIT&lt;/code>, &lt;code>REDIRECTED&lt;/code>, &lt;code>TRACED&lt;/code>, &lt;code>TRANSLATED&lt;/code>. For the &lt;code>DROPPED&lt;/code> case, Hubble includes a structured reason (&lt;code>drop_reason&lt;/code>) and, since Cilium 1.19, &lt;strong>the exact NetworkPolicy&lt;/strong> that caused it.&lt;/p>
&lt;p>That last point changes day-to-day operations. Before, when a pod could not talk to another one, the debug flow was:&lt;/p>
&lt;ol>
&lt;li>See the dropped flow in Hubble.&lt;/li>
&lt;li>Look at every CiliumNetworkPolicy in the namespace.&lt;/li>
&lt;li>Reason manually about which one, with which labels, is blocking it.&lt;/li>
&lt;/ol>
&lt;p>With Cilium 1.19&amp;rsquo;s attribution, the &lt;code>policy_match_info&lt;/code> field tells you directly &amp;ldquo;it was dropped by the &lt;code>frontend-egress&lt;/code> policy, rule 3&amp;rdquo;. You go from &amp;ldquo;Sherlock Holmes for 20 minutes&amp;rdquo; to &amp;ldquo;kubectl get -o yaml of that specific policy&amp;rdquo;.&lt;/p>
&lt;h2 id="prometheus-metrics-and-grafana-dashboards">Prometheus metrics and Grafana dashboards&lt;/h2>
&lt;p>Hubble also exposes aggregated metrics in Prometheus format, separate from the gRPC flow stream. Enabling it: &lt;code>--set hubble.metrics.enabled=true&lt;/code> (Helm) plus a list of the set you want to export.&lt;/p>
&lt;p>The usual metric groups:&lt;/p>
&lt;ul>
&lt;li>&lt;code>flow&lt;/code>: total flows by verdict, source/dest, protocol.&lt;/li>
&lt;li>&lt;code>http&lt;/code>: requests by method, response code, latency (histogram).&lt;/li>
&lt;li>&lt;code>dns&lt;/code>: queries, response codes, top-N domains.&lt;/li>
&lt;li>&lt;code>tcp&lt;/code>: handshakes, retransmissions, congestion window.&lt;/li>
&lt;li>&lt;code>drop&lt;/code>: drops by reason, with NetworkPolicy attribution.&lt;/li>
&lt;li>&lt;code>port-distribution&lt;/code>: histogram of active ports.&lt;/li>
&lt;li>&lt;code>policy&lt;/code>: hits by policy and verdict.&lt;/li>
&lt;/ul>
&lt;p>These metrics have rich K8s labels (&lt;code>source_workload&lt;/code>, &lt;code>destination_workload&lt;/code>, &lt;code>namespace&lt;/code>, and so on) that make them pivotable in Grafana. There are &lt;a href="https://grafana.com/grafana/dashboards/?search=hubble">prebuilt dashboards on Grafana Labs&lt;/a> covering the common cases; importing one and having immediate visibility costs five minutes.&lt;/p>
&lt;p>Cost: metrics with many K8s labels can &lt;strong>blow up cardinality&lt;/strong> in Prometheus. For large clusters (&amp;gt;1,000 pods), it is worth reviewing which set you export and using drop rules in Prometheus to keep a lid on it.&lt;/p>
&lt;h2 id="deployment-helm-on-one-screen">Deployment: Helm on one screen&lt;/h2>
&lt;p>The canonical Cilium install with full Hubble:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">hubble&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">dns:query;ignoreAAAA&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">drop&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">tcp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">flow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">port-distribution&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">icmp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">httpV2:exemplars=true;labelsContext=source_ip,source_namespace,source_workload,destination_ip,destination_namespace,destination_workload,traffic_direction&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serviceMonitor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># auto-discovered by kube-prometheus-stack&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">relay&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rollOutPods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ui&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rollOutPods&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ingress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">className&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">cilium&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hosts&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">hubble.example.local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the install itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm upgrade --install cilium cilium/cilium -n kube-system -f values.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After installing, validate with &lt;code>cilium status&lt;/code> (the Cilium CLI) that the Hubble section shows OK, and try your first flow with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">cilium hubble observe --namespace prod-api --pod checkout-7c9f-x8j2
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="state-of-the-art-in-2026">State of the art in 2026&lt;/h2>
&lt;p>&lt;a href="https://www.infoq.com/news/2026/02/cilium-119/">Cilium 1.19 was released in February 2026&lt;/a>, marking the project&amp;rsquo;s tenth anniversary. Hubble reached version 1.19.3 on 22 April 2026. The relevant news:&lt;/p>
&lt;h3 id="direct-attribution-of-drops-to-a-networkpolicy">Direct attribution of drops to a NetworkPolicy&lt;/h3>
&lt;p>Already covered above; it is probably the most valuable operational change in the release. Any dropped flow carries the name, namespace and specific rule of the policy responsible. It is also available via Prometheus metrics, which allows alerts of the form &amp;ldquo;policy X is dropping &amp;gt;N requests/second&amp;rdquo;.&lt;/p>
&lt;h3 id="tracing-with-ip-options">Tracing with IP options&lt;/h3>
&lt;p>Hubble can now trace individual packets with IP options enabled. It is a mechanism similar to traceroute, but at L3: you put a mark on the packet and Cilium reports it every time the packet crosses a node or an eBPF decision. Useful for debugging multi-cluster paths, fabric mesh, or NetworkPolicies applied at different layers.&lt;/p>
&lt;h3 id="filtering-by-encryption-status">Filtering by encryption status&lt;/h3>
&lt;p>A new CLI flag: &lt;code>hubble observe --encryption-status=encrypted&lt;/code> (or &lt;code>unencrypted&lt;/code>). Useful for validating deployments with WireGuard or IPsec enabled pod-to-pod: you confirm that the traffic that &lt;strong>should&lt;/strong> be encrypted is, and you spot regressions quickly.&lt;/p>
&lt;h3 id="hubble-field-mask-api-stabilised">Hubble field mask API stabilised&lt;/h3>
&lt;p>The &lt;code>field_mask&lt;/code> lets you ask for only the parts of the flow you care about, hugely reducing bandwidth and processing when all you need is, say, source/dest and verdict. It used to be experimental; now it is stable and it is default-on in the CLI.&lt;/p>
&lt;h3 id="ai-driven-anomaly-detection-predictive-security">AI-driven anomaly detection (predictive security)&lt;/h3>
&lt;p>This is the most talked-about addition of 2026. Cilium 1.19 adds hooks so that an external consumer, typically an ML system, can process flows in streaming and detect statistical anomalies: pods that suddenly talk to new destinations, latency spikes on an API, odd DNS sequences. The detection part happens outside the Cilium agent (nobody wants heavy ML in the datapath), but Cilium exposes the flows with the pre-computed features the model needs. The published use cases focus on IoT and 5G, where traffic is high in volume and low in variety, ideal conditions for anomaly detection.&lt;/p>
&lt;h3 id="scaling-to-10000-pods">Scaling to 10,000+ pods&lt;/h3>
&lt;p>Cilium 1.19 has done serious work on scalability: Hubble Relay can now aggregate streams from hundreds of nodes without saturating; the default field_mask reduces inter-node bandwidth; and flows can be sampled under high load if your use is statistical analysis rather than forensic debugging.&lt;/p>
&lt;h3 id="cilium-120-in-development">Cilium 1.20 in development&lt;/h3>
&lt;p>&lt;a href="https://docs.cilium.io/en/latest/operations/upgrade/">Cilium 1.20&lt;/a> is in the development branch. The most relevant bits for Hubble:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Unification of &lt;code>preferIpv6&lt;/code>&lt;/strong>: the &lt;code>hubble.preferIpv6&lt;/code> flag was deprecated in favour of the global &lt;code>preferIpv6&lt;/code> that applies to every Cilium component.&lt;/li>
&lt;li>&lt;strong>&lt;code>tetragon-python&lt;/code> SDK&lt;/strong>: although it belongs to Tetragon, not Hubble, it sets a trend, namely eBPF policies written in Python instead of YAML. Hubble will probably follow a similar path.&lt;/li>
&lt;/ul>
&lt;h2 id="the-new-frontier-ebpf-and-ai-agents">The new frontier: eBPF and AI agents&lt;/h2>
&lt;p>So far, the classic Hubble content. But there is a 2026 twist worth covering, because it closes the loop with the other series on this blog.&lt;/p>
&lt;p>When a Kubernetes cluster starts running AI agents, such as Claude Code, Gemini CLI, or LangGraph-based agents calling APIs and MCP servers, the observability problem changes shape. It is no longer enough to know &amp;ldquo;which pod talked to which pod&amp;rdquo; (that is Hubble) or &amp;ldquo;which process ran what&amp;rdquo; (that is Tetragon). You need to know:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Which external APIs the agent is calling&lt;/strong> and with which prompts.&lt;/li>
&lt;li>&lt;strong>Which MCP tools it is invoking&lt;/strong>, with which arguments.&lt;/li>
&lt;li>&lt;strong>How many tokens it consumes&lt;/strong>, which model it picks, how much it costs.&lt;/li>
&lt;li>&lt;strong>Whether the agent drifts&lt;/strong> from expected behaviour (out-of-policy queries, jailbreak attempts, secret leakage).&lt;/li>
&lt;/ul>
&lt;p>The traditional solutions, instrumenting the agent&amp;rsquo;s code with OpenTelemetry or parsing structured logs, do not work well when the agent is a third-party binary (Anthropic&amp;rsquo;s Claude Code, Google&amp;rsquo;s Gemini CLI) or when the MCP servers live in other languages with stdio as their transport.&lt;/p>
&lt;h3 id="agentsight-zero-instrumentation-for-llm-agents">AgentSight: zero-instrumentation for LLM agents&lt;/h3>
&lt;p>&lt;a href="https://github.com/eunomia-bpf/agentsight">AgentSight&lt;/a> (a project from the &lt;code>eunomia-bpf&lt;/code> group, the same ecosystem behind several high-profile eBPF runtimes) attacks this problem with the same philosophy as Hubble: do not instrument; listen. It puts eBPF hooks at two critical points:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>uprobes on SSL/TLS libraries&lt;/strong> (&lt;code>libssl&lt;/code>, &lt;code>boringssl&lt;/code>, &lt;code>rustls&lt;/code>). It captures the plaintext before encryption on send and after decryption on recv. For an HTTP call to &lt;code>https://api.anthropic.com/v1/messages&lt;/code>, AgentSight sees the full JSON of the prompt and the response without decrypting anything in transit, simply because it has reached the syscall level before the TLS layer does its work.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>&lt;code>stdiocap&lt;/code> BPF&lt;/strong>: it captures &lt;code>read&lt;/code>, &lt;code>write&lt;/code> and &lt;code>dup&lt;/code> on a process&amp;rsquo;s stdin/stdout/stderr file descriptors. This is what makes it possible to observe MCP servers that speak stdio with their client, the usual pattern for local MCP servers. You capture the JSON-RPC going back and forth without either the client or the server knowing.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;p>Reported overhead: &amp;lt;3% CPU, comparable to Hubble in its own regime.&lt;/p>
&lt;h3 id="how-it-fits-with-hubble-and-tetragon">How it fits with Hubble and Tetragon&lt;/h3>
&lt;p>The three complement each other cleanly:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hubble&lt;/strong> tells you: &amp;ldquo;the agent&amp;rsquo;s pod opened a TCP connection to &lt;code>api.anthropic.com:443&lt;/code> with verdict ALLOW&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Tetragon&lt;/strong> tells you: &amp;ldquo;the &lt;code>claude-code&lt;/code> process with PID 1843 did a &lt;code>connect()&lt;/code> to that IP&amp;rdquo; (plus the binary, the arguments, the pod namespace).&lt;/li>
&lt;li>&lt;strong>AgentSight&lt;/strong> tells you: &amp;ldquo;the HTTPS content of that connection was a prompt &lt;code>messages=[{role:'user', content:'analyze this repo and modify the firewall config'}]&lt;/code> and the response included a tool call to &lt;code>read_file&lt;/code> with argument &lt;code>/etc/passwd&lt;/code>&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>It is the difference between flow, process and semantics. For a security team that wants to watch AI agents in production, all three are necessary. For anyone who wants to understand cost, all three are useful (Hubble for network latency, Tetragon for resource usage, AgentSight for tokens and chosen model).&lt;/p>
&lt;h3 id="emerging-use-cases">Emerging use cases&lt;/h3>
&lt;p>The patterns consolidating in 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Agent audit trail&lt;/strong>: recording every LLM call and every tool call for compliance, especially in regulated sectors.&lt;/li>
&lt;li>&lt;strong>Jailbreak and prompt injection detection&lt;/strong>: applying rules over the prompts captured by AgentSight (similar to Tetragon&amp;rsquo;s TracingPolicy, but over semantic content).&lt;/li>
&lt;li>&lt;strong>Cost accountability&lt;/strong>: seeing which team/agent consumes which tokens, without instrumenting.&lt;/li>
&lt;li>&lt;strong>Replay and debugging&lt;/strong>: reproducing an agent&amp;rsquo;s reasoning in production without asking it to run again (which is non-deterministic).&lt;/li>
&lt;/ul>
&lt;p>It is a young field, AgentSight is months old rather than years, but the &amp;ldquo;eBPF as zero-instrumentation observability&amp;rdquo; pattern is very clearly spreading beyond network and process. The coming year will see consolidation and, probably, native integration with Hubble.&lt;/p>
&lt;h2 id="common-hubble-use-cases">Common Hubble use cases&lt;/h2>
&lt;p>Back to Hubble proper, the cases in which any organisation deploys it:&lt;/p>
&lt;h3 id="1-networkpolicy-debugging">1. NetworkPolicy debugging&lt;/h3>
&lt;p>The classic use: &amp;ldquo;this pod cannot reach this Service&amp;rdquo;. Without Hubble, it meant SSH, tcpdump, comparing rules. With Hubble:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hubble observe --from-pod prod-api/checkout --to-pod prod-db/postgres --verdict DROPPED
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If there are drops, you see the policy responsible (Cilium 1.19+). If there are no drops, the problem is not policy: it is DNS, routing or the target service.&lt;/p>
&lt;h3 id="2-auditing-inter-namespace-communication">2. Auditing inter-namespace communication&lt;/h3>
&lt;p>For compliance: validating that isolated namespaces are not communicating against what was declared.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hubble observe --from-namespace prod-payments --to-namespace &lt;span class="s1">&amp;#39;NOT prod-db&amp;#39;&lt;/span> --output json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-exfiltration-detection">3. Exfiltration detection&lt;/h3>
&lt;p>Outbound traffic to suspicious public destinations. Hubble detects them by IP/SNI, not by payload (which is encrypted):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">hubble observe --to-fqdn &lt;span class="s1">&amp;#39;NOT *.example.com&amp;#39;&lt;/span> --to-fqdn &lt;span class="s1">&amp;#39;NOT *.internal&amp;#39;&lt;/span> --protocol tcp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Combined with Prometheus metrics and Grafana alerts, this gives you an exfiltration radar at zero cost.&lt;/p>
&lt;h3 id="4-real-time-service-slos">4. Real-time service SLOs&lt;/h3>
&lt;p>The &lt;code>hubble:http:response_time_seconds&lt;/code> metrics with &lt;code>source_workload&lt;/code>, &lt;code>destination_workload&lt;/code>, &lt;code>method&lt;/code> and &lt;code>status_code&lt;/code> labels allow SLO dashboards without needing to instrument the apps. The SRE sees the p95 latency of &lt;code>checkout → catalog&lt;/code> directly.&lt;/p>
&lt;h3 id="5-performance-debugging">5. Performance debugging&lt;/h3>
&lt;p>&lt;code>hubble:tcp:retransmissions_total&lt;/code> and &lt;code>hubble:tcp:flags_total{flag=&amp;quot;RST&amp;quot;}&lt;/code> are early signals of network problems. A rise correlated with a latency regression points you at something in the infrastructure (NIC, switch, MTU) before you go down to investigate the app.&lt;/p>
&lt;h3 id="6-post-incident-forensics">6. Post-incident forensics&lt;/h3>
&lt;p>Configuring Hubble to export flows to persistent storage (via OTLP to Tempo/Loki, or &lt;code>hubble observe --output jsonpb&lt;/code> to S3) gives you forensic capability: if at T+30 days you detect that something was wrong at T, you can reconstruct the traffic.&lt;/p>
&lt;h2 id="hubble-and-the-rest-of-the-observability-stack">Hubble and the rest of the observability stack&lt;/h2>
&lt;p>Hubble does not replace Prometheus, Loki, Tempo or Jaeger; it complements them:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prometheus&lt;/strong>: receives Hubble&amp;rsquo;s aggregated metrics. Hubble exports a native Prometheus endpoint.&lt;/li>
&lt;li>&lt;strong>Loki&lt;/strong>: receives the structured flow logs if you export them as logs. Hubble has no native Loki exporter, but a Fluent Bit with an OTLP plugin or a custom one bridges it easily.&lt;/li>
&lt;li>&lt;strong>Tempo / Jaeger&lt;/strong>: the Cilium Operator has an OTLP exporter of flows in trace format (each HTTP/gRPC flow is a span). It integrates with Tempo or any other OTLP tracing backend.&lt;/li>
&lt;li>&lt;strong>Grafana&lt;/strong>: there are already public Hubble dashboards. Combined with Prometheus, Loki and Tempo, they give you a unified panel: metrics, logs, traces, all correlated by K8s labels.&lt;/li>
&lt;/ul>
&lt;p>The full-stack pile seen in production in 2026 (described in &lt;a href="https://dev.to/x4nent/building-a-production-ebpf-observability-security-stack-for-kubernetes-in-2026-5051">Building a Production eBPF Observability &amp;amp; Security Stack for Kubernetes in 2026&lt;/a>):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Data&lt;/strong>: Cilium + Hubble (network), Tetragon (process), AgentSight (AI agent).&lt;/li>
&lt;li>&lt;strong>Pipeline&lt;/strong>: OTLP Collector as the single router.&lt;/li>
&lt;li>&lt;strong>Storage&lt;/strong>: Prometheus (metrics), Loki (logs), Tempo (traces).&lt;/li>
&lt;li>&lt;strong>UI&lt;/strong>: Grafana with domain-specific dashboards.&lt;/li>
&lt;li>&lt;strong>Alerting&lt;/strong>: AlertManager with rules over the Hubble + Tetragon metrics.&lt;/li>
&lt;/ul>
&lt;h2 id="comparison-with-the-alternatives">Comparison with the alternatives&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>System&lt;/th>
&lt;th>Layer&lt;/th>
&lt;th>Focus&lt;/th>
&lt;th>Model&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Hubble&lt;/strong>&lt;/td>
&lt;td>L3-L7 network&lt;/td>
&lt;td>K8s cluster with Cilium&lt;/td>
&lt;td>eBPF, pull metrics, push flows gRPC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GKE Dataplane v2 obs&lt;/strong>&lt;/td>
&lt;td>L3-L7 network&lt;/td>
&lt;td>GKE managed&lt;/td>
&lt;td>eBPF (Cilium-based, managed)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tigera Calico Whisker&lt;/strong>&lt;/td>
&lt;td>L3-L7 network&lt;/td>
&lt;td>Cluster with Calico&lt;/td>
&lt;td>eBPF + pcap, own UI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tetragon&lt;/strong>&lt;/td>
&lt;td>Process/syscall&lt;/td>
&lt;td>K8s cluster&lt;/td>
&lt;td>eBPF, push events gRPC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Falco&lt;/strong>&lt;/td>
&lt;td>Process/syscall&lt;/td>
&lt;td>K8s cluster&lt;/td>
&lt;td>eBPF in userspace or kernel module&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AgentSight&lt;/strong>&lt;/td>
&lt;td>LLM agent&lt;/td>
&lt;td>Agentic systems&lt;/td>
&lt;td>eBPF (SSL uprobes + stdio)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Beyla&lt;/strong> (Grafana)&lt;/td>
&lt;td>Application&lt;/td>
&lt;td>App L7 + tracing&lt;/td>
&lt;td>eBPF (uprobes on libs)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Pixie&lt;/strong>&lt;/td>
&lt;td>App + system&lt;/td>
&lt;td>Broad cluster visibility&lt;/td>
&lt;td>eBPF + PXL script&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Parca&lt;/strong>&lt;/td>
&lt;td>CPU/mem profiling&lt;/td>
&lt;td>Performance&lt;/td>
&lt;td>eBPF profile sampling&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If your CNI is Cilium, Hubble is the natural entry point and it does not compete with the rest: it complements them. For Calico clusters, Whisker is the equivalent. For profiling, Parca. For AI agents, AgentSight. The era of &amp;ldquo;one tool for everything&amp;rdquo; is passing: the modern stack combines several specialised pieces, all eBPF-based, exposed via OTLP.&lt;/p>
&lt;h2 id="operational-traps">Operational traps&lt;/h2>
&lt;h3 id="cardinality-in-prometheus">Cardinality in Prometheus&lt;/h3>
&lt;p>Hubble metrics with all the K8s labels can blow up Prometheus. &lt;strong>Measure cardinality before exporting everything.&lt;/strong> The most prolific metrics are &lt;code>flow&lt;/code> and &lt;code>httpV2&lt;/code>; start with &lt;code>drop&lt;/code> and &lt;code>port-distribution&lt;/code> and add the rest incrementally.&lt;/p>
&lt;h3 id="l7-visibility-costs-cpu">L7 visibility costs CPU&lt;/h3>
&lt;p>Enabling L7 parsing via embedded Envoy adds load to the agent (not to the base datapath, but yes to the node&amp;rsquo;s envoy proxy). For heavy HTTP traffic, measure. For flows where you only need L4, leave Envoy disabled.&lt;/p>
&lt;h3 id="hubble-relay-without-ha">Hubble Relay without HA&lt;/h3>
&lt;p>A single Relay replica is a single point of failure for the CLI and the UI (not for the local agent, which keeps working). For production, deploy with &lt;code>replicas: 2+&lt;/code> and &lt;code>topologySpreadConstraints&lt;/code> so both do not go down together.&lt;/p>
&lt;h3 id="encryption-status-reporting-depends-on-the-cilium-config">Encryption status reporting depends on the Cilium config&lt;/h3>
&lt;p>The new &lt;code>--encryption-status&lt;/code> filter only gives real data if Cilium has encryption enabled (WireGuard or IPsec). Without that, everything is &lt;code>unencrypted&lt;/code> and the filter contributes nothing.&lt;/p>
&lt;h3 id="ui-exposed-without-auth">UI exposed without auth&lt;/h3>
&lt;p>Hubble UI has no native auth. If you expose it through Ingress, &lt;strong>there has to be authentication in front of it&lt;/strong>: OIDC via oauth2-proxy, mTLS, IP allowlist. It is not optional.&lt;/p>
&lt;h3 id="storage-that-does-not-scale">Storage that does not scale&lt;/h3>
&lt;p>If you keep flows for days for forensics, the volume is serious. For a cluster with 100 active pods, easily 1-10 GB/day of flow logs. Plan the lifecycle (compaction, retention, cold storage) before enabling it.&lt;/p>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Mesh / multi-cluster Hubble&lt;/strong>: aggregating flows from several Cilium clusters into a single Relay. Use case: a cross-cluster view, debugging a distributed service mesh.&lt;/li>
&lt;li>&lt;code>hubble export&lt;/code>: local persistence on the agent&amp;rsquo;s disk for forensics with low retention.&lt;/li>
&lt;li>&lt;strong>Anomaly detection with your own models&lt;/strong>: how to connect the gRPC stream to a custom ML consumer.&lt;/li>
&lt;li>&lt;strong>AgentSight in depth&lt;/strong>: the project deserves its own article. Next instalment.&lt;/li>
&lt;li>&lt;strong>eBPF for LLM serving profiling&lt;/strong>: how to measure vLLM&amp;rsquo;s TTFT, TPOT and throughput without instrumenting, using uprobes on libcudart.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;p>Hubble and Cilium:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/cilium/hubble">Hubble GitHub&lt;/a> — main repo.&lt;/li>
&lt;li>&lt;a href="https://docs.cilium.io/en/stable/observability/hubble/">Hubble — Network Observability (Cilium docs)&lt;/a> — official reference.&lt;/li>
&lt;li>&lt;a href="https://www.infoq.com/news/2026/02/cilium-119/">Cilium 1.19 release notes (InfoQ, feb 2026)&lt;/a> — tenth anniversary and 1.19 news.&lt;/li>
&lt;li>&lt;a href="https://github.com/cilium/cilium/releases">Cilium releases&lt;/a> — all releases.&lt;/li>
&lt;li>&lt;a href="https://grafana.com/grafana/dashboards/19423-hubble-l7-http-metrics-by-workload/">Hubble L7 HTTP Metrics — Grafana dashboard 19423&lt;/a> — ready to import.&lt;/li>
&lt;li>&lt;a href="https://cloud-cod.com/index.php/2026/03/03/end-to-end-l7-visibility-with-cilium-hubble/">End‑to‑end L7 Visibility with Cilium Hubble (cloud-cod.com, mar 2026)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.youngju.dev/blog/cilium/cilium_hubble_observability.en">Cilium Hubble Observability Platform Internal Analysis (Young-ju)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://johal.in/ciliumnetworkpolicy-python-hubble-l7-visibility-2026/">CiliumNetworkPolicy Python Hubble: L7 Visibility 2026&lt;/a> — one of the threads on the Python SDK.&lt;/li>
&lt;/ul>
&lt;p>State of the art 2026 and the full stack:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://dev.to/x4nent/building-a-production-ebpf-observability-security-stack-for-kubernetes-in-2026-5051">Building a Production eBPF Observability &amp;amp; Security Stack for Kubernetes in 2026 (DEV)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.cloudraft.io/blog/ebpf-based-network-observability-using-cilium-hubble">eBPF-Based Network Observability: Exploring Cilium Hubble and Alternatives (CloudRaft)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>eBPF + AI agents:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://github.com/eunomia-bpf/agentsight">AgentSight (GitHub eunomia-bpf)&lt;/a> — the referenced project.&lt;/li>
&lt;li>&lt;a href="https://klizosolutions.medium.com/harnessing-ebpf-for-high-performance-llm-workloads-a-cloud-native-guide-efb7d73e19ed">Harnessing eBPF for High‑Performance LLM Workloads (Klizo Solutions)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Part 1: &lt;a href="https://blog.lo0.es/en/posts/ebpf-zero-to-cilium-kernel-tcp-ip-bypass/">eBPF from zero to Cilium&lt;/a>.&lt;/li>
&lt;li>Part 2: &lt;a href="https://blog.lo0.es/en/posts/tetragon-cilium-security-syscalls-kernel/">Tetragon: Cilium&amp;rsquo;s security cousin&lt;/a>.&lt;/li>
&lt;li>LLM inference series: &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on K8s&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">PagedAttention&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM K8s Operators&lt;/a> — where the traffic Hubble observes carries the prompts AgentSight inspects.&lt;/li>
&lt;/ul></description></item></channel></rss>