<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ OpenTelemetry - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ OpenTelemetry - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 25 Aug 2026 19:18:38 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/opentelemetry/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Trace Multi-Agent AI Swarms with Jaeger v2 ]]>
                </title>
                <description>
                    <![CDATA[ When you run a single AI agent, debugging is straightforward. You read the log, you see what happened. When you run five agents in a swarm, each spawning its own tool calls and producing its own outpu ]]>
                </description>
                <link>https://www.freecodecamp.org/news/multi-agent-ai-swarms-tracing/</link>
                <guid isPermaLink="false">69eaae45904b915438cefb47</guid>
                
                    <category>
                        <![CDATA[ jaeger ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ distributed tracing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ multi-agent systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai agents ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Christopher Galliart ]]>
                </dc:creator>
                <pubDate>Thu, 23 Apr 2026 23:41:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/308710e6-cfe6-4007-887a-c49a5e2e6b9a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you run a single AI agent, debugging is straightforward. You read the log, you see what happened.</p>
<p>When you run five agents in a swarm, each spawning its own tool calls and producing its own output, "read the log" stops being a strategy.</p>
<p>I built <a href="https://github.com/HatmanStack/claude-forge">Claude Forge</a> as an adversarial multi-agent coding framework on top of Claude Code. A typical run spawns a planner, an implementer, a reviewer, and a fixer. They evaluate each other's work and loop back when quality checks fail.</p>
<p>But when something went wrong, I had timestamps and text dumps but no way to see which agent was responsible, how long it actually took, or where the tokens went.</p>
<p>Jaeger fixed that. This article covers setting up Jaeger v2 with Docker, wiring it into a multi-agent system through OpenTelemetry, and what I learned along the way.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-distributed-tracing">What Is Distributed Tracing?</a></p>
</li>
<li><p><a href="#heading-why-jaeger-v2">Why Jaeger v2?</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-installing-docker-on-debian">Installing Docker on Debian</a></p>
</li>
<li><p><a href="#heading-setting-up-jaeger-v2">Setting Up Jaeger v2</a></p>
</li>
<li><p><a href="#heading-setting-up-claude-forge-tracing">Setting Up Claude Forge Tracing</a></p>
</li>
<li><p><a href="#heading-understanding-the-span-model">Understanding the Span Model</a></p>
</li>
<li><p><a href="#heading-instrumenting-a-multi-agent-swarm">Instrumenting a Multi-Agent Swarm</a></p>
</li>
<li><p><a href="#heading-viewing-traces-in-the-jaeger-ui">Viewing Traces in the Jaeger UI</a></p>
</li>
<li><p><a href="#heading-lessons-from-the-trenches">Lessons from the Trenches</a></p>
</li>
<li><p><a href="#heading-environment-variable-reference">Environment Variable Reference</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ul>
<h2 id="heading-what-is-distributed-tracing">What Is Distributed Tracing?</h2>
<p>Distributed tracing tracks a single operation as it moves through multiple services. A span is one unit of work with a start time, end time, and key-value attributes. Spans nest into parent-child trees. One tree per operation is one trace.</p>
<p>Microservices people already know this pattern: follow an HTTP request from the gateway through auth, the database, and the cache. Same idea works for multi-agent AI. Follow one swarm invocation from the orchestrator through each subagent and its tool calls.</p>
<p>OpenTelemetry (OTel) is the standard. It gives you SDKs for creating spans and shipping them over OTLP. Jaeger receives that data and renders it as a searchable timeline.</p>
<h2 id="heading-why-jaeger-v2">Why Jaeger v2?</h2>
<p>Jaeger started at Uber and graduated as a CNCF project in 2019. v1 hit end of life in December 2025. v2 is the current release, built on the OpenTelemetry Collector framework. Single binary: collector, query service, and UI. It speaks OTLP natively on port 4317 (gRPC) and 4318 (HTTP). There's no separate collector needed for local work.</p>
<p>One important difference from v1: configuration moved from CLI flags and environment variables to a YAML file. The old <code>-e SPAN_STORAGE_TYPE=badger</code> env vars are silently ignored in v2. The container starts fine but falls back to in-memory storage. I lost two days of traces before noticing. More on the correct setup below.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><strong>Docker</strong> installed and running.</p>
</li>
<li><p><strong>Claude Code</strong> installed.</p>
</li>
<li><p><strong>Python 3.8+</strong> for the tracing hook.</p>
</li>
<li><p><strong>Claude Forge</strong> or another multi-agent system to instrument.</p>
</li>
</ul>
<h2 id="heading-installing-docker-on-debian">Installing Docker on Debian</h2>
<p>Skip this if you already have Docker. macOS and Windows users can use Docker Desktop. On Debian:</p>
<pre><code class="language-bash">sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/debian \
  \((. /etc/os-release &amp;&amp; echo "\)VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
</code></pre>
<p>Ubuntu users: replace both <code>linux/debian</code> URLs with <code>linux/ubuntu</code>.</p>
<h2 id="heading-setting-up-jaeger-v2">Setting Up Jaeger v2</h2>
<h3 id="heading-basic-run">Basic Run</h3>
<p>For quick testing with no persistence:</p>
<pre><code class="language-bash">docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  jaegertracing/jaeger:2.17.0
</code></pre>
<p>Port 16686 is the UI. Port 4317 is OTLP/gRPC ingestion. Port 4318 is OTLP/HTTP. Remove the container and your traces are gone.</p>
<h3 id="heading-persistent-storage-with-badger">Persistent Storage with Badger</h3>
<p>v2 reads configuration from a YAML file, not environment variables. Save this as <code>~/.local/share/jaeger/config.yaml</code>:</p>
<pre><code class="language-yaml">service:
  extensions: [jaeger_storage, jaeger_query, healthcheckv2]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger_storage_exporter]
extensions:
  healthcheckv2:
    use_v2: true
    http: { endpoint: 0.0.0.0:13133 }
  jaeger_query:
    storage: { traces: main_store }
  jaeger_storage:
    backends:
      main_store:
        badger:
          directories: { keys: /badger/key, values: /badger/data }
          ephemeral: false
          ttl: { spans: 720h }
receivers:
  otlp:
    protocols:
      grpc: { endpoint: 0.0.0.0:4317 }
      http: { endpoint: 0.0.0.0:4318 }
processors:
  batch:
exporters:
  jaeger_storage_exporter:
    trace_storage: main_store
</code></pre>
<p>The Jaeger container runs as UID 10001. Docker named volumes default to root ownership. Without fixing permissions first, the container crash-loops with <code>mkdir /badger/key: permission denied</code>.</p>
<p>Pre-create the volume and fix ownership:</p>
<pre><code class="language-bash">docker volume create jaeger-data

docker run --rm \
  -v jaeger-data:/badger \
  alpine sh -c "mkdir -p /badger/data /badger/key &amp;&amp; chown -R 10001:10001 /badger"
</code></pre>
<p>Then run Jaeger with the config mounted in:</p>
<pre><code class="language-bash">docker run -d --name jaeger \
  --restart unless-stopped \
  -v ~/.local/share/jaeger/config.yaml:/etc/jaeger/config.yaml:ro \
  -v jaeger-data:/badger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  jaegertracing/jaeger:2.17.0 \
  --config /etc/jaeger/config.yaml
</code></pre>
<p>Verify persistence by running <code>docker restart jaeger</code> and confirming a previously recorded trace is still there. Hit <code>http://localhost:16686</code> and you should see the UI.</p>
<h2 id="heading-setting-up-claude-forge-tracing">Setting Up Claude Forge Tracing</h2>
<h3 id="heading-installing-claude-forge">Installing Claude Forge</h3>
<p>Install it through the Claude Code plugin marketplace:</p>
<pre><code class="language-bash">/plugin marketplace add hatmanstack/claude-forge
/plugin install forge@claude-forge
/reload-plugins
</code></pre>
<p>The install opens a TUI to confirm scope and settings. After reload, commands use the <code>forge:</code> prefix (for example, <code>/forge:pipeline</code>).</p>
<p>You can also clone the repo from <a href="https://github.com/HatmanStack/claude-forge">GitHub</a>.</p>
<h3 id="heading-installing-the-tracing-hook">Installing the Tracing Hook</h3>
<p>From your target project directory, run the install script. For plugin installs:</p>
<pre><code class="language-bash">cd your-project
forge-trace                # if you set up the alias from the README
# or, without the alias:
bash "$(find ~/.claude -path '*/forge*' -name install-tracing.sh 2&gt;/dev/null | head -1)"
</code></pre>
<p>For clone installs:</p>
<pre><code class="language-bash">cd your-project
bash /path/to/claude-forge/bin/install-tracing.sh
</code></pre>
<p>The script builds a dedicated venv at <code>~/.local/share/claude-forge/venv</code> (prefers <code>uv</code>, falls back to <code>python3 -m venv</code>), installs the OpenTelemetry packages, copies the hook into place, merges hook entries into <code>.claude/settings.local.json</code>, and self-tests against the OTLP endpoint.</p>
<p>Pass <code>--no-settings</code> to skip the settings merge, or <code>--uninstall</code> to tear everything down.</p>
<h3 id="heading-opting-in">Opting In</h3>
<p>Add to your shell init and restart your terminal:</p>
<pre><code class="language-bash">export CLAUDE_FORGE_TRACING=1
</code></pre>
<p>Restart Claude Code, run <code>/pipeline</code>, then check <code>http://localhost:16686</code> for the <code>claude-forge</code> service.</p>
<h2 id="heading-understanding-the-span-model">Understanding the Span Model</h2>
<p>Here's what the hierarchy looks like for a typical swarm run:</p>
<pre><code class="language-plaintext">session: "implement login form with OAuth"        &lt;- root span
├── subagent:planner
│   ├── tool:Write  (Phase-0.md)                  &lt;- mutation spans (on by default)
│   ├── tool:Write  (Phase-1.md)
│   └── subagent_result:planner                   &lt;- duration, token counts, output
├── subagent:implementer
│   ├── tool:Edit   (src/auth.ts)
│   ├── tool:Bash   (npm test)
│   ├── tool:Write  (src/oauth.ts)
│   └── subagent_result:implementer
├── subagent:reviewer
│   └── subagent_result:reviewer
└── session_complete                              &lt;- session totals
</code></pre>
<p>The root span's name comes from the first line of your prompt. Find traces by what you asked for, not by a UUID.</p>
<p>Subagents get an anchor span on start and a result span on completion. The result carries duration, token counts, prompt, and output.</p>
<h3 id="heading-three-tiers-of-detail">Three Tiers of Detail</h3>
<p>Not all inner tool calls are equally interesting. Write, Edit, MultiEdit, and Bash are mutational: small in number, high signal. They tell you what actually changed. Read, Glob, Grep, and WebFetch are navigation: lots of them, mostly noise.</p>
<p>Tracing captures mutations by default. That middle ground turned out to be the right one. Before this change, you either saw nothing inside subagents or you saw 200+ spans per run.</p>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Subagents</th>
<th>Mutations (Write/Edit/Bash)</th>
<th>Other inner tools</th>
</tr>
</thead>
<tbody><tr>
<td>Default</td>
<td>yes</td>
<td>yes</td>
<td>no</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_TRACE_INNER=1</code></td>
<td>yes</td>
<td>yes</td>
<td>yes (minus blocklist)</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_TRACE_MUTATIONS=0</code></td>
<td>yes</td>
<td>no</td>
<td>no (or per INNER)</td>
</tr>
</tbody></table>
<h3 id="heading-span-attributes">Span Attributes</h3>
<p><strong>On</strong> <code>session_complete</code><strong>:</strong> <code>session.tokens.input</code>, <code>session.tokens.output</code>, <code>session.tokens.total</code>, <code>session.tokens.turns</code>, <code>session.duration_ms</code>, <code>user.prompt</code> (first 2KB).</p>
<p><strong>On</strong> <code>subagent_result</code><strong>:</strong> <code>agent.description</code>, <code>agent.prompt</code>, <code>agent.output</code>, <code>agent.duration_ms</code>, <code>agent.is_error</code>, <code>agent.tokens.input</code>, <code>agent.tokens.output</code>.</p>
<p><strong>On</strong> <code>tool:*</code><strong>:</strong> <code>tool.name</code>, <code>tool.input</code>, <code>tool.output</code>, <code>tool.duration_ms</code>, <code>tool.is_error</code>.</p>
<h2 id="heading-instrumenting-a-multi-agent-swarm">Instrumenting a Multi-Agent Swarm</h2>
<h3 id="heading-hook-architecture">Hook Architecture</h3>
<p>Claude Code has lifecycle hooks that fire scripts on specific events. Four matter here:</p>
<ol>
<li><p><strong>UserPromptSubmit</strong> (create the root span),</p>
</li>
<li><p><strong>PreToolUse</strong> (start a span),</p>
</li>
<li><p><strong>PostToolUse</strong> (end it with results), and</p>
</li>
<li><p><strong>Stop</strong> (finalize the trace). Each hook gets a JSON payload on stdin and runs as a subprocess.</p>
</li>
</ol>
<h3 id="heading-sending-spans-with-opentelemetry">Sending Spans with OpenTelemetry</h3>
<p>Here's some minimal Python to get a span into Jaeger:</p>
<pre><code class="language-python">from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource

resource = Resource.create({"service.name": "my-agent-system"})
exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("agent-tracer")

with tracer.start_as_current_span("my-agent-task") as span:
    span.set_attribute("agent.name", "planner")
    span.set_attribute("agent.tokens.input", 1500)
    span.set_attribute("agent.tokens.output", 800)
</code></pre>
<p>Refresh <code>localhost:16686</code>, pick your service, click "Find Traces."</p>
<h3 id="heading-correlating-pre-and-post-events">Correlating Pre and Post Events</h3>
<p>You need to match each PreToolUse to its PostToolUse. Agent-type tool calls didn't include a <code>tool_use_id</code> in the payload, so I hashed the tool name and input instead. Pre and Post carry identical <code>tool_input</code>, so the hashes line up.</p>
<pre><code class="language-python">import hashlib, json

def correlation_key(tool_name: str, tool_input: dict) -&gt; str:
    content = json.dumps({"tool": tool_name, "input": tool_input}, sort_keys=True)
    return hashlib.sha1(content.encode()).hexdigest()[:16]
</code></pre>
<h3 id="heading-state-across-invocations">State Across Invocations</h3>
<p>Every hook call is a separate process. No shared memory. So I wrote span context to JSON files on Pre and read them back on Post:</p>
<pre><code class="language-plaintext">/tmp/claude-forge-tracing/&lt;session_id&gt;/
├── _root.json              # trace ID, root span context
├── _session_start_ns.json  # timestamp for duration calculation
├── subagent_&lt;hash&gt;.json    # per-subagent span context
└── tool_&lt;hash&gt;.json        # per-tool span context
</code></pre>
<p>File names get sanitized against path traversal. <code>_safe_name()</code> strips everything outside <code>[A-Za-z0-9._-]</code> and falls back to a SHA1 slug.</p>
<h3 id="heading-flushing-without-blocking">Flushing Without Blocking</h3>
<pre><code class="language-python">try:
    provider.force_flush(timeout_millis=1000)
except Exception:
    pass  # Never block the swarm
</code></pre>
<p>I tried 2000ms first and the swarm felt slow. 100ms lost spans on cold TLS connections. 1000ms worked. If Jaeger is down, the swarm keeps running regardless.</p>
<h2 id="heading-viewing-traces-in-the-jaeger-ui">Viewing Traces in the Jaeger UI</h2>
<p>Open <code>http://localhost:16686</code>. Pick <code>claude-forge</code> from the service dropdown. Click "Find Traces."</p>
<p>The trace search filters by operation name, tags, and time range. Since session spans take their name from your prompt, searching "login form" pulls up the runs where you asked for one.</p>
<p>The timeline view is where I spend most of my time. Every span is a horizontal bar, nested by parent-child relationships. I can see the planner took 12 seconds, the implementer 45, the reviewer 8. Click any bar to see token counts, prompts, outputs, error status.</p>
<p>Trace comparison puts two runs side by side. This is good for figuring out why one run succeeded and another did not.</p>
<h2 id="heading-lessons-from-the-trenches">Lessons from the Trenches</h2>
<p><strong>One trace per swarm, not per subagent:</strong> My first version wiped the root span's state file on every Stop event, so each subagent started a new trace. I changed Stop to mark a timestamp while preserving the root.</p>
<p><strong>Use descriptions, not type names:</strong> Subagents all report their type as <code>general-purpose</code>. The description field is where the actual role lives.</p>
<p><strong>Token attribution needs per-agent transcripts:</strong> Claude Code writes subagent transcripts to <code>~/.claude/projects/&lt;project&gt;/&lt;session&gt;/subagents/agent-*.jsonl</code>. Match them via <code>agent-*.meta.json</code>.</p>
<p><strong>Parse boolean env vars explicitly:</strong> <code>bool("0")</code> in Python is <code>True</code>. Use an allowlist: <code>{"1", "true", "yes", "on"}</code>.</p>
<h2 id="heading-environment-variable-reference">Environment Variable Reference</h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>CLAUDE_FORGE_TRACING=1</code></td>
<td>Master opt-in. Hook is a no-op without this.</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_TRACE_MUTATIONS=0</code></td>
<td>Disable default mutation spans (Write/Edit/Bash). On by default.</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_TRACE_INNER=1</code></td>
<td>Capture all inner tool calls as child spans (off by default).</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_TRACE_TOOL_BLOCKLIST</code></td>
<td>Comma-separated tools to skip when inner tracing is on. Defaults to <code>Read,Glob,Grep,TodoWrite,NotebookRead</code>.</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_HOOK_DEBUG=1</code></td>
<td>Enable debug logging of raw hook payloads. Off by default.</td>
</tr>
<tr>
<td><code>CLAUDE_FORGE_HOOK_DEBUG_LOG</code></td>
<td>Override debug log path. Defaults to <code>~/.cache/claude-forge/hook.log</code>.</td>
</tr>
<tr>
<td><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></td>
<td>OTLP/gRPC endpoint. Defaults to <code>http://localhost:4317</code>.</td>
</tr>
</tbody></table>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Without visibility into the process, you're being inefficient with tokens and your time. Multi-agent swarms cost real money on every run. When an agent fails and retries, or when a reviewer rejects work that was close, you're paying for that blind.</p>
<p>Tracing gives you the map. You find out where the failure modes are. You find out which agents burn tokens going nowhere. A 45-second implementer run might have been 10 seconds with a better planner prompt. But you would never know that without seeing the breakdown.</p>
<p>Get observability in early. Jaeger and OpenTelemetry make it cheap to set up. Once you can see where things go wrong you can actually fix them.</p>
<p>Claude Forge tracing is on the <a href="https://github.com/HatmanStack/claude-forge">main branch</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build End-to-End LLM Observability in FastAPI with OpenTelemetry ]]>
                </title>
                <description>
                    <![CDATA[ This article shows how to build end-to-end, code-first LLM observability in a FastAPI application using the OpenTelemetry Python SDK. Instead of relying on vendor-specific agents or opaque SDKs, we wi ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-end-to-end-llm-observability-in-fastapi-with-opentelemetry/</link>
                <guid isPermaLink="false">69b4379c6e27dd07d920f14c</guid>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jessica Patel ]]>
                </dc:creator>
                <pubDate>Fri, 13 Mar 2026 16:13:16 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c69a589a-2dce-46a1-ac49-a0d0e2c23c6e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>This article shows how to build end-to-end, code-first LLM observability in a FastAPI application using the OpenTelemetry Python SDK.</p>
