Documentation
Every page of docs/: the guides, one page per tool, one per product, the demo scripts. The pages stay in the repository, reviewed with the code.
Clicking a page shows it; a link to another page opens it here. Each tool keeps its blue box for the page itself, and its 📖 Documentation link, top right, opens its full page here.
docs/demos/observability.md
Observability demo: see what the product does
Disclaimer: this is a personal demo project, not a source of truth or a reference implementation. It reflects my own understanding and choices: its content remains subjective, despite the quotes and references it contains. It may be incomplete, simplified, inaccurate or outdated, and it is not meant for production use. Always check the official documentation before applying anything from it. Provided as is, without any warranty: you use it at your own risk.
Step-by-step script to show an audience what observability brings, with the same product as the testcontainers demo: every call to the API leaves a trace, and a trace tells what happened behind the page, across services, without opening a debugger or reproducing anything. In production the traces go to a platform such as Tsuga; here a free all-in-one container plays that platform on the laptop. About 15 minutes.
TL;DR
mise run up,mise run traffic, then Grafana on http://localhost:3000: Explore, Tempo, Service Namehello-api.- Read one healthy trace, one refusal, then break the bank (
docker compose stop partner) and find the failure in the traces.- No telemetry code in the product:
opentelemetry-instrumentand a fewOTEL_*variables do it all, and sending to a real platform only changes the endpoint.
What observability is for
A test says whether something works, before it ships. Observability says what the system actually did, once it runs, on the requests nobody wrote a test for. It is the ability to answer a question about a running system ("why was this call slow?", "which service refused this customer?", "since when do we answer 502?") from what the system already emits, without adding code and without reproducing the problem.
Three kinds of signals carry that information:
- Traces: the path of one request through every service it crosses, as a tree of timed steps called spans. The best tool to answer "what happened to this call".
- Metrics: numbers aggregated over time (calls per route, error rate, durations). The best tool to answer "how much, how often, since when", and to alert.
- Logs: the lines a service writes. Most useful when each line carries the trace it belongs to.
OpenTelemetry (OTel) is the open standard to produce and ship these signals: an SDK in each service, the OTLP protocol on the wire, and any backend at the end. Because the standard sits between the code and the backend, the same service can send to Tsuga in production and to a local stack on a laptop.
For a QA engineer, it changes three things: a failing end-to-end test points to a trace that says which service broke; behaviour in production can be checked against what the tests assumed; and instrumentation itself can be verified before it ships, as this demo does.
How the project is wired
- The services (
hello-api,bank-partner,order-worker,identity-service) and the Quality Console start withopentelemetry-instrumentin their image. It instruments FastAPI, httpx, Redis, PyMySQL and the RabbitMQ client automatically: there is no telemetry line insrc/orpartner/. docker-compose.ymlgives them the standard variables:OTEL_EXPORTER_OTLP_ENDPOINT(OTLP over HTTP, port 4318),OTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES(team=<owner>, the team owning the service, missing on purpose onnewsletter-service), and which signals to export (traces and metrics; logs stay ondocker compose logs).- The
observabilityservice isgrafana/otel-lgtm: an OpenTelemetry collector, Tempo (traces), Prometheus (metrics), Loki (logs) and Grafana (the UI), in one container. It plays the role Tsuga plays in production. - In the tests,
OTEL_SDK_DISABLED=trueturns the SDK off: no collector runs in a test. - Grafana is only the screen. The services know nothing about it: they speak OpenTelemetry (OTLP) to the collector inside
observability, which stores the traces in Tempo and the metrics in Prometheus. Swap the container for Tsuga's endpoint and the same data lands there; Grafana's login is switched off indocker-compose.ymlbecause nobody needs an account on a laptop.
Before the talk
- Install and pre-pull as in the testcontainers demo.
mise run uponce to build the images, open http://localhost:3000 to check Grafana answers, thenmise run down.
On stage
1. Start the product and use it
mise run up. It returns once MySQL, Redis, the bank, the API, the front, the Quality Console and the observability stack all run.- Open http://localhost:8080, type
ada(or click one of the examples under the field:ada,grace,zoe,dennis), click "Say hello". mise run trafficsends five more greetings: ada, grace, zoe (no bank account), dennis (closed customer), nobody (unknown).
2. Find a trace
mise run traces: Grafana opens straight on thehello-apitraces of the last 15 minutes (the same link is the Observability card of the console). No account needed: the stack opens Grafana to anonymous visitors and hides its login form; the "Sign in" button top right can be ignored. A login page anyway means an old cookie of another app onlocalhost:3000: use a private window, oradmin/admin.- Traces show up 10 seconds to a minute after the calls: the collector and Tempo batch before they make them searchable. If your call is not listed yet, click the blue refresh button (top right).
- Each line is one request: its route, its start time, its duration. Click the trace id to open it.
<details> <summary>Getting there by hand, without the link</summary>
Explore in the left menu, data source Tempo (the dropdown at the top), query type Search, Service Name hello-api, then Run query (blue button, top right). Explore opens on another data source by default, which is why the link is easier.
</details>
3. Read a healthy trace
Click a trace of ada and read the waterfall top to bottom:
GET /api/hello: the request as the API received it, the whole duration.SELECT: the customer read in MySQL. Open the span:db.statementholds the SQL, the username is a bound parameter.GET: the call to the bank. It continues insidebank-partner, another service:GET /balance, then its ownSELECTonbank_accounts. One trace crosses two services because the trace context travels in an HTTP header (traceparent).INCRBY: the visit counted in Redis.
Message to pass: one page view, five steps, two services, and every step timed. Nobody wrote a line of code for it.
4. Read a refusal
Run a search again and click a trace of dennis: GET /api/hello then SELECT, and nothing else. The trace proves the refusal happened in the API, before the bank was called and before any visit was counted.
5. A trace across a queue
- On http://localhost:8080, click ada buys Foundation in the order card.
- In Explore, search the service
hello-apiagain: the newest trace isPOST /api/orders. Open it. - It does not stop at the API. After its
INSERTs and itsorder-workflow send(the event published to RabbitMQ), the same trace continues in another service,order-worker: eachorder-workflow receive, then the step'sSELECT,UPDATEandINSERT, three times, one per step, seconds apart. - The trace context travels in the message headers: the API answered
202long before the work was done, and the trace still shows the whole story.
Message to pass: asynchronous work is exactly where logs alone lose the thread; a trace follows the request through the broker. More in The order workflow.
6. An incident, found in the traces
- Break the bank:
docker compose stop partner. mise run traffic: ada and grace now get{"detail":"Bank Partner unreachable: ..."}, a502.- In Explore, switch the query type to TraceQL and run
{ resource.service.name = "hello-api" && status = error }. - Open one: the request ended in
502, theGETspan to the bank is in error with the exception attached, and theINCRBYnever happened. - Repair:
docker compose start partner, thenmise run trafficagain. Back to healthy traces.
Message to pass: without traces, a 502 on the page is a guess between three neighbours. With them, the failing step, its error and its duration are one click away.
7. Metrics: the same calls, counted
- In Explore, switch the data source to Prometheus, then the editor to Code.
- Run
sum by (http_status_code) (http_server_duration_milliseconds_count{service_name="hello-api"}): how many calls ended in200,404and502. - Traces explain one call, metrics show the trend: this is what an alert watches.
8. And in production
Nothing in the product changes. In a cluster, the platform injects OTEL_EXPORTER_OTLP_ENDPOINT pointing at its collector, and the traces land in Tsuga instead of this local Grafana. To try it from the laptop, point that one variable at another OTLP backend: the code and the images stay the same.
After the talk
mise run down. It deletes every container, the traces included.
<details> <summary>If something goes wrong on stage</summary>
- No trace after a minute: check the time range (top right, "Last 15 minutes") and that
docker compose psshowsobservabilityrunning. - Service Name list empty: no call reached a service yet, run
mise run traffic. - Port 3000 or 4318 already in use: stop what listens there, or change the left side of the port in
docker-compose.yml. - Another backend wanted (an APM-style UI like SigNoz): start it from its own
docker compose, then pointOTEL_EXPORTER_OTLP_ENDPOINTat it. Mind its UI port, often 8080 like the front.
</details>
See also: README.md, section "Seeing the API calls".