Capstone: one request, every layer
Everything the book has built, in a single request. A curl carrying a traceparent hits a FastAPI service that calls a Quarkus service; both emit spans, metrics, and logs on one trace_id, while an Aya program watches the same request at the socket layer and ties its eBPF view into that trace. The result is one investigation that reads from the HTTP span all the way down to the kernel — the payoff of sixty-two chapters.
This is the chapter the whole book has been walking toward. Individually you can
now write a kprobe, a uprobe, an XDP filter, an LSM hook; emit metrics, drain a
ring buffer, pin a program, relocate it with CO-RE; and stand up Tempo, Loki,
and Prometheus behind Grafana. The capstone puts them in one place: a single
request, triggered by curl or Postman, traced from the HTTP handler in one
service, through a call to a second service, and down into the kernel via an
Aya program — all stitched together on one trace_id. When you can click a
slow span in Tempo and see, for that exact request, the app’s logs, its metrics,
and what the kernel did underneath it, you’ve built the thing eBPF and
OpenTelemetry exist to make possible.
The code is in examples/63-capstone/. ./demo.sh brings up both services and
the eBPF observer, fires one traced curl, and shows the trace_id across every
store; the README.md has the details.
The scenario
A deliberately small but real shape: a /checkout request. The FastAPI
service receives it, and to fulfil it calls a Quarkus /inventory service —
two hops, two languages, two containers, one trace. Both run under Podman
(Chapters 45–47’s pattern), both export OTLP to the Chapter 3 stack. We fire it
with one command:
curl -H "traceparent: 00-$(openssl rand -hex 16)-$(openssl rand -hex 8)-01" \
http://127.0.0.1:8000/checkout
Because the traceparent propagates from FastAPI to Quarkus (their OTel SDKs do
this automatically), the result in Tempo is a single trace with spans from
both services nested under the incoming request — the distributed trace
Chapter 62 described, now real.
Three signals from the apps
Each service, instrumented exactly as Chapter 62’s example was, contributes all three application signals under that one trace_id:
- Spans → Tempo.
checkout(FastAPI) as the parent, an HTTP client span for the call to Quarkus, andinventory(Quarkus) as the child — the request’s shape and timing, hop by hop. - Metrics → Prometheus.
app_requests_totaland latency histograms from each service, with exemplars carrying the trace_id so a latency spike links straight to the trace. - Logs → Loki. Each service’s log line for the request, stamped with the
trace_id (the
otelTraceIDfield) so Grafana’s derived field links it back to the span.
That alone is a complete application-observability story. The book’s contribution is the fourth view.
The fourth view: eBPF underneath
While the apps describe themselves from the inside, an Aya program watches the
same request from underneath — at the socket layer, where the bytes actually
move. The observer counts the request’s read/write syscalls and bytes for the
service processes and emits ebpf_capstone_* metrics, giving the kernel-side
truth (how many syscalls, how much data, how long on-CPU) that no application
SDK can see.
The crucial step is joining that view to the trace, and there are two genuine ways, both built earlier in the book:
- Extract the trace_id at L7. The
traceparenttravels in the HTTP request bytes. An eBPF program reading the socket payload — the OBI technique from Chapter 46, or a uprobe on the TLS/read path from Chapters 14–15 — can scan fortraceparent: 00-<trace_id>-…and tag its event with that exact id. The example ships this as the canonicalreference/l7_traceparent.bpf.c; it’s the same id the app reported, captured from the wire. - Correlate by time, pid, and service. Where extracting the id is
impractical (encrypted payloads without a uprobe, an interpreter that resists
probing), the eBPF metrics still share a wall-clock window, the container’s
pid/cgroup, and the
service.namewith the span — enough for Grafana to line them up. The runnable observer uses this path; the demo captures the trace_id from the response and tags the window so the correlation is exact.
Either way, the outcome is the headline: an ebpf_capstone_* series you can put
on the same Grafana view as the trace, for the same request.
Reading one request end to end
With the stack wired (Chapter 62’s correlation data sources) and the request fired, the investigation reads top to bottom:
- Tempo — find the trace by its id: the
checkoutspan, the client call, the nestedinventoryspan, with timings. - Span → Logs — click through to Loki for each service’s log line for this request.
- Metric exemplar → trace — from
app_requests_total’s graph, the exemplar dot returns you to this trace. - The eBPF layer — alongside,
ebpf_capstone_syscalls_totaland on-CPU time for the request window show what the kernel did to serve it — the read syscalls on each socket, the bytes moved — the layer the app can’t report.
That is the full picture: application intent (spans), application state (metrics, logs), and kernel reality (eBPF), for one request, in one place.
Build, deploy, observe
cd examples/63-capstone && ./demo.sh
The demo builds and runs both services with podman-compose, starts the Aya
observer, fires the traced curl, prints the resulting trace_id, and lists
the Grafana steps to read it across all four views. In Grafana
(127.0.0.1:3000): Explore → Tempo → the trace; then the linked logs, the
metric exemplars, and ebpf_capstone_* for the same window. It’s the
three-signals capstone of Chapter 46, completed — two real services, the full
LGTM correlation, and the eBPF view joined in.
Cross-check
[host]$ curl -s "http://127.0.0.1:3200/api/traces/<trace_id>" | jq '.batches | length' # spans from both services
[host]$ curl -s 'http://127.0.0.1:9090/api/v1/query?query=ebpf_capstone_syscalls_total' | jq '.data.result'
[host]$ podman ps # both app containers + their pids for the eBPF side
A trace containing spans from both ebpf-capstone-fastapi and
ebpf-capstone-quarkus, next to an ebpf_capstone_* series for the same window,
is the proof the whole stack — apps, backend, correlation, and eBPF — is working
as one.
What you learned
- A single
curlwith atraceparentproduces one distributed trace across FastAPI → Quarkus, with spans (Tempo), metrics (Prometheus), and logs (Loki) all sharing the trace_id — the full application-observability story you assembled in Chapter 62. - An Aya program adds the fourth view: the kernel-side truth (syscalls, bytes, on-CPU time) for the same request, joined to the trace by L7 traceparent extraction (OBI/uprobe) or by time + pid + service.
- Read end to end, one request now spans application intent, application state, and kernel reality in a single Grafana investigation — the reason eBPF and OpenTelemetry are worth combining, and the payoff of the whole book.
Next, Part 10 is an optional field guide to the command-line tools we’ve
leaned on for validation — bpftrace, bpftool, and the BCC tools — driven
from Python. Then a closing retrospective.
Verification status: verified — Fedora 44, kernel 7.1.3. Built and run on the lab VM host (Fedora 44): both services build and come up under podman-compose, and one traceparented request flows FastAPI /checkout -> Quarkus /inventory across both services.