<p>Instead of relying on vendor-specific agents or opaque SDKs, we will manually design traces, spans, and semantic attributes that capture the full lifecycle of an LLM-powered request.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-introduction">Introduction</a></p>
</li>
<li><p><a href="#heading-prerequisites-and-technical-context">Prerequisites and Technical Context</a></p>
</li>
<li><p><a href="#heading-why-llm-observability-is-fundamentally-different">Why LLM Observability Is Fundamentally Different</a></p>
</li>
<li><p><a href="#heading-reference-architecture-a-traceable-rag-request">Reference Architecture: A Traceable RAG Request</a></p>
</li>
<li><p><a href="#heading-reference-architecture-explained">Reference Architecture Explained</a></p>
</li>
<li><p><a href="#heading-why-this-design-is-better-than-simpler-alternatives">Why This Design Is Better Than Simpler Alternatives</a></p>
</li>
<li><p><a href="#heading-llm-models-that-work-best-for-this-architecture">LLM Models That Work Best for This Architecture</a></p>
</li>
<li><p><a href="#heading-opentelemetry-primer-llm-relevant-concepts-only">OpenTelemetry Primer (LLM-Relevant Concepts Only)</a></p>
</li>
<li><p><a href="#heading-designing-llm-aware-spans">Designing LLM-Aware Spans</a></p>
</li>
<li><p><a href="#heading-fastapi-example-end-to-end-llm-spans-complete-and-explained">FastAPI Example: End-to-End LLM Spans (Complete and Explained)</a></p>
</li>
<li><p><a href="#heading-semantic-attributes-best-practices-for-llm-observability">Semantic Attributes: Best Practices for LLM Observability</a></p>
</li>
<li><p><a href="#heading-evaluation-hooks-inside-traces">Evaluation Hooks Inside Traces</a></p>
</li>
<li><p><a href="#heading-exporting-and-visualizing-traces-where-this-fits-with-vendor-tooling">Exporting and Visualizing Traces (Where This Fits with Vendor Tooling)</a></p>
</li>
<li><p><a href="#heading-operational-patterns-and-anti-patterns">Operational Patterns and Anti-Patterns</a></p>
</li>
<li><p><a href="#heading-extending-the-system">Extending the System</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>Large Language Models (LLMs) are rapidly becoming a core component of modern software systems. Applications that once relied on deterministic APIs are now incorporating LLM-powered features such as conversational assistants, document summarization, intelligent search, and retrieval-augmented generation (RAG).</p>
<p>While these capabilities unlock new user experiences, they also introduce operational complexity that traditional monitoring approaches were never designed to handle.</p>
<p>Unlike conventional software services, LLM systems are probabilistic by nature. The same request may produce slightly different responses depending on factors such as prompt structure, model configuration, retrieval context, and sampling parameters such as temperature or top-p.</p>
<p>In addition, LLM workloads introduce entirely new operational dimensions such as token consumption, prompt construction latency, inference cost, context window limits, and response quality.</p>
<p>These factors mean that a request can appear technically successful from an infrastructure perspective while still producing an incorrect, hallucinated, or low-quality result.</p>
<p>Traditional observability tools typically focus on infrastructure-level signals such as latency, error rate, and throughput. While these metrics remain important, they are insufficient for understanding how an LLM application behaves in production.</p>
<p>Engineers must also understand what prompt was constructed, which documents were retrieved, how many tokens were consumed, which model configuration was used, and how the final response was evaluated. Without this visibility, debugging LLM behavior becomes extremely difficult and operational costs can quickly spiral out of control.</p>
<p>This is where LLM observability becomes essential. Observability for LLM systems extends beyond infrastructure monitoring. It captures the full lifecycle of an AI-driven request — from user input and context retrieval to prompt construction, model inference, post-processing, and quality evaluation.</p>
<p>When implemented correctly, observability allows teams to answer why the model generated a particular response, which retrieval results influenced the output, how much a request cost in terms of tokens, where latency occurred within the request pipeline, and whether the response passed basic quality or safety checks.</p>
<p>This article demonstrates how to implement end-to-end LLM observability in a FastAPI application using OpenTelemetry. Instead of relying on proprietary monitoring agents or opaque vendor SDKs, we take a code-first approach to instrumentation. By explicitly designing traces, spans, and semantic attributes, we gain precise control over how LLM interactions are observed and analyzed.</p>
<p>Throughout the guide, we will walk through a practical architecture for tracing a retrieval-augmented generation (RAG) workflow, where each stage of the request lifecycle is represented as a trace span. We will explore how to design meaningful span boundaries, capture prompt and model metadata safely, record token usage and cost signals, and attach evaluation results directly to traces.</p>
<p>The article also explains how this instrumentation can be exported to any OpenTelemetry-compatible backend such as Jaeger, Grafana Tempo, or LLM-specific platforms like Phoenix.</p>
<p>By the end of this guide, you will understand how to:</p>
<ul>
<li><p>Structure traces so that each user request maps to a single end-to-end LLM interaction</p>
</li>
<li><p>Design span hierarchies that reflect the logical stages of an LLM pipeline</p>
</li>
<li><p>Capture prompt metadata, model configuration, and token usage safely</p>
</li>
<li><p>Attach evaluation and quality signals to traces for deeper analysis</p>
</li>
<li><p>Export observability data to different backends without changing instrumentation</p>
</li>
</ul>
<p>Most importantly, the goal of this article is not simply to demonstrate how to add telemetry to an application. Instead, it aims to show how to think about observability when building LLM-powered systems.</p>
<p>When LLM operations are treated as first-class components within a distributed system, traces become a powerful tool for debugging, optimization, cost management, and continuous improvement of model behavior.</p>
<h2 id="heading-prerequisites-and-technical-context">Prerequisites and Technical Context</h2>
<p>Before following this guide, you should be familiar with the Python programming language, basic web API concepts, and general microservice architecture. Below are some key tools and concepts used in this article.</p>
<h3 id="heading-fastapi-web-framework">FastAPI (Web Framework)</h3>
<p>FastAPI is used as the primary web framework for the application. It is a modern Python framework designed for building high-performance APIs using standard Python type hints. FastAPI simplifies request validation, serialization, and API documentation while remaining lightweight and fast.</p>
<h3 id="heading-large-language-models-llms">Large Language Models (LLMs)</h3>
<p>Large Language Models (LLMs) are the computational core of the example system. An LLM is a model trained on vast amounts of text data to generate or transform language in ways that resemble human communication. In production environments, LLMs are commonly used for tasks such as conversational interfaces, summarization, and question answering.</p>
<h3 id="heading-observability-concept">Observability (Concept)</h3>
<p>Observability is the overarching concept that connects all the technical pieces in this article. At a high level, observability refers to the ability to understand a system's internal behavior by examining the data it produces during execution. Rather than asking whether a system is simply "up" or "down," observability helps answer deeper questions about why a request behaved a certain way, where latency was introduced, or how different components interacted.</p>
<h3 id="heading-opentelemetry-instrumentation-standard">OpenTelemetry (Instrumentation Standard)</h3>
<p>OpenTelemetry is the mechanism used to implement observability within the application. It is an open, vendor-neutral standard for generating telemetry data such as traces, metrics, and logs. By instrumenting key parts of the LLM workflow, we can observe how requests flow through the system, how long each step takes, and what contextual data influenced the final outcome. OpenTelemetry serves as the foundation for collecting this information in a consistent and portable way, independent of any specific monitoring backend.</p>
<h2 id="heading-why-llm-observability-is-fundamentally-different">Why LLM Observability Is Fundamentally Different</h2>
<p>Traditional observability assumes deterministic behavior: the same input produces the same output. LLM systems violate this assumption. The same request can vary due to prompt template changes, retrieval differences, sampling parameters (temperature, top-p), model version upgrades, and context window truncation.​</p>
<p>As a result, teams need visibility into what the model saw, how it was configured, what it retrieved, how long it took, and how much it cost, all correlated to a single user request. Logs alone are insufficient, and metrics lack dimensionality. Distributed traces are the backbone of LLM observability.</p>
<h2 id="heading-reference-architecture-a-traceable-rag-request">Reference Architecture: A Traceable RAG Request</h2>
<p>A typical FastAPI-based RAG service follows this flow:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/6979762ba2442d262dacf388/50e7fda4-7407-43d6-8f12-045b8e73c7eb.png" alt="FastAPI Based RAG Service" style="display:block;margin:0 auto" width="936" height="330" loading="lazy">

