Files
LedgerLine/docs/roadmap/phase-2.md
T
Movses AslanyanandClaude Opus 5.5 c06d6be4ce Mark Phase 2 done
First auto-instrumented sanctions trace verified in Tempo. Records what
carries forward: /healthz trace noise, the prod environment label in the
base compose file, and the red G10 guard test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:55:31 +03:30

146 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2 — The model, and one signal end to end
*Part of `docs/roadmap/` — see [`intro.md`](intro.md) for platform/background context and [`summary.md`](summary.md) for how every phase fits together. Prev: [`phase-1.md`](phase-1.md) · Next: [`phase-3.md`](phase-3.md)*
**Status:** Done
**Message:** Started and finished 2026-10-12. The platform is deployed and the
Alloy component graph has been read. `sanctions` runs under zero-code
auto-instrumentation (`opentelemetry-distro==0.66b1` +
`opentelemetry-exporter-otlp==1.45.1`, instrumentors installed by
`opentelemetry-bootstrap` at build time). It exports traces only, over
OTLP/HTTP to `https://otlp.vione.cloud`. First real trace:
`9bfa05d03eabe3d3338a8672cf127465` (`POST /internal/lookup`), found with
`{resource.service.name="sanctions" && kind=server && span.http.route!="/healthz"}`.
Found along the way: Alloy returns **404** for OTLP metrics and logs. They are
not silently accepted, and the docs were corrected to say so.
Carried forward, all undecided:
- **`/healthz` traces.** The Docker healthcheck produces a trace every 5s and
floods Tempo search. `OTEL_PYTHON_FASTAPI_EXCLUDED_URLS` would filter it.
- **`deployment.environment.name=prod`** is set in the base
`docker-compose.yml`, so local runs would also be labelled `prod`.
- **Guard test G10** (`test_dependency_files_do_not_list_observability_libraries`)
is red by choice until the guard suite is retired in Phase 3.
**Time-box:** one evening.
**Why:** you need the vocabulary, but more importantly you need to see one trace
appear in Grafana within an hour, so the rest of the roadmap feels concrete rather
than abstract.
### Concepts
- **Monitoring vs observability.** Monitoring answers questions you knew to ask
(known unknowns): dashboards and alerts built from predefined aggregates.
Observability is a property of the system: can you ask a *new* question about
production, that you never anticipated, without shipping new code? Charity Majors'
framing — monitoring is known-unknowns, observability is unknown-unknowns. The
practical implication for you: emit **wide, high-dimensional events** (rich span
attributes), not just pre-aggregated counters.
- **The OTel architecture split: API vs SDK vs Collector.**
- The **API** is what your code and third-party instrumentation libraries call.
If no SDK is configured, API calls are no-ops — that's deliberate, it means a
library can be instrumented without forcing a dependency on any implementation.
- The **SDK** is the engine: sampling, batching, context propagation, export.
Application owners configure the SDK; library authors only touch the API.
- The **Collector** is a standalone process that receives, processes and exports
telemetry. Optional, but you should use one.
- **OTLP** is the wire protocol — gRPC on 4317, HTTP/protobuf on 4318.
- **Resource** vs **attributes**: a Resource describes the *producer* (`service.name`,
`service.version`, `deployment.environment`) and is attached to every signal from
that process. Attributes describe the individual span/metric/log.
- **Semantic conventions**: standardised attribute names (`http.request.method`,
`db.system`, `server.address`). Use them for anything standard; namespace your own
(`ledgerline.account.id`). This is what makes cross-service, cross-language
correlation work — and it's what Grafana's prebuilt dashboards key off.
- **Signals**: traces, metrics, logs. (Profiling is a fourth signal, in alpha —
aware of, not learning yet.)
- **Why a Collector rather than exporting straight to Tempo/Loki/Prometheus:** your
app learns one endpoint. Swapping backends becomes a YAML edit, not a redeploy of
seven services. The Collector also does the things you don't want in your app:
retry, PII redaction, tail sampling, memory limiting, fan-out to multiple
destinations.
### Resources
- freeCodeCamp *OpenTelemetry Course* (~1 h, free) — watch at 1.5×, take notes on the
M.E.L.T. and context-propagation sections; skip the Node code.
- OpenTelemetry docs → Concepts → *Components* and *Signals*. ~30 min.
- Optional: run the OpenTelemetry Demo (`docker compose up` in the astronomy-shop
repo) once and click around Grafana/Jaeger for 20 minutes. Don't try to understand
it — just see what "done" looks like.
### Build
1. **Deploy the platform.** *Deployed 2026-10-12.* The mentor's Ansible
Grafana stack was ported to the k3s cluster `falken` as `k8s/`, keeping the
same versions, configs and pipeline. Deploy with `kubectl apply -k k8s/`.
`k8s/README.md` covers deploy/teardown, credentials and every deviation
from the Ansible role.
**Why not the Ansible route on a VM:**
- `czy` (1 GB) can't host it. The platform README recommends 2 vCPU / 4 GB.
- `*.vione.cloud` sits behind Cloudflare's proxy, which only forwards a few
ports (80/443, 8080, 8443, …), so raw ports like `:3000` never reach any
origin. That's why `ledgerline.vione.cloud:3000` showed nothing.
**Endpoints:** each one is a subdomain behind Traefik. Everything except
Grafana uses basic auth (`k8s/.credentials`).
| | |
|---|---|
| Grafana | https://grafana.vione.cloud |
| Prometheus | https://prometheus.vione.cloud |
| Alertmanager | https://alertmanager.vione.cloud |
| Loki | https://loki.vione.cloud |
| Tempo (query) | https://tempo.vione.cloud |
| Alloy UI | https://alloy.vione.cloud |
| OTLP/HTTP ingest | https://otlp.vione.cloud |
Container logs from `czy` reach Loki through an Alloy agent there (the
`alloy-agent` service in `docker-compose.traefik.yml`).
**Verified live:** all of the following worked, using a scratch trace sent
from `czy`.
- Every endpoint answers, and returns 401 without auth.
- Prometheus scrapes all six platform targets.
- Traces flow czy → Alloy → Tempo.
- Tempo's metrics-generator emits `traces_spanmetrics_*` and
`traces_service_graph_*` with exemplars.
- czy container logs land in Loki.
**Two platform bugs, fixed in the port.** As shipped, the Ansible role
produced no span metrics or service graph (Tempo needs
`metrics_generator.traces_storage`), and Prometheus discarded exemplars
(missing `--enable-feature=exemplar-storage`).
**One issue left open on purpose:** Tempo labels its exemplars `traceID`,
but the datasource's `exemplarTraceIdDestinations` expects `trace_id`.
Now open the Alloy UI and **read the component graph**. It shows you the
exact pipeline, including the fact that the OTLP receiver has only a traces
output. Seeing that in the UI before you get bitten by it is worth ten
minutes.
2. Pick the **`sanctions`** service (the smallest) and add nothing but zero-code
auto-instrumentation:
```
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
```
and change its command to
`opentelemetry-instrument uvicorn services.sanctions.app:app --host 0.0.0.0 --port 8003`
with `OTEL_SERVICE_NAME=sanctions`,
`OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.vione.cloud`,
`OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` — pointing at **Alloy**, not at
Tempo directly — plus the basic-auth header
`OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20<base64 user:pass>`
(URL-encoded: the space is `%20`; see `k8s/README.md`). Keep the
credential in the gitignored `.env.observability` on czy, not in compose.
3. Fire a request. Open Grafana → Explore → Tempo → find the trace.
**Done when:** you can see a single-service trace in Tempo, and you can explain out
loud what the Resource attributes on it mean and where they came from.