<p>Each step is observable, but only if we deliberately instrument it. The goal is one trace per user request, with child spans representing each logical LLM step.</p>
<h2 id="heading-reference-architecture-explained">Reference Architecture Explained</h2>
<h3 id="heading-client-sends-a-request-to-chat">Client Sends a Request to /chat</h3>
<p>The architecture begins when a client sends a request to the <code>/chat</code> endpoint. This request typically contains the user's query along with any session or conversation context required by the application.</p>
<p>Keeping the client interface minimal and well-defined is intentional: it ensures the backend receives a predictable input shape and prevents application-specific logic from leaking into downstream LLM processing.</p>
<p>From an observability perspective, this request marks the start of a single end-to-end trace, allowing every subsequent operation to be correlated back to the original user action.</p>
<h3 id="heading-fastapi-validates-input-and-authenticates-the-user">FastAPI Validates Input and Authenticates the User</h3>
<p>Once the request reaches the service, FastAPI performs schema validation and authentication. Validation guarantees that only well-formed inputs proceed through the pipeline, while authentication ensures that expensive LLM operations are only executed for authorized users.</p>
<p>Placing this step early reduces unnecessary computation and protects the system from abuse. It also improves trace quality by ensuring that all observed requests represent legitimate execution paths rather than malformed or rejected traffic.</p>
<h3 id="heading-retriever-queries-the-vector-database">Retriever Queries the Vector Database​</h3>
<p>After validation, the system queries a vector database to retrieve documents relevant to the user's request. This retrieval step is the foundation of retrieval-augmented generation (RAG). By grounding the LLM in external knowledge, the system improves factual accuracy and reduces hallucinations.</p>
<p>Separating retrieval from generation allows teams to tune similarity thresholds, embedding models, and top-k values independently, and it makes it easier to diagnose whether poor responses are caused by bad retrieval or model behavior.</p>
<h3 id="heading-prompt-is-assembled-using-retrieved-documents">Prompt Is Assembled Using Retrieved Documents</h3>
<p>With relevant documents in hand, the system constructs the final prompt that will be sent to the LLM. This step combines the user query, retrieved context, system instructions, and formatting rules into a single structured prompt.</p>
<p>Making prompt assembly an explicit stage enables prompt versioning, experimentation, and observability. It also provides a natural place to detect issues such as context window overflows or excessive prompt size before invoking the model.</p>
<h3 id="heading-llm-api-is-invoked">LLM API Is Invoked</h3>
<p>The LLM API call is the most expensive and non-deterministic operation in the pipeline, which is why it occurs only after all preparatory work is complete. At this stage, the model receives a fully constructed prompt and produces a response based on its configuration parameters.</p>
<p>This step is the primary focus of latency, cost, and reliability controls such as retries, timeouts, and circuit breakers. From an observability standpoint, this span becomes the anchor for token usage, cost attribution, and prompt-level debugging.</p>
<h3 id="heading-response-is-post-processed-and-returned">Response Is Post-Processed and Returned</h3>
<p>After the LLM returns a response, the system performs post-processing before sending the result back to the client. This may include formatting, filtering, validation, or enrichment of the output. Post-processing acts as a final safeguard against malformed or low-quality responses and ensures consistency with application requirements. It also provides a clean boundary for attaching evaluation signals, such as response length, relevance scores, or truncation indicators, before the request completes.</p>
<h2 id="heading-why-this-design-is-better-than-simpler-alternatives">Why This Design Is Better Than Simpler Alternatives</h2>
<p>This architecture intentionally avoids coupling responsibilities together. Validation, retrieval, prompt construction, model execution, and response handling are all distinct steps. This separation makes the system easier to test, easier to observe, and easier to evolve. When something fails, engineers can identify <em>where</em> and <em>why</em> rather than treating the LLM as a black box.​</p>
<p>Compared to a monolithic "send user input directly to the LLM" approach, this design offers better correctness, lower cost, and higher resilience. It also aligns naturally with distributed tracing, since each block maps cleanly to a trace span with a clear semantic purpose. As the system grows, additional features such as caching, fallback models, or policy enforcement can be added without destabilizing the entire flow.​</p>
<p>Most importantly, this architecture treats the LLM as one component in a larger system, not the system itself. That mindset is essential for building reliable production applications.</p>
<h2 id="heading-llm-models-that-work-best-for-this-architecture">LLM Models That Work Best for This Architecture</h2>
<p>This architecture is model-agnostic, but certain model characteristics work particularly well with retrieval-augmented workflows.</p>
<p>Models with strong instruction-following and reasoning capabilities tend to perform best, especially when prompts include structured context from retrieved documents. General-purpose models such as GPT-4-class systems perform well when accuracy and reasoning depth are critical.</p>
<p>For lower-latency or cost-sensitive use cases, smaller instruction-tuned models can be effective when paired with high-quality retrieval. Open-source models such as LLaMA-derived or Mistral-based systems also fit well into this architecture, particularly when deployed behind a private inference endpoint.​</p>
<p>The key requirement is not the model itself, but how it is used. Models that can reliably ground their responses in provided context, respect system instructions, and produce stable outputs under varying prompts integrate most cleanly into this design. Because retrieval and prompt construction are explicit stages, models can be swapped or compared without changing the overall system structure.</p>
<h2 id="heading-opentelemetry-primer-llm-relevant-concepts-only">OpenTelemetry Primer (LLM-Relevant Concepts Only)</h2>
<p>OpenTelemetry defines three core types of telemetry data: traces, metrics, and logs. For LLM systems, traces are the most important. To make them useful, you need to understand a few building blocks:</p>
<ul>
<li><p>a <strong>trace</strong> represents a single end-to-end request</p>
</li>
<li><p>a <strong>span</strong> is a timed operation within that trace</p>
</li>
<li><p><strong>attributes</strong> are key–value metadata attached to spans</p>
</li>
<li><p><strong>events</strong> are time-stamped annotations</p>
</li>
<li><p><strong>context propagation</strong> ensures child spans attach to the correct parent.</p>
</li>
</ul>
<p>FastAPI’s async nature makes correct context propagation essential, but OpenTelemetry’s Python SDK handles this as long as spans are created correctly.</p>
<p>With those concepts in place, the next step is to wire OpenTelemetry into the app. Start by configuring the OpenTelemetry SDK in FastAPI: define a <code>TracerProvider</code>, attach a <code>Resource</code> (service name and environment), configure an exporter (Jaeger, Tempo, Phoenix, and so on), and enable FastAPI auto-instrumentation.</p>
<h2 id="heading-designing-llm-aware-spans">Designing LLM-Aware Spans</h2>
<h3 id="heading-span-taxonomy">Span Taxonomy</h3>
<p>A clean span hierarchy is critical. In this guide, a single <code>http.request</code> span (usually auto-generated) acts as the root, and it contains child spans such as <code>rag.retrieval</code>, <code>rag.prompt.build</code>, <code>llm.call</code>, <code>llm.postprocess</code>, and, optionally, <code>llm.eval</code>. Each of these spans represents a logical unit of work rather than an implementation detail.</p>
<h3 id="heading-span-boundaries">Span Boundaries</h3>
<p>Getting span boundaries right is just as important as picking the right span names. Avoid extremes like wrapping the entire LLM workflow in one giant span, creating a separate span for every token, or dumping all data into logs.</p>
<p>Instead, aim for a few coarse-grained spans that each represent a meaningful step in the request, enrich them with well-chosen attributes, and use events to mark important milestones within a span rather than splitting everything into smaller spans.</p>
<h3 id="heading-instrumenting-the-llm-call">Instrumenting the LLM Call</h3>
<p>When instrumenting the LLM call, treat it as the most critical span in the trace. Whether you are calling OpenAI, Anthropic, or another provider, start the span immediately before the API request and end it only after the full response (or stream) is complete.</p>
<p>Within that span, capture retries, timeouts, and errors so it becomes the central place for latency analysis, cost attribution, and prompt debugging.</p>
<p>For streaming responses, you can emit events for each chunk to track progress, but avoid creating separate child spans unless you truly need fine-grained timing.</p>
<h2 id="heading-fastapi-example-end-to-end-llm-spans-complete-and-explained">FastAPI Example: End-to-End LLM Spans (Complete and Explained)</h2>
<pre><code class="language-python">from fastapi import FastAPI, Request
from opentelemetry import trace
from opentelemetry.trace import Tracer
from typing import List
import asyncio
import hashlib

# Obtain a tracer instance from OpenTelemetry.
# All spans created with this tracer will be part of the same distributed
# tracing system and exported to the configured backend.
tracer: Tracer = trace.get_tracer(__name__)

# Initialize the FastAPI application.
app = FastAPI()

# Helper functions used by the observable endpoint
async def retrieve_documents(query: str) -&gt; List[str]:
    """
    Simulate document retrieval (e.g., vector search or knowledge base lookup).
    This function represents the retrieval stage in a RAG pipeline.
    In a real system, this might query a vector database or search index.
    """
    await asyncio.sleep(0.05)  # Simulate I/O latency
    return [
        "FastAPI enables high-performance async APIs.",
        "OpenTelemetry provides vendor-neutral observability.",
        "LLM observability requires tracing prompts and tokens.",
    ]


def build_prompt(query: str, documents: List[str]) -&gt; str:
    """
    Construct the final prompt from retrieved documents and the user query.
    Prompt construction is kept separate so it can be observed or modified
    independently if needed (for example, to measure prompt assembly latency).
    """
    context = "\n".join(documents)
    return f"""
Context:
{context}

Question:
{query}
"""


class LLMResponse:
    """
    Minimal abstraction for an LLM response.
    This keeps the example self-contained while still allowing us to attach
    token usage and other metadata for observability.
    """

    def __init__(self, text: str, prompt_tokens: int, completion_tokens: int):
        self.text = text
        self.prompt_tokens = prompt_tokens
        self.completion_tokens = completion_token
    
    @property
    def total_tokens(self) -&gt; int:
        return self.prompt_tokens + self.completion_tokens

async def call_llm(prompt: str) -&gt; LLMResponse:
    """
    Simulate an LLM API call.
    In a real implementation, this would call OpenAI, Anthropic, or another
    provider. The artificial delay represents model latency.
    """
    await asyncio.sleep(0.2)  # Simulate inference time
    response_text = "FastAPI and OpenTelemetry enable end-to-end LLM observability."
    # Token count is approximated here for demonstration purposes.
    prompt_tokens = len(prompt.split())
    completion_tokens = len(response_text.split())
    return LLMResponse(response_text, prompt_tokens, completion_tokens)


def summarize_response(response: LLMResponse) -&gt; str:
    """
    Example post-processing step.
    Post-processing is separated into its own phase so any additional latency
    or errors are not incorrectly attributed to the LLM itself.
    """
    return response.text


# Observable FastAPI endpoint
@app.post("/query")
async def rag_query(request: Request, query: str):
    """
    Handle a single RAG-style request with explicit OpenTelemetry spans.
    This endpoint demonstrates how to create one trace per request, with child
    spans for retrieval, LLM invocation, and post-processing.
    """

    # Create a top-level span for the HTTP request.
    # Even if FastAPI auto-instrumentation is enabled, defining this explicitly
    # allows us to attach domain-specific metadata.
    with tracer.start_as_current_span("http.request") as http_span:
        http_span.set_attribute("http.method", "POST")
        http_span.set_attribute("http.route", "/query")

        # Retrieval phase
        # This span isolates the retrieval step so that relevance issues can be
        # debugged independently of LLM behavior.
        with tracer.start_as_current_span("rag.retrieval") as retrieval_span:
            retrieval_span.set_attribute("rag.top_k", 5)
            retrieval_span.set_attribute("rag.similarity_threshold", 0.8)
            documents = await retrieve_documents(query)

            # Record how many documents were returned.
            # This is a key signal when diagnosing hallucinations
            # or missing context in the final response.
            retrieval_span.set_attribute(
                "rag.documents_returned",
                len(documents),
            )

        # LLM invocation phase
        # This span wraps the actual LLM call and is the primary anchor for
        # latency, cost, and prompt-related analysis.
        with tracer.start_as_current_span("llm.call") as llm_span:
            llm_span.set_attribute("llm.provider", "example")
            llm_span.set_attribute("llm.model", "example-llm")
            llm_span.set_attribute("llm.temperature", 0.7)
            llm_span.set_attribute("llm.prompt_template_id", "rag_v1")

            # Build the final prompt using retrieved context.
            # The raw prompt is intentionally not stored as a span attribute.
            prompt = build_prompt(query, documents)
            
            # Prompt metadata
            prompt_hash = hashlib.sha256(prompt.encode()).hexdigest()
            llm_span.set_attribute("llm.prompt_hash", prompt_hash)
            llm_span.set_attribute("llm.prompt_length", len(prompt))

            response = await call_llm(prompt)

            # Hash the response instead of storing raw text.
            # This allows correlation across traces without exposing content.
            response_hash = hashlib.sha256(
                response.text.encode()
            ).hexdigest()
            llm_span.set_attribute("llm.response_hash", response_hash)

            # Record token usage to enable cost attribution
            # and capacity planning.
            llm_span.set_attribute("llm.usage.prompt_tokens", response.prompt_tokens)
            llm_span.set_attribute("llm.usage.completion_tokens", response.completion_tokens)
            llm_span.set_attribute("llm.usage.total_tokens", response.total_tokens)
            
            # example price per token
            estimated_cost = response.total_tokens * 0.000002
            llm_span.set_attribute("llm.cost_estimated_usd", estimated_cost)

        # Post-processing phase
        # Any transformation after the LLM response is captured here,
        # ensuring inference latency is not overstated.
        with tracer.start_as_current_span("llm.postprocess") as post_span:
            summary = summarize_response(response)
            post_span.set_attribute(
                "llm.summary_length",
                len(summary),
            )

    # Return the final response to the client.
    # All spans above belong to the same distributed trace.
    return {"summary": summary}
</code></pre>
<p>Before examining the full code example, it helps to understand how the instrumentation relates to the observability principles described earlier in this article.</p>
<p>The goal of the example is not simply to show how to create spans, but to demonstrate how a single user request can be represented as a structured trace containing meaningful metadata about each stage of the LLM pipeline.</p>
<p>At a high level, the code follows three key design ideas:</p>
<ol>
<li><p>One trace per user request</p>
</li>
<li><p>One span per logical LLM workflow stage</p>
</li>
<li><p>Semantic attributes attached to spans for debugging, cost tracking, and analysis</p>
</li>
</ol>
<p>Each of these concepts directly corresponds to the observability practices discussed earlier.</p>
<h3 id="heading-top-level-request-span">Top-Level Request Span</h3>
<p>The FastAPI endpoint begins by creating a top-level span called <code>http.request</code>. This span represents the entire lifecycle of the incoming request and serves as the root span for the trace.</p>
<pre><code class="language-python">with tracer.start_as_current_span("http.request") as http_span:
</code></pre>
<p>Although FastAPI can generate HTTP spans automatically through OpenTelemetry auto-instrumentation, explicitly creating this span allows the application to attach domain-specific metadata such as route names or user identifiers.</p>
<p>Attributes such as the HTTP method and route are attached here:</p>
<pre><code class="language-python">http_span.set_attribute("http.method", "POST")
http_span.set_attribute("http.route", "/query")
</code></pre>
<p>This ensures that every trace can be easily filtered by endpoint when analyzing production traffic.</p>
<h3 id="heading-retrieval-span">Retrieval Span</h3>
<p>The next span captures the retrieval phase of the RAG pipeline:</p>
<pre><code class="language-python">with tracer.start_as_current_span("rag.retrieval") as retrieval_span:
</code></pre>
<p>This span isolates the vector search or knowledge retrieval step from the rest of the pipeline. If users report irrelevant answers, engineers can inspect this span to determine whether the issue originates from poor retrieval results rather than model behavior.</p>
<p>Several semantic attributes are attached here:</p>
<ul>
<li><p><code>rag.top_k</code> – number of documents requested</p>
</li>
<li><p><code>rag.similarity_threshold</code> – similarity cutoff used for filtering results</p>
</li>
<li><p><code>rag.documents_returned</code> – number of documents actually retrieved</p>
</li>
</ul>
<p>These attributes align with the RAG observability signals discussed in the earlier section of the article.</p>
<h3 id="heading-llm-invocation-span">LLM Invocation Span</h3>
<p>The most important span in the trace is the <code>llm.call</code> span, which wraps the actual model invocation.</p>
<pre><code class="language-python">with tracer.start_as_current_span("llm.call") as llm_span:
</code></pre>
<p>This span captures the latency, configuration, and token usage associated with the LLM request. In production systems, it becomes the primary location for analyzing model behavior and cost.</p>
<p>Key attributes recorded in this span include:</p>
<ul>
<li><p><code>llm.provider</code> – the model provider (OpenAI, Anthropic, etc.)</p>
</li>
<li><p><code>llm.model</code> – the specific model version</p>
</li>
<li><p><code>llm.temperature</code> – sampling parameter controlling response randomness</p>
</li>
<li><p><code>llm.prompt_template_id</code> – identifier for the prompt template used</p>
</li>
</ul>
<p>These attributes make it possible to correlate changes in model configuration with downstream quality or cost changes.</p>
<h3 id="heading-prompt-handling-and-privacy">Prompt Handling and Privacy</h3>
<p>Instead of storing the full prompt or response text directly in the trace, the example demonstrates a safer practice: hashing sensitive data.</p>
<pre><code class="language-python">response_hash = hashlib.sha256(response.text.encode()).hexdigest()
</code></pre>
<p>The resulting hash is stored as a span attribute:</p>
<pre><code class="language-python">llm_span.set_attribute("llm.response_hash", response_hash)
</code></pre>
<p>This approach allows engineers to correlate repeated responses across traces without exposing potentially sensitive content in observability systems.</p>
<h3 id="heading-token-usage-tracking">Token Usage Tracking</h3>
<p>The <code>llm.call</code> span also records token usage:</p>
<pre><code class="language-python">llm_span.set_attribute(
    "llm.usage.total_tokens",
    response.total_tokens
)
</code></pre>
<p>Capturing token usage at the span level is critical for monitoring cost and efficiency, since token consumption directly determines billing for most LLM providers.</p>
<h3 id="heading-post-processing-span">Post-Processing Span</h3>
<p>Finally, the example includes a <code>llm.postprocess</code> span:</p>
<pre><code class="language-python">with tracer.start_as_current_span("llm.postprocess") as post_span:
</code></pre>
<p>This span represents any transformation applied after the model generates its response. Separating post-processing from the LLM call ensures that additional latency — such as formatting, filtering, or validation — is not incorrectly attributed to the model itself.</p>
<p>An attribute such as response length is recorded here:</p>
<pre><code class="language-python">post_span.set_attribute("llm.summary_length", len(summary))
</code></pre>
<p>This can be useful when diagnosing issues such as unexpectedly short or truncated outputs.</p>
<h3 id="heading-how-the-spans-form-a-complete-trace">How the Spans Form a Complete Trace</h3>
<p>When the request finishes, all spans belong to the same distributed trace:</p>
<pre><code class="language-plaintext">http.request
 ├── rag.retrieval
 ├── llm.call
 └── llm.postprocess
</code></pre>
<p>This hierarchy reflects the logical workflow of a retrieval-augmented LLM system. Because each span contains structured metadata, engineers can quickly answer questions such as:</p>
<ul>
<li><p>Was the latency caused by retrieval or model inference?</p>
</li>
<li><p>How many documents influenced the prompt?</p>
</li>
<li><p>Which model configuration produced the response?</p>
</li>
<li><p>How many tokens were consumed?</p>
</li>
<li><p>Was the response post-processed or truncated?</p>
</li>
</ul>
<p>This structured trace design is what transforms observability from simple monitoring into a practical debugging and optimization tool for LLM systems.</p>
<h2 id="heading-semantic-attributes-best-practices-for-llm-observability">Semantic Attributes: Best Practices for LLM Observability</h2>
<p>The goal is not to capture every possible detail, but to record the minimal set of stable, high-signal attributes that enable effective debugging, cost control, and quality analysis in production. Poor attribute design leads to noisy traces, privacy risks, and dashboards that are impossible to reason about.</p>
<h3 id="heading-prompt-response-and-model-metadata">Prompt, Response, and Model Metadata​</h3>
<p>Storing raw prompts is often unsafe and expensive, so it is better to record minimal, structured metadata instead. In practice, this means attaching a stable template identifier with <code>llm.prompt_template_id</code>, a hashed version of the final prompt using <code>llm.prompt_hash</code> (to avoid storing raw text), and a size indicator such as <code>llm.prompt_length</code>, which captures the number of tokens or characters.</p>
<p>You should also always record key inference parameters: <code>llm.provider</code> (for example, "openai" or "anthropic"), <code>llm.model</code> (for example, "gpt-4.1"), <code>llm.temperature</code> and <code>llm.top_p</code> (sampling parameters), <code>llm.max_tokens</code> (the maximum tokens allowed), and <code>llm.stream</code> to indicate whether streaming was enabled, while staying within your organization’s privacy and compliance requirements.</p>
<pre><code class="language-python">
with tracer.start_as_current_span("llm.call") as llm_span:
            llm_span.set_attribute("llm.provider", "example")
            llm_span.set_attribute("llm.model", "example-llm")
            llm_span.set_attribute("llm.temperature", 0.7)
            llm_span.set_attribute("llm.top_p", 0.9)
            llm_span.set_attribute("llm.max_tokens", 512)
            llm_span.set_attribute("llm.stream", False)
            llm_span.set_attribute("llm.prompt_template_id", "rag_v1")

            # Build the final prompt using retrieved context.
            # The raw prompt is intentionally not stored as a span attribute.
            prompt = build_prompt(query, documents)
            
            # Prompt metadata
            prompt_hash = hashlib.sha256(prompt.encode()).hexdigest()
            llm_span.set_attribute("llm.prompt_hash", prompt_hash)
            llm_span.set_attribute("llm.prompt_length", len(prompt))
</code></pre>
<h3 id="heading-token-usage-and-cost-why-this-matters-in-practice">Token Usage and Cost (Why This Matters in Practice)</h3>
<p>Token usage is one of the most common blind spots in LLM systems. Many teams monitor latency and error rates but discover runaway costs only after invoices spike. Because token consumption varies significantly by prompt structure, retrieved context, and model configuration, it must be captured explicitly at the span level.​</p>
<p>The most important practice is to record token usage at the end of the LLM span, once the model has completed inference. This ensures that the values reflect the full request rather than partial or streamed output.</p>
<p>At minimum, capture the attributes:​<code>llm.usage.prompt_tokens</code> ,<code>llm.usage.completion_tokens</code> and <code>llm.usage.total_tokens</code>​.</p>
<pre><code class="language-python">def __init__(self, text: str, prompt_tokens: int, completion_tokens: int):
        self.text = text
        self.prompt_tokens = prompt_tokens
        self.completion_tokens = completion_token
    
    @property
    def total_tokens(self) -&gt; int:
        return self.prompt_tokens + self.completion_tokens

async def call_llm(prompt: str) -&gt; LLMResponse:
    """
    Simulate an LLM API call.
    In a real implementation, this would call OpenAI, Anthropic, or another
    provider. The artificial delay represents model latency.
    """
    await asyncio.sleep(0.2)  # Simulate inference time
    response_text = "FastAPI and OpenTelemetry enable end-to-end LLM observability."
    # Token count is approximated here for demonstration purposes.
    prompt_tokens = len(prompt.split())
    completion_tokens = len(response_text.split())
    return LLMResponse(response_text, prompt_tokens, completion_tokens)
</code></pre>
<p>These values allow you to distinguish between requests that are expensive because of large prompts (often caused by excessive retrieval or poor prompt construction) versus those that are expensive because of long model-generated outputs.</p>
<p>*Where possible, also attach an estimated cost:*​ <code>llm.cost_estimated_usd</code>​</p>
<pre><code class="language-python">    # example price per token
    estimated_cost = response.total_tokens * 0.000002
    llm_span.set_attribute("llm.cost_estimated_usd", estimated_cost)
</code></pre>
<p>This value is typically derived by multiplying token counts by the model's published pricing. Even if the estimate is approximate, it enables powerful analysis. For example, you can identify which endpoints, prompt templates, or user flows are responsible for the highest cumulative cost, rather than relying on coarse, account-level billing dashboards.</p>
<p>Once spans carry the right attributes, the next step is to connect them to output quality, not just system health.</p>
<h2 id="heading-evaluation-hooks-inside-traces">Evaluation Hooks Inside Traces</h2>
<p>This section describes an additional pattern you can layer on top of the core instrumentation in this guide. It is optional and not implemented in the sample code, but it shows how to attach quality signals directly to your traces.</p>
<p>Observability is not just about whether the system stayed up, it is also about whether the model produced a useful answer. Evaluation hooks inside traces let you attach lightweight quality signals directly to the same spans you use for latency and cost.</p>
<p>Inline evaluations are the simplest approach. You can run quick checks synchronously and record the results as span attributes, such as <code>llm.eval.passed</code> for a simple boolean check, <code>llm.eval.relevance_score</code> for an optional numerical score, or flags like <code>llm.eval.hallucination_detected</code> and <code>llm.eval.refusal_detected</code>. These attributes travel with the trace, so you can filter and aggregate on them in your observability backend just like any other field.</p>
<p>For higher accuracy, you can introduce model-based evaluation as a separate step. In this pattern, an evaluator LLM runs asynchronously on the original prompt and response, and its work is captured in a child span (for example, <code>llm.eval</code>) that shares the same trace ID as the main <code>llm.call</code> span. You then attach scores such as relevance, faithfulness, or toxicity to that evaluation span.</p>
<p>Because the evaluation span shares the same trace ID, you can correlate quality regressions with changes in prompts or retrieval.</p>
<h2 id="heading-exporting-and-visualizing-traces-where-this-fits-with-vendor-tooling">Exporting and Visualizing Traces (Where This Fits with Vendor Tooling)</h2>
<p>This code-first observability design is vendor-agnostic. Once traces are emitted using OpenTelemetry, they can be exported to different backends without changing instrumentation.</p>
<p>General-purpose tracing systems like Jaeger and Grafana Tempo help engineers debug latency, errors, and request flow across retrieval, prompting, and model calls, answering how the system behaved. LLM-focused platforms such as Arize Phoenix use the same data but add model-specific insights like prompt clustering, token analysis, and quality correlation.</p>
<p>Because instrumentation stays OpenTelemetry-native, you maintain full control over attributes and trace structure while still using vendor dashboards, and you can switch backends as your needs evolve without touching the application code.</p>
<h2 id="heading-operational-patterns-and-anti-patterns">Operational Patterns and Anti-Patterns</h2>
<p>Effective LLM observability requires disciplined practices. High-volume systems should sample traces to limit overhead, and prompts or responses should be hashed by default to reduce storage and privacy risk. Traces must be treated as production data, with proper access control and retention policies.</p>
<p>Common pitfalls include relying only on vendor SDK traces, logging prompts without trace correlation, or ignoring evaluation signals. These issues fragment visibility and hide quality regressions, especially when observability focuses only on agents instead of full application context.</p>
<h2 id="heading-extending-the-system">Extending the System</h2>
<p>Once traces are reliable, they support advanced capabilities. Metrics like p95 latency can be derived from spans, logs can be linked using trace IDs, and historical traces can power offline evaluation or prompt testing.​</p>
<p>By following OpenTelemetry conventions, the observability stack also stays aligned with emerging LLM semantic standards, keeping the system flexible and future-proof.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>End-to-end LLM observability is not achieved by installing another agent. It is achieved through intentional span design, meaningful semantic attributes, and, where needed, lightweight evaluation hooks.​</p>
<p>By treating LLM calls as first-class operations within distributed traces, you gain faster debugging, controlled costs, safer deployments, and measurable quality improvements. The backend — Jaeger, Tempo, Phoenix — is interchangeable. The instrumentation strategy is not.​</p>
<p>A well-designed trace is the most valuable artifact in a production LLM system.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Debug Kubernetes Apps When Logs Fail You – An eBPF Tracing Handbook ]]>
                </title>
                <description>
                    <![CDATA[ Let’s say your Kubernetes pod crashes at 3am and the logs show nothing useful. By the time you SSH into the node, the container is gone, and you're left guessing what happened in those final moments. This is the reality of debugging modern applicatio... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-debug-kubernetes-apps-when-logs-fail-you-an-ebpf-tracing-handbook/</link>
                <guid isPermaLink="false">694190c566a5d5cb99995f9f</guid>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ eBPF ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ inspektor gadget ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Opaluwa Emidowojo ]]>
                </dc:creator>
                <pubDate>Tue, 16 Dec 2025 17:03:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765899860869/3eadf316-8539-4624-afba-1d4190b6c62a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Let’s say your Kubernetes pod crashes at 3am and the logs show nothing useful. By the time you SSH into the node, the container is gone, and you're left guessing what happened in those final moments.</p>
<p>This is the reality of debugging modern applications. Traditional monitoring wasn't built for containers that live for seconds, services that shift across nodes, or network paths that change constantly.</p>
<p>eBPF changes this. It lets you see <em>inside</em> the kernel itself, watching every system call, every network packet, and every process execution – without modifying a single line of code.</p>
<p>In this tutorial, you will trace a real Kubernetes application using eBPF-powered tools. You’ll learn fundamentals that apply across the entire modern observability ecosystem, with gadgets from the Inspektor Gadget ecosystem.</p>
<p>By the end, you’ll be able to:</p>
<ul>
<li><p>Trace requests as they move through your Kubernetes pods</p>
</li>
<li><p>Observe behavior at the kernel and syscall level</p>
</li>
<li><p>Debug failures that logs and metrics simply can’t explain</p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p><strong>Knowledge requirements:</strong></p>
<ul>
<li><p>Basic Kubernetes concepts: pods, deployments, services, namespaces</p>
</li>
<li><p>Familiarity with kubectl: <code>get</code>, <code>describe</code>, <code>logs</code>, <code>exec</code></p>
</li>
<li><p>Container basics</p>
</li>
<li><p>Basic Linux concepts: processes, system calls</p>
</li>
</ul>
<p><strong>Technical requirements:</strong></p>
<ul>
<li><p>Kubernetes cluster (local or cloud-based)</p>
</li>
<li><p><code>kubectl</code> installed and configured</p>
</li>
<li><p>Cluster admin permissions</p>
</li>
<li><p>Linux kernel 5.10+ (most managed services have this)</p>
</li>
</ul>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-understanding-ebpf-observability">Understanding eBPF Observability</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-ebpf-tracing-works-without-getting-lost-in-the-kernel">How eBPF Tracing Works (Without Getting Lost in the Kernel)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-your-environment">How to Set Up Your Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-trace-your-first-request-hands-on-tutorial">How to Trace Your First Request: Hands-On Tutorial</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-interpret-traces">How to Interpret Traces</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-debugging-scenarios">Real-World Debugging Scenarios</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-advanced-tracing-insights">Advanced Tracing Insights</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-and-production-considerations">Best Practices and Production Considerations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-next-steps-and-resources">Next Steps and Resources</a></p>
</li>
</ul>
<h2 id="heading-understanding-ebpf-observability">Understanding eBPF Observability</h2>
<p>eBPF (extended Berkeley Packet Filter) is a technology that allows you to run custom programs inside the Linux kernel without changing kernel code or loading kernel modules.</p>
<p>The Linux kernel is the control center of your operating system. Historically, if you wanted to observe low-level activity (like network packets, system calls, or file operations), you had to rely on kernel changes or kernel modules. Both approaches were fragile, difficult to maintain, and carried real stability and security risks.</p>
<p>eBPF shifts how we approach observability. It provides a safe, sandboxed environment where you can run observability programs directly in the kernel with built-in safety checks that prevent crashes or security vulnerabilities.</p>
<h3 id="heading-why-does-this-matter-for-observability">Why does this matter for observability?</h3>
<p>In traditional observability, you instrument your application code. You add logging statements, metrics libraries, and tracing SDKs. This works, but has significant limitations:</p>
<ul>
<li><p><strong>Code changes are required</strong>: You must modify and redeploy applications</p>
</li>
<li><p><strong>It’s language-specific</strong>: Different languages need different libraries</p>
</li>
<li><p><strong>There will likely be blind spots</strong>: You can only see what you explicitly instrument</p>
</li>
<li><p><strong>The overhead</strong>: Heavy instrumentation slows down applications</p>
</li>
<li><p><strong>Container challenges</strong>: By the time you add instrumentation and redeploy, the problem may have disappeared</p>
</li>
</ul>
<p>eBPF takes a different approach. Instead of instrumenting applications, you instrument the kernel. Since every application ultimately makes system calls to the kernel for network I/O, file operations, and process management, you can observe everything from one vantage point.</p>
<h3 id="heading-the-ebpf-advantage-for-kubernetes">The eBPF advantage for Kubernetes</h3>
<p>Kubernetes adds another layer of complexity. Your application might be spread across multiple containers, pods, and nodes. Traditional APM (Application Performance Monitoring) tools struggle here because containers come and go rapidly, network topology changes constantly, service meshes add routing complexity, and you often don't control application code (think third-party services or legacy applications you can't modify.)</p>
<p>eBPF doesn't care about any of this. It sees all activity at the kernel level, regardless of what language your app is written in, whether it's containerized, how many times the pod has been rescheduled, or whether you have access to modify the source code. This universal visibility is why the Cloud Native Computing Foundation (CNCF) and major cloud providers are betting heavily on eBPF for the future of observability.</p>
<h2 id="heading-how-ebpf-tracing-works-without-getting-lost-in-the-kernel">How eBPF Tracing Works (Without Getting Lost in the Kernel)</h2>
<p>When your application runs on Kubernetes, there's a clear separation between user space and kernel space. Your code runs in user space, where it's isolated, safe, and has limited access to system resources. To do anything useful – make network calls, read files, allocate memory – your application must ask the kernel for help. The kernel handles these requests via system calls, commonly called syscalls.</p>
<p>eBPF lets us hook into these syscalls without slowing the system down. It’s like having a CCTV camera at every doorway between user space and kernel space, watching who passes through, when, and what they’re carrying.</p>
<h3 id="heading-a-simple-example-http-request-tracing">A Simple Example: HTTP Request Tracing</h3>
<p>Your application initiates an HTTP GET request, which needs to go through the network stack. To establish a connection, your application first makes a <code>socket()</code> system call to create a network socket. Then it calls <code>connect()</code> to establish a connection to the remote server. Once connected, it uses <code>send()</code> to transmit the HTTP request. Network packets are sent across the wire, and eventually your application calls <code>recv()</code> to receive the response.</p>
<p>With eBPF tools like Inspektor Gadget's Traceloop, you can automatically hook into these syscalls. The eBPF program captures request metadata including source and destination IPs, ports, timing information, and payload sizes. You get a complete trace of the request without touching your application code.</p>
<h3 id="heading-the-ebpf-execution-flow">The eBPF Execution Flow</h3>
<p>Here's what happens under the hood when you run a trace. When you deploy Inspektor Gadget and run a gadget, several things happen behind the scenes. Once deployed, the eBPF program springs into action whenever a traced event occurs.</p>
<p>When your application makes a syscall, the eBPF hook triggers and quickly collects relevant data: timestamps, process IDs, container IDs, pod names, request details, and latency information. This data is sent to user space through eBPF maps, which are efficient data structures for kernel-to-userspace communication.</p>
<p>Inspektor Gadget adds Kubernetes context to raw kernel data. Instead of seeing only process IDs, you can see pod names, namespaces, labels, and other metadata. For example, you can tell that a request originated from the frontend pod in the production namespace and targeted the backend service.</p>
<p>The gadget then presents this information in a format that's immediately useful, whether you're using the CLI or integrating with other observability tools.</p>
<p>eBPF is fast because:</p>
<ul>
<li><p><strong>JIT compilation</strong>: Programs are turned into native machine code for maximum performance</p>
</li>
<li><p><strong>Event-driven</strong>: Only execute when relevant events occur, not continuously polling</p>
</li>
<li><p><strong>Kernel-resident</strong>: No expensive context switching between kernel and user space</p>
</li>
<li><p><strong>Highly optimized</strong>: Typically adds less than 5% overhead even under heavy load</p>
</li>
</ul>
<h3 id="heading-the-tool-inspektor-gadget-amp-traceloop">The Tool: Inspektor Gadget &amp; Traceloop</h3>
<p>For this tutorial, we're using Traceloop, an eBPF-based tool that traces request flows through applications by observing syscalls, network calls, and I/O operations at the kernel level.</p>
<p>Why are we using Traceloop for this tutorial?</p>
<ul>
<li><p>It’s quick to install and run (one command)</p>
</li>
<li><p>The output maps directly to the application’s behavior</p>
</li>
<li><p>It automatically adds Kubernetes context (pod names, namespaces)</p>
</li>
<li><p>You don’t need to make any application code changes</p>
</li>
</ul>
<p>What you'll learn applies beyond Traceloop. All eBPF tracing tools (Pixie, Cilium Hubble, Tetragon) work the same way under the hood. They attach to kernel hooks and collect event data. Once you understand the concepts here, you can use any eBPF observability tool effectively.</p>
<h2 id="heading-how-to-set-up-your-environment">How to Set Up Your Environment</h2>
<p>To get your environment ready for hands-on tracing, we'll verify that your cluster meets the requirements, install Inspektor Gadget, and deploy a sample application to trace.</p>
<h3 id="heading-verify-that-your-cluster-meets-the-requirements">Verify that Your Cluster Meets the Requirements</h3>
<p>Before installing anything, confirm that your Kubernetes cluster is ready for eBPF.</p>
<h4 id="heading-check-your-kubernetes-version">Check your Kubernetes version:</h4>
<pre><code class="lang-bash">kubectl version --short
</code></pre>
<p>You need Kubernetes 1.19 or later. Most modern clusters exceed this requirement, but it's worth verifying.</p>
<h4 id="heading-verify-kernel-version-on-your-nodes">Verify kernel version on your nodes:</h4>
<pre><code class="lang-bash">kubectl get nodes -o wide
</code></pre>
<p>Then check the kernel version on one of your nodes:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># If using a local cluster like minikube or kind</span>
uname -r

<span class="hljs-comment"># For cloud clusters, you might need to check node details</span>
kubectl debug node/&lt;node-name&gt; -it --image=ubuntu -- bash -c <span class="hljs-string">"uname -r"</span>
</code></pre>
<p>You need Linux kernel 5.10 or later for the best eBPF support. Kernel 4.18+ works but with some limitations. If you're using a managed Kubernetes service (GKE, EKS, AKS), you almost certainly have a compatible kernel.</p>
<h4 id="heading-confirm-that-you-have-cluster-admin-permissions">Confirm that you have cluster admin permissions:</h4>
<pre><code class="lang-bash">kubectl auth can-i create deployments --all-namespaces
</code></pre>
<p>This should return "yes". Inspektor Gadget needs elevated permissions to load eBPF programs into the kernel.</p>
<h3 id="heading-install-inspektor-gadget">Install Inspektor Gadget</h3>
<p>You can install Inspektor Gadget in several ways. We'll use the kubectl plugin method as it's the most straightforward for learning.</p>
<h4 id="heading-install-the-kubectl-gadget-plugin">Install the kubectl gadget plugin:</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># Download and install kubectl-gadget</span>
kubectl krew install gadget

<span class="hljs-comment"># Verify installation</span>
kubectl gadget version
</code></pre>
<p>If you don't have krew (the kubectl plugin manager), you can install it first:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install krew</span>
(
  <span class="hljs-built_in">set</span> -x; <span class="hljs-built_in">cd</span> <span class="hljs-string">"<span class="hljs-subst">$(mktemp -d)</span>"</span> &amp;&amp;
  OS=<span class="hljs-string">"<span class="hljs-subst">$(uname | tr '[:upper:]' '[:lower:]')</span>"</span> &amp;&amp;
  ARCH=<span class="hljs-string">"<span class="hljs-subst">$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')</span>"</span> &amp;&amp;
  KREW=<span class="hljs-string">"krew-<span class="hljs-variable">${OS}</span>_<span class="hljs-variable">${ARCH}</span>"</span> &amp;&amp;
  curl -fsSLO <span class="hljs-string">"https://github.com/kubernetes-sigs/krew/releases/latest/download/<span class="hljs-variable">${KREW}</span>.tar.gz"</span> &amp;&amp;
  tar zxvf <span class="hljs-string">"<span class="hljs-variable">${KREW}</span>.tar.gz"</span> &amp;&amp;
  ./<span class="hljs-string">"<span class="hljs-variable">${KREW}</span>"</span> install krew
)

<span class="hljs-comment"># Add krew to your PATH</span>
<span class="hljs-built_in">export</span> PATH=<span class="hljs-string">"<span class="hljs-variable">${KREW_ROOT:-<span class="hljs-variable">$HOME</span>/.krew}</span>/bin:<span class="hljs-variable">$PATH</span>"</span>
</code></pre>
<h4 id="heading-deploy-inspektor-gadget-to-your-cluster">Deploy Inspektor Gadget to your cluster:</h4>
<pre><code class="lang-bash">kubectl gadget deploy
</code></pre>
<p>This creates a <code>gadget</code> namespace and deploys the Inspektor Gadget daemon as a DaemonSet, ensuring each node in your cluster can run eBPF programs.</p>
<h4 id="heading-verify-the-deployment">Verify the deployment:</h4>
<pre><code class="lang-bash">kubectl get pods -n gadget
</code></pre>
<p>You should see one <code>gadget-*</code> pod per node, all in the <code>Running</code> state. If a pod is stuck in <code>Pending</code> or <code>CrashLoopBackOff</code>, check that your kernel meets the version requirements.</p>
<h4 id="heading-deploying-a-sample-application">Deploying a sample application</h4>
<p>To learn tracing effectively, we need an application that does something interesting. We'll deploy a simple microservices application with multiple components so you can see traces flowing across service boundaries.</p>
<p>Start by creating a namespace for our demo app:</p>
<pre><code class="lang-bash">kubectl create namespace demo-app
</code></pre>
<p>Then deploy a simple web application with a backend:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">frontend</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">frontend</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">gcr.io/google-samples/microservices-demo/frontend:v0.8.0</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8080</span>
        <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">PORT</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">"8080"</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">PRODUCT_CATALOG_SERVICE_ADDR</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">"productcatalog:3550"</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">LoadBalancer</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
    <span class="hljs-attr">targetPort:</span> <span class="hljs-number">8080</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">productcatalog</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">productcatalog</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">productcatalog</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">server</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">gcr.io/google-samples/microservices-demo/productcatalogservice:v0.8.0</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">3550</span>
        <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">PORT</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">"3550"</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">productcatalog</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">demo-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">productcatalog</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">3550</span>
    <span class="hljs-attr">targetPort:</span> <span class="hljs-number">3550</span>
</code></pre>
<p>Apply the configuration:</p>
<pre><code class="lang-bash">kubectl apply -f demo-app.yaml
</code></pre>
<p>And wait for pods to be ready:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=ready pod -l app=frontend -n demo-app --timeout=300s
kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=ready pod -l app=productcatalog -n demo-app --timeout=300s
</code></pre>
<p>Then just verify that everything is running:</p>
<pre><code class="lang-bash">kubectl get pods -n demo-app
</code></pre>
<p>You should see both <code>frontend</code> and <code>productcatalog</code> pods in the <code>Running</code> state.</p>
<p>Now you’ll need to get the frontend URL:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># For local clusters (minikube, kind, Docker Desktop)</span>
kubectl port-forward -n demo-app service/frontend 8080:80

<span class="hljs-comment"># Then access http://localhost:8080 in your browser</span>

<span class="hljs-comment"># For cloud clusters</span>
kubectl get service frontend -n demo-app
<span class="hljs-comment"># Look for the EXTERNAL-IP</span>
</code></pre>
<p>Visit the application in your browser to confirm it's working. You should see a simple e-commerce storefront. This application makes HTTP requests from the frontend to the product catalog service, which is perfect for tracing.</p>
<h2 id="heading-how-to-trace-your-first-request-hands-on-tutorial">How to Trace Your First Request: Hands-On Tutorial</h2>
<p>Now that everything is set up, let's capture our first trace and see eBPF observability in action.</p>
<h3 id="heading-generate-the-traffic-to-trace">Generate the Traffic to Trace</h3>
<p>First, we need some application activity to observe. We will generate a few requests for our demo application.</p>
<p>In one terminal, start the Traceloop gadget:</p>
<pre><code class="lang-bash">kubectl gadget traceloop -n demo-app
</code></pre>
<p>This command starts tracing HTTP request handling in the <code>demo-app</code> namespace. Inspektor Gadget monitors the kernel to capture the function calls and system events that occur while processing each request.  </p>
<p>In another terminal, generate some traffic:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># If using port-forward</span>
curl http://localhost:8080

<span class="hljs-comment"># If you have an external IP</span>
curl http://&lt;EXTERNAL-IP&gt;

<span class="hljs-comment"># Generate multiple requests</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..10}; <span class="hljs-keyword">do</span> curl http://localhost:8080; sleep 1; <span class="hljs-keyword">done</span>
```

<span class="hljs-comment">### Viewing Your First Trace</span>

Switch back to the terminal running the trace loop gadget. You should see output appearing as requests flow through your application. The output will look something like this:
```
NODE         NAMESPACE   POD              CONTAINER    PID    TYPE       COUNT  
minikube     demo-app    frontend-abc123  frontend     1234   loop       1      
minikube     demo-app    frontend-abc123  frontend     1234   loop       2
</code></pre>
<p>Each line shows a traced execution flow, with the count increasing as the same pattern is observed again.</p>
<p>We can make the output more interesting by filtering:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Stop the previous trace with Ctrl+C, then run:</span>
kubectl gadget traceloop -n demo-app --podname frontend
</code></pre>
<p>This narrows our observation to just the frontend pod, reducing noise and making patterns clearer.</p>
<h4 id="heading-understanding-what-youre-seeing">Understanding what you're seeing:</h4>
<p>Each column shows different information about your application:</p>
<ul>
<li><p><strong>NODE</strong>: Which Kubernetes node the traced event occurred on. In multi-node clusters, this helps you understand workload distribution and identify node-specific issues.</p>
</li>
<li><p><strong>NAMESPACE</strong>: The Kubernetes namespace. We filtered to <code>demo-app</code>, so you'll only see that namespace. In production, filtering by namespace is crucial for focusing on specific applications.</p>
</li>
<li><p><strong>POD</strong>: The specific pod where the event occurred. Each pod gets a unique name (like <code>frontend-abc123</code>), allowing you to distinguish between replicas of the same application.</p>
</li>
<li><p><strong>CONTAINER</strong>: Which container within the pod. Pods can have multiple containers (main application, sidecars, init containers), so this helps you pinpoint exactly where activity is happening.</p>
</li>
<li><p><strong>PID</strong>: The process ID inside the container. This is the actual Linux process that made the syscalls eBPF observed. Multiple PIDs might appear if your application uses multiple processes or threads.</p>
</li>
<li><p><strong>TYPE</strong>: The type of event traced. For Traceloop, this identifies kernel-level patterns detected during request processing.</p>
</li>
<li><p><strong>COUNT</strong>: How many times this pattern has been observed. A rapidly incrementing count indicates high request volume.</p>
</li>
</ul>
<h4 id="heading-what-this-tells-you-about-your-application">What this tells you about your application:</h4>
<p>Even from this simple output, you can derive insights. If you see events appearing for the <code>frontend</code> pod but not the <code>productcatalog</code> pod, it might indicate that requests aren't making it to the backend. This is a potential configuration issue. If the <code>COUNT</code> increases rapidly for one pod but not others, you know which replica is receiving traffic, useful for debugging load balancing issues.</p>
<p>The real power becomes clear when you correlate these kernel-level observations with what you know about your application. When you made 10 curl requests, you should see corresponding activity in the trace output. This direct relationship between application behavior and kernel observations is the foundation of eBPF observability.</p>
<h2 id="heading-how-to-interpret-traces">How to Interpret Traces</h2>
<p>Understanding raw trace output is valuable, but interpreting what it means for your application's health and performance is where the real skill lies.</p>
<h3 id="heading-trace-anatomy-spans-timing-and-request-flow">Trace Anatomy: Spans, Timing, and Request Flow</h3>
<p>A trace represents a single request's journey through your system. When you curl the frontend, that generates one trace. A span represents a single operation within that trace like "frontend handles request," "frontend calls product catalog," "product catalog queries data," and "frontend returns response." Each span has timing information: when it started, when it ended, and therefore how long it took.</p>
<p>In traditional distributed tracing with OpenTelemetry or Jaeger, you'd explicitly create these spans in your application code. With eBPF, the tool infers spans from syscall patterns. When eBPF sees your frontend process call <code>connect()</code> to the product catalog's IP, followed by <code>send()</code> and <code>recv()</code>, it understands that's a span representing an HTTP request to the backend service.</p>
<p>The request flow is the sequence of spans showing how your request moved through services. In our demo app,</p>
<ol>
<li><p>The user request arrives at the frontend,</p>
</li>
<li><p>the frontend connects to the product catalog,</p>
</li>
<li><p>the product catalog processes the request,</p>
</li>
<li><p>the product catalog returns the data, the frontend renders the page,</p>
</li>
<li><p>and finally, the response is sent to user.</p>
</li>
</ol>
<h3 id="heading-how-to-follow-requests-across-services">How to Follow Requests Across Services</h3>
<p>Let's trace a request across service boundaries to see this flow in action.</p>
<p>First, we’ll start a more detailed trace:</p>
<pre><code class="lang-bash">kubectl gadget trace_tcp -n demo-app
</code></pre>
<p>The trace_tcp gadget shows network connections, giving us visibility into service-to-service communication.</p>
<p>Next, generate a request:</p>
<pre><code class="lang-bash">curl http://localhost:8080
</code></pre>
<p>In the trace output, look for connection patterns:</p>
<p>You should see the frontend pod establishing a TCP connection to the product catalog service. The trace will show the source (frontend) and destination (product catalog) IPs and ports, along with timing information.</p>
<p>This is how eBPF lets you follow requests: by observing the network syscalls that implement service communication. You don't need a service mesh or instrumentation libraries, the kernel sees all network activity and eBPF captures it.</p>
<h4 id="heading-understanding-the-flow">Understanding the flow:</h4>
<ol>
<li><p>Your curl command triggers a TCP connection to the frontend pod's IP on port 8080</p>
</li>
<li><p>The frontend processes the request and opens a TCP connection to the product catalog's IP on port 3550</p>
</li>
<li><p>Data flows back and forth (you'll see send/receive events)</p>
</li>
<li><p>Connections close when requests complete</p>
</li>
</ol>
<p>Each step is visible to eBPF because each step requires syscalls that the kernel handles.</p>
<h3 id="heading-how-to-identify-bottlenecks-and-errors">How to Identify Bottlenecks and Errors</h3>
<p>We can also use tracing to identify performance issues.</p>
<p>First, let’s start by simulating a slow backend:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create a deliberately slow endpoint by modifying our deployment</span>
kubectl scale deployment productcatalog -n demo-app --replicas=0

<span class="hljs-comment"># Wait a moment, then scale back up</span>
kubectl scale deployment productcatalog -n demo-app --replicas=1
</code></pre>
<p>While the product catalog is down, generate some requests:</p>
<pre><code class="lang-bash"><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..5}; <span class="hljs-keyword">do</span> curl http://localhost:8080; <span class="hljs-keyword">done</span>
</code></pre>
<p>You should see connection attempts from the frontend to the product catalog, but if the service is unavailable, you'll see different patterns, possibly connection timeouts or connection refused errors, depending on the exact timing.</p>
<p>What bottlenecks look like in traces:</p>
<ul>
<li><p><strong>Long spans</strong>: A span that takes significantly longer than others indicates a bottleneck. In trace loop output, you might see gaps between events or notice certain operations taking longer.</p>
</li>
<li><p><strong>Retries</strong>: Repeated connection attempts to the same destination suggest a failing or slow service.</p>
</li>
<li><p><strong>Error patterns</strong>: Connection failures, timeouts, or unusual syscall sequences indicate problems.</p>
</li>
</ul>
<p>The best skill to have is pattern recognition. A typical, healthy request flow has a rhythm, and events occur in predictable sequences with consistent timing. When something breaks, the rhythm changes. Requests take longer, errors appear, or expected events don't occur at all.</p>
<h2 id="heading-real-world-debugging-scenarios">Real-World Debugging Scenarios</h2>
<p>Now let's go through three realistic scenarios where eBPF helps:</p>
<h3 id="heading-scenario-1-finding-a-slow-endpoint">Scenario 1: Finding a Slow Endpoint</h3>
<p><strong>The problem:</strong> Users report that the product catalog page sometimes loads very slowly, but metrics show normal average latency.</p>
<p>Let’s use Traceloop to investigate:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Start tracing with timing information</span>
kubectl gadget traceloop -n demo-app --podname frontend
</code></pre>
<p>We’ll generate some mixed traffic:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Some requests to the homepage (fast)</span>
curl http://localhost:8080

<span class="hljs-comment"># Some requests to the product catalog (potentially slow)</span>
curl http://localhost:8080/products
</code></pre>
<p>In the trace output, compare the <code>COUNT</code> increments for different request patterns. If certain patterns show significantly more loop iterations or longer gaps between events, that indicates those requests are doing more work, possibly hitting a slow endpoint.</p>
<h4 id="heading-the-diagnosis">The diagnosis:</h4>
<p>You might notice that requests to <code>/products</code> cause the frontend to make multiple calls to the product catalog service (visible with <code>kubectl gadget trace_tcp</code>), while homepage requests don't. This explains why the product page is slow: it's making synchronous calls to a backend service, and if that service is slow or the network is congested, users feel the delay.</p>
<h4 id="heading-the-fix">The fix:</h4>
<p>You might implement caching, make the backend calls asynchronous, or optimize the product catalog service itself. The key is that eBPF helped you identify which specific code path was slow without adding instrumentation to your application.</p>
<h3 id="heading-scenario-2-tracking-down-failed-requests">Scenario 2: Tracking Down Failed Requests</h3>
<p><strong>The problem:</strong> Your monitoring shows a 5% error rate, but application logs don't show any errors. Where are the failures happening?</p>
<p>Now let’s use eBPF to investigate:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Trace network connections to see connection failures</span>
kubectl gadget trace_tcp -n demo-app
</code></pre>
<p>We’ll simulate intermittent failures:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create a failing scenario by temporarily breaking service connectivity</span>
kubectl delete service productcatalog -n demo-app

<span class="hljs-comment"># Generate requests</span>
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> {1..10}; <span class="hljs-keyword">do</span> curl http://localhost:8080; sleep 1; <span class="hljs-keyword">done</span>

<span class="hljs-comment"># Restore the service</span>
kubectl apply -f demo-app.yaml
</code></pre>
<p>In the TCP trace, you'll see connection attempts from the frontend to the product catalog that fail or time out. The trace will show the source, destination, and what happened (connection refused, timeout, and so on).</p>
<h4 id="heading-the-diagnosis-1">The diagnosis:</h4>
<p>The failures are happening at the network level, the frontend can't reach the product catalog. This might be due to network policy issues, service mesh misconfiguration, or DNS problems. Traditional application logs might not capture this because the application never receives a response to log, and the connection fails before the application layer even gets involved.</p>
<h4 id="heading-why-ebpf-finds-this-when-logs-dont">Why eBPF finds this when logs don't:</h4>
<p>Your application logs what it experiences. If a connection fails at the TCP level, your application might just see "connection refused" and retry without detailed logging.</p>
<p>eBPF sees the actual syscalls and network events, giving you visibility into what's happening beneath your application layer.</p>
<h3 id="heading-scenario-3-understanding-service-dependencies">Scenario 3: Understanding Service Dependencies</h3>
<p><strong>The problem:</strong> You're not sure which services depend on each other, and you want to understand the actual runtime dependencies before making changes.</p>
<p>We’ll use eBPF to map dependencies:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Trace all TCP connections to see who talks to whom</span>
kubectl gadget trace_tcp -n demo-app
</code></pre>
<p>And then generate normal traffic:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Make various requests to exercise different code paths</span>
curl http://localhost:8080
curl http://localhost:8080/products
curl http://localhost:8080/cart
</code></pre>
<p>The trace output shows source and destination for every connection. Build a mental (or actual) map of which pods connect to which services.</p>
<h4 id="heading-the-discovery">The discovery:</h4>
<p>You'll see that the frontend pod connects to the product catalog service, but you might also discover unexpected dependencies. Perhaps the frontend also makes calls to a Redis cache, an authentication service, or external APIs. These runtime dependencies might not be documented or might differ from what architectural diagrams show.</p>
<h4 id="heading-why-this-matters">Why this matters:</h4>
<p>Before deploying a change to the product catalog service, you now know exactly which services will be affected. Before implementing a network policy, you know which connections to allow. Before decomposing a monolith, you understand the actual communication patterns.</p>
<p>This is observability-driven architecture understanding: letting the system show you how it actually works, not how you think it works.</p>
<h2 id="heading-advanced-tracing-insights">Advanced Tracing Insights</h2>
<p>Once you're comfortable with basic request tracing, Inspektor Gadget offers deeper observability capabilities that reveal even more about your system's behavior.</p>
<h3 id="heading-syscall-level-observation">Syscall-Level Observation</h3>
<p>The traceloop and trace_tcp gadgets give you application-level insights, but sometimes you need to go deeper. The trace_exec gadget shows you every process execution in your containers.</p>
<p>First, let’s monitor process execution:</p>
<pre><code class="lang-bash">kubectl gadget trace_exec -n demo-app
</code></pre>
<p>And generate activity:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Exec into a pod and run commands</span>
kubectl <span class="hljs-built_in">exec</span> -it -n demo-app deployment/frontend -- /bin/sh
ls -la
ps aux
<span class="hljs-built_in">exit</span>
</code></pre>
<p>Every command you run inside the container appears in the trace: <code>/bin/sh</code>, <code>ls</code>, <code>ps</code>, and anything else. This helps you understand what's running in your containers, detect suspicious activity, or debug initialization issues.</p>
<p>In production scenarios, this helps you answer questions like: Is my application spawning unexpected subprocesses? Are there security issues like someone running <code>curl</code> to download malicious scripts? Is my <code>init</code> script actually running the commands I think it is?</p>
<h3 id="heading-network-tracing-insights">Network Tracing Insights</h3>
<p>Beyond TCP connections, you can trace DNS queries, which often reveal surprising things about your application's behavior.</p>
<p>Run <code>trace_dns</code>:</p>
<pre><code class="lang-bash">kubectl gadget trace_dns -n demo-app
</code></pre>
<p>Generate requests:</p>
<pre><code class="lang-bash">curl http://localhost:8080
</code></pre>
<p>You'll see every DNS query your application makes: resolving service names, checking for external APIs, perhaps even unexpected queries that indicate misconfiguration or dependencies you didn't know about.</p>
<p>Common insights from DNS tracing include discovering that your application is using external dependencies you didn't document, finding DNS resolution failures that cause intermittent errors, or identifying excessive DNS queries that could be cached.</p>
<h3 id="heading-combining-ebpf-data-with-logs-and-metrics">Combining eBPF Data with Logs and Metrics</h3>
<p>eBPF observability delivers the best results when combined with traditional observability signals. To combine them effectively:</p>
<ul>
<li><p>Use metrics for high-level health monitoring, alerting on anomalies, tracking trends over time, and dashboard visualization.</p>
</li>
<li><p>Use logs for application-specific context, business logic details, error messages with stack traces, and debugging application code.</p>
</li>
<li><p>Use eBPF traces for understanding request flows, identifying where time is spent, discovering runtime dependencies, and debugging issues that don't appear in logs.</p>
</li>
</ul>
<h4 id="heading-a-practical-workflow">A practical workflow:</h4>
<p>Your metrics alert you that latency increased. You check logs but don't see errors, requests are succeeding, just slowly. You use eBPF tracing to identify that requests are spending extra time in network I/O to a particular backend service. Now you check that service's metrics and logs, and discover it's under heavy load. The eBPF trace gave you the clue that logs and metrics alone couldn't provide.</p>
<p>This approach to observability, using the right tool for each question, is how experienced engineers debug complex systems efficiently.</p>
<h3 id="heading-what-ebpf-can-and-cant-see"><strong>What eBPF Can and Can't See</strong></h3>
<p>eBPF excels at:</p>
<ul>
<li><p>Network traffic (requests, responses, latency)</p>
</li>
<li><p>System calls (file I/O, process creation, memory allocation)</p>
</li>
<li><p>Kernel functions (scheduling, locking, resource usage)</p>
</li>
<li><p>Function calls in binaries (with uprobes)</p>
</li>
</ul>
<p>But keep in mind that eBPF has limitations:</p>
<ul>
<li><p>Cannot decrypt encrypted payloads (unless hooking SSL libraries before encryption)</p>
</li>
<li><p>Doesn't automatically understand application logic</p>
</li>
<li><p>Captures low-level events but may need context for high-level semantics</p>
</li>
</ul>
<p>That's why eBPF complements traditional observability rather than replacing it entirely. It gives you infrastructure-level visibility with no code changes and universal coverage. Traditional APM provides application-level context, business metrics, and custom instrumentation. Together, they give you complete observability across your entire stack.</p>
<h2 id="heading-best-practices-and-production-considerations">Best Practices and Production Considerations</h2>
<p>Before using eBPF tracing in production, there are important considerations around performance, security, and operational practices.</p>
<h3 id="heading-performance-impact">Performance Impact</h3>
<p>eBPF's reputation for low overhead is well-deserved, but "low" isn't "zero."</p>
<p>Most eBPF tracing tools add 2-5% CPU overhead and negligible memory overhead. The exact number depends on event frequency, tracing a service that handles 10,000 requests per second will have more overhead than one handling 10 per second.</p>
<p>Measuring the impact:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Before enabling tracing, check baseline resource usage</span>
kubectl top pods -n demo-app

<span class="hljs-comment"># Enable tracing</span>
kubectl gadget traceloop -n demo-app

<span class="hljs-comment"># Check resource usage again</span>
kubectl top pods -n demo-app
</code></pre>
<p>You should see a small increase in CPU usage in the pods where tracing is active. This is the cost of the eBPF programs running in the kernel and processing events.</p>
<h4 id="heading-production-best-practices">Production best practices:</h4>
<p>Use targeted tracing rather than tracing everything everywhere. Trace specific namespaces, pods, or individual containers when investigating issues. For high-volume services, reduce overhead by applying filters, aggregation, or sampling where supported by the tracing tool.</p>
<p>Stop tracing when you’re done investigating. Unlike metrics collection, which typically runs continuously, eBPF-based tracing is best used as an on-demand diagnostic tool to capture detailed insights during active debugging.</p>
<h4 id="heading-when-overhead-matters">When overhead matters:</h4>
<p>If you're running latency-sensitive applications (like high-frequency trading systems or real-time communications), even 2-5% overhead might be unacceptable. In these cases, use eBPF tracing in pre-production environments to identify issues, or enable it temporarily in production only when actively debugging.</p>
<h3 id="heading-security-considerations">Security Considerations</h3>
<p>eBPF is powerful, which means it requires elevated privileges. Understanding the security implications is crucial.</p>
<h4 id="heading-what-ebpf-can-access">What eBPF can access:</h4>
<p>eBPF programs can observe all syscalls, network traffic, and process execution in the kernel. This includes potentially sensitive data like connection details, file paths, and process arguments. While eBPF programs run in a sandbox and can't modify data or crash the kernel, they can read information that might be sensitive.</p>
<h4 id="heading-privilege-requirements">Privilege requirements:</h4>
<p>Loading eBPF programs requires <code>CAP_SYS_ADMIN</code> or <code>CAP_BPF</code> capabilities (on newer kernels). This is a privileged operation, only trusted users should have this access. The Inspektor Gadget DaemonSet runs with these privileges, so protect access to it accordingly.</p>
<h4 id="heading-best-practices">Best practices:</h4>
<p>Implement RBAC (Role-Based Access Control) to restrict who can run gadgets. Not every developer needs the ability to trace production systems.</p>
<p>Also, be mindful of what data you're collecting, if your traces might contain sensitive information (like authentication tokens in HTTP headers), restrict access to trace data.</p>
<p>Lastly, consider using admission controllers to prevent unauthorized eBPF program loading. Audit eBPF usage in production environments to track who ran which gadgets when.</p>
<h4 id="heading-network-policies">Network policies:</h4>
<p>Inspektor Gadget's DaemonSet needs to communicate with the API server and between its components. Ensure your network policies allow this communication while still maintaining appropriate segmentation.</p>
<h3 id="heading-when-to-use-ebpf-tracing-vs-traditional-apm">When to Use eBPF Tracing vs. Traditional APM</h3>
<p>eBPF tracing and traditional APM tools like New Relic, Datadog, or Dynatrace serve different purposes. Understanding when to use each helps you build an effective observability strategy.</p>
<p>Use eBPF tracing when:</p>
<ul>
<li><p>You can't modify application code (third-party applications, legacy systems, compiled binaries)</p>
</li>
<li><p>You need infrastructure-level visibility (network, syscalls, kernel behavior)</p>
</li>
<li><p>You're debugging issues that span service boundaries but don't show up in application logs</p>
</li>
<li><p>You want zero instrumentation overhead during normal operation (run tracing only when needed)</p>
</li>
<li><p>You need to understand what's actually happening versus what the application reports</p>
</li>
</ul>
<p>Use traditional APM when:</p>
<ul>
<li><p>You need business-context metrics (user IDs, transaction types, business-specific data)</p>
</li>
<li><p>You want automatic instrumentation with minimal setup for supported frameworks</p>
</li>
<li><p>You need long-term storage and analysis of all traces (eBPF tracing is often used for real-time investigation)</p>
</li>
<li><p>You want pre-built dashboards and alerting for common application patterns</p>
</li>
<li><p>You need application code-level visibility (stack traces, variable values, function calls)</p>
</li>
</ul>
<h3 id="heading-the-ideal-approach-use-both">The Ideal Approach: Use Both</h3>
<p>Many teams run traditional APM for continuous monitoring and use eBPF tracing for targeted investigation when APM data isn't sufficient. For example, your APM shows that a service is slow but doesn't explain why. You enable eBPF tracing on that service to understand what's happening at the kernel level, network delays, excessive syscalls, unexpected dependencies, and find the root cause.</p>
<p>This complementary approach gives you both the continuous visibility of APM and the deep diagnostic power of eBPF without the overhead of running both at maximum depth all the time.</p>
<h2 id="heading-next-steps-and-resources">Next Steps and Resources</h2>
<p>If you got this far, thanks for reading! Now that you have learned the fundamentals of eBPF observability, and hands-on tracing with Inspektor Gadget, you can continue your journey by:</p>
<h3 id="heading-exploring-other-ebpf-tools">Exploring Other eBPF Tools</h3>
<p>Now that you understand eBPF concepts through traceloop, exploring other tools will be much easier.</p>
<h4 id="heading-try-other-inspektor-gadget-gadgets">Try other Inspektor Gadget gadgets:</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># See all available gadgets</span>
kubectl gadget --<span class="hljs-built_in">help</span>

<span class="hljs-comment"># Some useful ones to explore:</span>
kubectl gadget trace_open -n demo-app     <span class="hljs-comment"># File I/O tracing</span>
kubectl gadget trace_bind -n demo-app     <span class="hljs-comment"># Port binding events</span>
kubectl gadget profile cpu -n demo-app    <span class="hljs-comment"># CPU profiling</span>
kubectl gadget snapshot process -n demo-app  <span class="hljs-comment"># Process listing</span>
</code></pre>
<p>Each gadget teaches you something different about system behavior and gives you another diagnostic tool in your toolkit.</p>
<h3 id="heading-experiment-with-other-ebpf-platforms">Experiment with other eBPF platforms:</h3>
<p>If you're interested in broader observability platforms, try Pixie for its auto-instrumentation and rich UI. Install Cilium with Hubble if you're focused on network observability and want to understand service mesh behavior. Explore Tetragon if security observability interests you, seeing what processes are executing and what files they're accessing.</p>
<p>The concepts transfer directly: all these tools attach eBPF programs to kernel hooks, collect event data, and present it in different ways. Your understanding of syscalls, traces, and kernel-level observation applies universally.</p>
<h3 id="heading-connect-to-the-cncf-observability-ecosystem">Connect to the CNCF Observability Ecosystem</h3>
<p>eBPF observability tools don't exist in isolation. They're part of the broader Cloud Native Computing Foundation ecosystem.</p>
<h4 id="heading-opentelemetry-integration">OpenTelemetry integration:</h4>
<p>Many eBPF tools can export data in OpenTelemetry format, allowing you to combine kernel-level traces with application-level traces in a unified observability backend. This gives you the complete picture: eBPF shows you infrastructure behavior while OpenTelemetry shows you application context.</p>
<h4 id="heading-prometheus-and-grafana">Prometheus and Grafana:</h4>
<p>eBPF-derived metrics can be exposed as Prometheus metrics and visualized in Grafana alongside your application metrics. This unified dashboard approach helps you correlate infrastructure and application behavior.</p>
<h4 id="heading-service-mesh-integration">Service mesh integration:</h4>
<p>If you're using Istio, Linkerd, or other service meshes, eBPF tools like Cilium Hubble can provide deeper visibility into service-to-service communication than the mesh alone provides. The mesh handles traffic management while eBPF gives you kernel-level visibility.</p>
<h4 id="heading-jaeger-and-zipkin">Jaeger and Zipkin:</h4>
<p>For organizations using distributed tracing backends, eBPF traces can be exported to these systems, enriching your trace data with infrastructure-level spans that application instrumentation misses.</p>
<h3 id="heading-community-resources-and-learning-paths">Community Resources and Learning Paths</h3>
<p>The eBPF community is vibrant and welcoming. You can continue learning from the resources below.</p>
<p><strong>Official documentation and blog:</strong></p>
<ul>
<li><p><a target="_blank" href="http://eBPF.io">eBPF.io</a>: The central hub for eBPF documentation, tutorials, and project listings</p>
</li>
<li><p><a target="_blank" href="https://inspektor-gadget.io/docs/latest/">Inspektor Gadget docs</a>: Comprehensive guides for all gadgets and use cases</p>
</li>
<li><p><a target="_blank" href="https://docs.cilium.io/en/stable/index.html">Cilium documentation</a>: Deep dives into eBPF networking</p>
</li>
<li><p><a target="_blank" href="https://www.cncf.io/blog/2025/01/27/what-is-observability-2-0/">CNCF Blog — “What is Observability 2.0?</a>: A quick overview of how modern observability moves beyond traditional tools by unifying metrics, logs, and traces for real-time insight in cloud-native systems.</p>
</li>
</ul>
<p><strong>Learning resources:</strong></p>
<ul>
<li><p><a target="_blank" href="https://cilium.isovalent.com/hubfs/Learning-eBPF%20-%20Full%20book.pdf">Learning eBPF by Liz Rice</a>: Comprehensive book covering eBPF fundamentals</p>
</li>
<li><p><a target="_blank" href="https://ebpf.io/summit-2025/">eBPF Summit</a>: Annual conference with talks from eBPF creators and users</p>
</li>
<li><p><a target="_blank" href="https://www.cncf.io/online-programs/cncf-on-demand-webinar-how-to-start-building-a-self-service-infrastructure-platform-on-kubernetes/">CNCF webinars</a>: Regular sessions on observability topics</p>
</li>
<li><p><a target="_blank" href="https://www.kubernetes.dev/community/community-groups/">Kubernetes observability SIGs</a>: Community discussions and projects</p>
</li>
</ul>
<p>To make this tutorial easy to follow and experiment with, I have included all Kubernetes manifests, demo applications, and eBPF tracing commands in this <a target="_blank" href="https://github.com/Emidowojo/ebpf-k8s-tracing-tutorial">repository</a>. You can also connect with me on <a target="_blank" href="https://www.linkedin.com/in/emidowojo/">LinkedIn</a> if you’d like to stay in touch.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Beginner's Guide to Observability in Cloud Native Applications ]]>
                </title>
                <description>
                    <![CDATA[ If you're new to cloud native technologies, you may have heard the term 'observability' before. But what exactly does it mean? Is it simply the ability to observe? And if so, what are we observing and why? I had the same questions when I started lear... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/observability-in-cloud-native-applications/</link>
                <guid isPermaLink="false">67e2d66c64d44185d5a6d406</guid>
                
                    <category>
                        <![CDATA[ otlp resource attributes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloud native applications ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Otel ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Victoria Nduka ]]>
                </dc:creator>
                <pubDate>Tue, 25 Mar 2025 16:14:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742917070693/fa372981-fb20-4230-bd9f-43b7255b8ced.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you're new to cloud native technologies, you may have heard the term 'observability' before. But what exactly does it mean? Is it simply the ability to observe? And if so, what are we observing and why?</p>
<p>I had the same questions when I started learning about cloud-native technologies. In this article, I'll share my understanding of core observability concepts, introduce essential observability tools, and share insights from a related project I’m working on.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-my-introduction-to-cloud-native-technologies">My Introduction to Cloud Native Technologies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-observability">What is Observability?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-types-of-observability-data">Types of Observability Data</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-metrics">1. Metrics</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-logs">2. Logs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-traces">3. Traces</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-observability-tools">Observability Tools</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-prometheus">Prometheus</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-opentelemetry">OpenTelemetry</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-otlp-resource-attributes">What are OTLP Resource Attributes?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-importance-of-otlp-resource-attributes">Importance of OTLP Resource Attributes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-my-project-work-fits-into-all-this">How My Project Work Fits into All This</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-additional-resources">Additional Resources</a></p>
</li>
</ul>
<h2 id="heading-my-introduction-to-cloud-native-technologies">My Introduction to Cloud Native Technologies</h2>
<p>I recently got selected as a mentee for the Linux Foundation Mentorship to work on the <a target="_blank" href="https://mentorship.lfx.linuxfoundation.org/project/36e3f336-ce78-4074-b833-012015eb59be">CNCF - Prometheus project</a>. The project is UX-focused, and for the next few months, I'll be working with my mentors to understand how users expect to use OpenTelemetry Line Protocol (OTLP) Resource Attributes in Prometheus.</p>
<p>That's quite a mouthful, I know. I was overwhelmed at first, and honestly, I’m still figuring it out. This is my third week, and although I still have a lot to learn—given that I had no knowledge of cloud native technologies when I applied for this internship—I've already learned quite a bit.</p>
<p>As I often do, I intend to document what I learn through articles to help reinforce concepts in my memory and serve as a resource for other newcomers who may find themselves grappling with these technical terms in the future. You know what they say: you can't say you've understood something until you're able to explain it to someone else who's also new to the topic.</p>
<h2 id="heading-what-is-observability">What is Observability?</h2>
<p>First, I had to learn what the unfamiliar terms meant—and there were a lot of them flying around. OpenTelemetry. Prometheus. Resource attributes. I’ve come to understand that these terms fall under one umbrella: Observability. Let's start there.</p>
<p>Let’s use a food delivery app to illustrate. When someone orders food, a lot happens behind the scenes:</p>
<ul>
<li><p>The app connects to different services (restaurants, payments, delivery)</p>
</li>
<li><p>Data flows between different systems to process the order, assign a driver, and track delivery</p>
</li>
</ul>
<p>Engineers need to monitor all the processes to ensure everything works smoothly. Are orders taking too long to process? Is the payment system failing? Does the app suddenly crash under load? Which part of the system is causing delays?</p>
<p>To answer these questions, engineers <strong>instrument</strong> their code. This means that they configure it to send back real-time data about the state, performance, and behavior of the application. This practice of understanding what's happening inside a complex system based on the data it generates is known as <strong>Observability</strong>.</p>
<p>You can see the process illustrated in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742335445056/5fe7bb0b-bdf9-4f52-a2c1-7f2977411c6c.png" alt="A flowchart diagram titled &quot;Visual flow of observability data&quot; showing how data moves through a food delivery application system. The flow starts with a User who orders food from a Food App. The Food App connects to three services (Restaurant, Payment, and Delivery). All these components send data to OpenTelemetry, which collects three types of data: Metrics, Logs, and Traces. OpenTelemetry then forwards only the Metrics data to Prometheus, which stores metrics." class="image--center mx-auto" width="2080" height="1888" loading="lazy"></p>
<p>In the above flowchart diagram, you can see how data might move through a food delivery application system. The flow starts with a User who orders food from a Food App. The Food App connects to three services (Restaurant, Payment, and Delivery). All these components send data to OpenTelemetry, which collects three types of data: Metrics, Logs, and Traces. OpenTelemetry then forwards only the Metrics data to Prometheus, which stores metrics.</p>
<h2 id="heading-types-of-observability-data">Types of Observability Data</h2>
<p>There are three key types of data that systems generate for observability:</p>
<h3 id="heading-1-metrics"><strong>1. Metrics</strong></h3>
<p>Metrics are numerical measurements collected over time that represent the state or performance of your system. Examples in a food delivery app would be the number of orders processed per minute, average order processing time in milliseconds, number of active users or delivery drivers, and so on.</p>
<h3 id="heading-2-logs"><strong>2. Logs</strong></h3>
<p>Logs are text-based records of discrete events that occur within your application. Logs for our food delivery app would look something like this:</p>
<pre><code class="lang-http"><span class="hljs-attribute">ERROR</span>: Payment failed for order #12345 - Credit card declined
<span class="hljs-attribute">INFO</span>: Driver #789 assigned to order #12345
</code></pre>
<h3 id="heading-3-traces"><strong>3. Traces</strong></h3>
<p>Traces track the entire lifecycle of a request as it moves through different services in a system. They help engineers see how different components interact and identify bottlenecks in complex, distributed systems.</p>
<p>For example, in our food delivery app, a single order request might go through the following steps:<br><code>User places an order</code> → <code>Request sent to restaurant system</code> → <code>Payment processor verifies payment</code> → <code>Delivery system assigns a driver</code> → <code>User receives confirmation</code>.</p>
<p>Each step in this journey is recorded as part of a trace. This helps engineers pinpoint where delays occur and optimize the system for better performance.</p>
<p>Observability relies on metrics, logs, and traces working together to provide full system visibility. Metrics tell you something is wrong (“Error rate increased by 5%”). Logs tell you why it happened (“Payment failed due to invalid card details”). Traces show exactly where it happened (“Delay in restaurant service response”).</p>
<h2 id="heading-observability-tools"><strong>Observability Tools</strong></h2>
<p>Observability tools give you visibility into what’s going on within your application. There are a lot of them, but for the purpose of this article, we’ll talk about two: Prometheus and OpenTelemetry. </p>
<h3 id="heading-prometheus"><strong>Prometheus</strong></h3>
<p><a target="_blank" href="https://prometheus.io/">Prometheus</a> is an open-source monitoring and alerting toolkit. It does two things:</p>
<ul>
<li><p>Collects data from applications, specifically metrics (remember the data types we talked about earlier)</p>
</li>
<li><p>and stores them in a time-series database.</p>
</li>
</ul>
<p>A time-series database is a database specifically designed to handle measurements or events that occur over time.</p>
<p>Prometheus uses what's called a <strong>pull-based model</strong> to collect metrics from applications. Pull-based means Prometheus actively requests (pulls) data from services at regular intervals. Think of it like refreshing a webpage to get the latest content.</p>
<h3 id="heading-opentelemetry"><strong>OpenTelemetry</strong></h3>
<p><a target="_blank" href="https://opentelemetry.io/">OpenTelemetry (OTel)</a> collects, processes, and exports observability data. Unlike Prometheus, which mainly focuses on metrics, OpenTelemetry provides a standardized way to instrument applications for all three types of observability data: logs, metrics, and traces.</p>
<p>OpenTelemetry is designed to be vendor-agnostic. This means you can instrument your applications once with OpenTelemetry and then send that telemetry data to any supported observability backend, which could be an open-source solution like Jaeger or Prometheus, or commercial platforms like Datadog, New Relic, Dynatrace, or Honeycomb.</p>
<p>So, for example, you can use OpenTelemetry to instrument your application – and then Prometheus can pull metrics from OpenTelemetry while other tools handle logs and traces.</p>
<h2 id="heading-what-are-otlp-resource-attributes"><strong>What are OTLP Resource Attributes?</strong></h2>
<p>When OpenTelemetry collects data from applications, it does more than just gather raw telemetry data. It also provides context about that data. This context comes in the form of <strong>resource attributes</strong>, which describe where the data came from and what it relates to.</p>
<p>The 'resource' is the component (or entity) producing the data, while the 'attributes' are specific details about that resource.</p>
<p>Resource attributes are structured as pairs of information:</p>
<ul>
<li><p>The "key" is the name or identifier of the attribute (like <code>service.name</code> or <code>host.id</code>)</p>
</li>
<li><p>The "value" is the specific information for that attribute (like <code>payment-service</code> or <code>server-123</code>)</p>
</li>
</ul>
<p>Together, these key-value pairs identify and describe the specific component that's generating the observability data.</p>
<p>For example, if a payment processing service is sending metrics about transaction times, the resource attributes might include:</p>
<ul>
<li><p><code>service.name: "payment-service"</code></p>
</li>
<li><p><code>service.version: "1.2.3"</code></p>
</li>
<li><p><code>deployment.environment: "production"</code></p>
</li>
</ul>
<p>These attributes tell you exactly which service, which version, and in which environment the data is coming from, providing context for interpreting the metrics, logs, or traces.</p>
<p>Resource attributes are not arbitrary. OpenTelemetry provides a standardized set of attribute names and formats that everyone should follow, similar to having an agreed-upon language for describing services and their properties.</p>
<p>For example, OpenTelemetry specifies that you should use <code>service.name</code> (not <code>app_name</code> or <code>service_id</code>) to identify your service. They've created these standardized naming conventions (called <a target="_blank" href="https://opentelemetry.io/docs/concepts/semantic-conventions/">semantic conventions</a>) so that:</p>
<ol>
<li><p>All tools in the ecosystem can understand the same attributes</p>
</li>
<li><p>Engineers across different companies use consistent terminology</p>
</li>
<li><p>Observability data can be easily shared between different systems</p>
</li>
</ol>
<p>You can still create your own custom attributes when you need something specific (like <code>payment.provider</code> for a payment service), but using the standard attributes whenever possible means your telemetry data will work better with existing tools and be more easily understood by other engineers.</p>
<h2 id="heading-importance-of-otlp-resource-attributes">Importance of OTLP Resource Attributes</h2>
<p>Let’s say engineers want to monitor how long food deliveries take and whether there are delays in specific locations. Without resource attributes, OpenTelemetry might simply collect and report this metric like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">delivery_time_seconds:</span> <span class="hljs-number">1800</span>
</code></pre>
<p>This tells us that a delivery took 1,800 seconds, or 30 minutes, but nothing else. That’s useful, but it lacks context. Where did this happen? Which service handled it? If there was a delay in delivery and engineers wanted to investigate the cause, this alone would not help.</p>
<p>With OpenTelemetry’s resource attributes, the metric becomes more meaningful:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">delivery_time_seconds:</span> <span class="hljs-number">1800</span>
<span class="hljs-attr">resource:</span>
  <span class="hljs-attr">service.name:</span> <span class="hljs-string">"delivery-service"</span>
  <span class="hljs-attr">service.instance.id:</span> <span class="hljs-string">"instance-456"</span>
  <span class="hljs-attr">cloud.region:</span> <span class="hljs-string">"ng-west-2"</span>
  <span class="hljs-attr">deployment.environment:</span> <span class="hljs-string">"production"</span>
  <span class="hljs-attr">customer.city:</span> <span class="hljs-string">"Lagos"</span>
  <span class="hljs-attr">restaurant.id:</span> <span class="hljs-string">"rest-789"</span>
</code></pre>
<p>This tells us:</p>
<ul>
<li><p>The data came from the delivery service.</p>
</li>
<li><p>The instance handling the request is "instance-456".</p>
</li>
<li><p>It’s running in the ng-west-2 cloud region.</p>
</li>
<li><p>The environment is Production (not testing or staging), and so on.</p>
</li>
</ul>
<p>Now, engineers can answer more specific questions:</p>
<ul>
<li><p>Are deliveries slower in certain cities? (Filter by <code>customer.city</code>)</p>
</li>
<li><p>Are certain restaurants taking longer to prepare food? (Filter by <code>restaurant.id</code>)</p>
</li>
<li><p>Are delays only happening in a specific cloud region? (Filter by <code>cloud.region</code>)</p>
</li>
<li><p>Are issues only happening in production or also in staging? (Filter by <code>deployment.environment</code>)</p>
</li>
</ul>
<p>When issues arise, resource attributes allow engineers to quickly narrow down the source of problems. Rather than investigating every service, they can filter by specific attributes to focus their efforts.</p>
<h2 id="heading-how-my-project-work-fits-into-all-this"><strong>How My Project Work Fits into All This</strong></h2>
<p>Many engineers use OpenTelemetry for data collection and then send metrics to Prometheus for storage, querying, and analysis.</p>
<p>But Prometheus does not natively support resource attributes in the same way as OpenTelemetry. Instead, it relies on labels to organize metrics. Since Prometheus traditionally has its own labeling system for metrics, integrating OpenTelemetry's resource attributes creates interesting UX challenges.</p>
<p>One key challenge is the <strong>cardinality</strong> explosion. Cardinality refers to the number of unique combinations of label values (or dimensions) that a metric can have. A "cardinality explosion" occurs when you add labels with many possible values. OpenTelemetry often includes many detailed attributes that, if directly converted to Prometheus labels, would create an overwhelming number of time series. This can slow down Prometheus dramatically or even cause it to crash.</p>
<p>The existing solution involves stuffing all resource attributes into a single JSON-encoded Prometheus label. While this prevents the cardinality explosion, it makes querying extremely cumbersome. Users have to use complex join operations and specialized query syntax to filter or aggregate based on these attributes.</p>
<p>This approach is technically functional but creates a poor user experience. My research aims to understand how users mentally model the transition from OpenTelemetry's rich attribute system to Prometheus's more constrained label system.</p>
<p>The research goals are to:</p>
<ol>
<li><p>Understand how engineers currently use OpenTelemetry resource attributes with Prometheus</p>
</li>
<li><p>Identify pain points in the current integration between these systems</p>
</li>
<li><p>Discover user expectations for how resource attributes should be represented in Prometheus</p>
</li>
</ol>
<p>This work is particularly important as more organizations adopt OpenTelemetry as their instrumentation standard while continuing to use Prometheus for metrics monitoring. Creating a seamless experience between these two popular open-source projects will help improve the overall observability ecosystem.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Observability in cloud native applications is clearly an interesting subject and important for building reliable, performant systems. The tools and concepts we've explored – metrics, logs, traces, Prometheus, and OpenTelemetry – form the foundation of modern observability practices.</p>
<p>As I continue my mentorship program, I'll share more insights about how these technologies work together and try to break them down from the perspective of a first-time learner.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<p>Learn more about:</p>
<ol>
<li><p><a target="_blank" href="https://opentelemetry.io/docs/">OpenTelemetry</a></p>
</li>
<li><p><a target="_blank" href="https://prometheus.io/docs/introduction/overview/">Prometheus</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/prometheus/prometheus/issues/15909">My UX research project</a></p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
