Port the observability platform to k3s and ship czy logs to it
k8s/ is now a faithful port of the mentor's Ansible Grafana stack (Prometheus, Alertmanager, Loki, Tempo, Alloy, Grafana, node-exporter) for the ledgerline-observability namespace on falken: same versions and configs, one *.vione.cloud ingress per tool behind Traefik basic auth, secrets generated by create-secrets.sh into gitignored files. Two fixes over the Ansible role, both verified live: Tempo's metrics-generator needs traces_storage or it runs no processors, and Prometheus needs exemplar-storage to keep exemplars. Ledgerline stays on czy; an Alloy agent in the Traefik override ships its container logs to Loki. Roadmap and CLAUDE.md now point at k8s/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
34a11da848
commit
7719ef5fcf
@@ -25,16 +25,30 @@ below. Phase 0 is built test-first from
|
||||
two specs: `docs/phase-0a-test-suite-spec.md` (the tests) and
|
||||
`docs/phase-0-base-project-spec.md` (the system).
|
||||
|
||||
### The observability backend is external and pre-built
|
||||
### The observability backend is pre-built, ported to k3s
|
||||
|
||||
The Grafana stack is **not** built in this repo. It is deployed by a separate
|
||||
Ansible project, `observability-ansible`, which the user received from his mentor.
|
||||
It deploys two stacks on a target host via Docker Compose:
|
||||
The platform came from the mentor as an Ansible project, `observability-ansible`
|
||||
(kept in `ansible/` as the reference). It deploys two stacks on one host via
|
||||
Docker Compose:
|
||||
|
||||
- **Grafana stack:** Prometheus + Alertmanager + Loki + Tempo + Grafana, with
|
||||
**Grafana Alloy** as the collector and node-exporter for host metrics.
|
||||
- **ELK stack:** Filebeat → Logstash → Elasticsearch → Kibana/Grafana. This is the
|
||||
hands-on subject of Phase 10, not part of the main pipeline.
|
||||
hands-on subject of Phase 10, not part of the main pipeline. **Not ported.**
|
||||
|
||||
**The Grafana stack actually runs on the user's k3s cluster (`falken`), not via
|
||||
Ansible.** `k8s/` is a faithful port of the role: same versions, configs and
|
||||
pipeline, namespace `ledgerline-observability`, deployed with
|
||||
`kubectl apply -k k8s/` (`k` is the user's alias with the right kubeconfig; if
|
||||
the API server is unreachable, `kubectl kustomize k8s/ | ssh falken kubectl
|
||||
apply -f -`). Endpoints are `https://{grafana,prometheus,alertmanager,loki,tempo,alloy,otlp}.vione.cloud`;
|
||||
all but Grafana sit behind basic auth with credentials in `k8s/.credentials`.
|
||||
Ledgerline stays on `czy` (Compose); its container logs reach Loki via an Alloy
|
||||
agent there (`observability/alloy-agent/config.alloy`, in
|
||||
`docker-compose.traefik.yml`) — the Docker-socket half of the original Alloy
|
||||
config. `k8s/README.md` lists every deviation from the Ansible role. Where a
|
||||
roadmap file still says "Ansible role" or "re-run the playbook", read it as
|
||||
"`k8s/`" and "`kubectl apply -k k8s/`".
|
||||
|
||||
Five facts about that platform that change what correct code looks like here. Verify
|
||||
them against the live system rather than assuming — they were read from the configs
|
||||
@@ -47,8 +61,10 @@ at one point in time and the user may have since extended them.
|
||||
separate `/metrics` Prometheus-exposition input. Never tell the user their OTLP
|
||||
metrics "should be arriving"; check Prometheus or the Alloy UI first.
|
||||
2. **App metrics reach Prometheus by direct scrape, not through Alloy.** Alloy has
|
||||
no `prometheus.scrape` component; `prometheus.yml.j2` scrapes the stack's own
|
||||
components plus whatever is listed in `prometheus_extra_scrape_jobs`. Note that
|
||||
no `prometheus.scrape` component; `k8s/prometheus/prometheus.yml` scrapes the
|
||||
stack's own components, and app jobs get added there (the role's
|
||||
`prometheus_extra_scrape_jobs`). czy's service ports are unpublished, so no app
|
||||
scrape path exists yet — a Phase 5 decision. Note that
|
||||
the diagram draws this differently (app metrics routed via Alloy) — the diagram
|
||||
and the code have drifted, and the user knows. Don't "correct" one to the other
|
||||
unasked.
|
||||
@@ -57,15 +73,20 @@ at one point in time and the user may have since extended them.
|
||||
Prometheus with `send_exemplars: true`) — **not** from collector connectors.
|
||||
Never add `spanmetrics` / `servicegraph` to Alloy; it duplicates every series.
|
||||
The metric names differ from the ones in most blog posts — look them up in
|
||||
Prometheus before writing PromQL.
|
||||
Prometheus before writing PromQL. (As shipped, the Ansible role produced none of
|
||||
this: Tempo lacked `traces_storage` and Prometheus lacked
|
||||
`--enable-feature=exemplar-storage`. Both are fixed in `k8s/`; verified live.)
|
||||
4. **Log→trace correlation runs on a case-sensitive regex:**
|
||||
`trace[_-]?id["=: ]+([a-fA-F0-9]+)`. Logs must carry **lowercase `trace_id`**.
|
||||
OpenTelemetry's logging auto-instrumentation emits `otelTraceID`, which does not
|
||||
match. Exemplar labels must also be `trace_id`. The diagram names this outright:
|
||||
"Correlation key: trace_id / exemplars."
|
||||
"Correlation key: trace_id / exemplars." **Known, unresolved:** Tempo's
|
||||
metrics-generator labels its exemplars `traceID` (verified live), which the
|
||||
provisioned `exemplarTraceIdDestinations: name: trace_id` does not match — so
|
||||
span-metric exemplars don't link to Tempo yet. The user decides the fix.
|
||||
5. **There is no dashboard provisioning in the platform** — datasources only.
|
||||
Dashboards-as-code is the user's deliverable and belongs in the Ansible role, so
|
||||
that `ansible-playbook` rebuilds them.
|
||||
Dashboards-as-code is the user's deliverable and belongs in `k8s/grafana/`, so
|
||||
that `kubectl apply -k k8s/` rebuilds them.
|
||||
|
||||
Ledgerline talks to this platform over the network. Endpoints come from env
|
||||
(`OTEL_EXPORTER_OTLP_ENDPOINT` etc.), never hardcoded.
|
||||
@@ -294,42 +315,53 @@ git diff --stat tests/ # did anything touch the contract? should
|
||||
The pathology suite (`make test-pathology`, `make test-slow`) is slow and is an
|
||||
answer key — see spoiler rule 3 before running it.
|
||||
|
||||
**Traces (Tempo, :3200)** — search by service and duration rather than trusting
|
||||
**The platform lives on falken** (see the backend section above). Load the
|
||||
credentials first; every endpoint except Grafana needs basic auth:
|
||||
```bash
|
||||
source k8s/.credentials; A="$OBS_USER:$OBS_PASSWORD"
|
||||
k get pods -n ledgerline-observability # what's actually running
|
||||
```
|
||||
|
||||
**Traces (Tempo)** — search by service and duration rather than trusting
|
||||
that a span exists:
|
||||
```bash
|
||||
curl -s 'http://localhost:3200/api/search?tags=service.name%3Dledger&limit=5'
|
||||
curl -s "http://localhost:3200/api/traces/<trace_id>" | jq '.batches | length'
|
||||
curl -s -u "$A" 'https://tempo.vione.cloud/api/search?tags=service.name%3Dledger&limit=5'
|
||||
curl -s -u "$A" "https://tempo.vione.cloud/api/traces/<trace_id>" | jq '.batches | length'
|
||||
```
|
||||
|
||||
**Metrics (Prometheus, :9090)** — and this is the cardinality check worth
|
||||
**Metrics (Prometheus)** — and this is the cardinality check worth
|
||||
running often:
|
||||
```bash
|
||||
curl -s 'http://localhost:9090/api/v1/query?query=count({__name__=~"ledgerline.*"})'
|
||||
curl -s 'http://localhost:9090/api/v1/status/tsdb' | jq '.data.seriesCountByMetricName[:10]'
|
||||
curl -s -u "$A" 'https://prometheus.vione.cloud/api/v1/query?query=count({__name__=~"ledgerline.*"})'
|
||||
curl -s -u "$A" 'https://prometheus.vione.cloud/api/v1/status/tsdb' | jq '.data.seriesCountByMetricName[:10]'
|
||||
```
|
||||
|
||||
**Logs (Loki, :3100)** — confirm `trace_id` is actually in the payload, not just
|
||||
in the code that was supposed to put it there:
|
||||
**Logs (Loki)** — confirm `trace_id` is actually in the payload, not just
|
||||
in the code that was supposed to put it there. The czy agent labels streams with
|
||||
the full container name (`container` and `service_name` are both e.g.
|
||||
`ledgerline-gateway-1`):
|
||||
```bash
|
||||
curl -s -G 'http://localhost:3100/loki/api/v1/query_range' \
|
||||
--data-urlencode 'query={service_name="gateway"} | json | trace_id != ""' \
|
||||
curl -s -u "$A" -G 'https://loki.vione.cloud/loki/api/v1/query_range' \
|
||||
--data-urlencode 'query={container="ledgerline-gateway-1"} | json | trace_id != ""' \
|
||||
--data-urlencode 'limit=5'
|
||||
```
|
||||
|
||||
**Alloy (:12345)** — the component graph is the fastest way to see what the pipeline
|
||||
actually does, including which signal outputs are wired. Check it before theorising
|
||||
about missing data.
|
||||
**Alloy (https://alloy.vione.cloud)** — the component graph is the fastest way to
|
||||
see what the pipeline actually does, including which signal outputs are wired.
|
||||
Check it before theorising about missing data. The czy log agent has no UI
|
||||
exposed; `ssh czy docker logs ledgerline-alloy-agent-1`.
|
||||
|
||||
**Metric names** — never assume; the Tempo-generated names differ from blog posts:
|
||||
```bash
|
||||
curl -s 'http://<obs-host>:9090/api/v1/label/__name__/values' | jq '.data[] | select(test("trace|span|service_graph"))'
|
||||
curl -s -u "$A" 'https://prometheus.vione.cloud/api/v1/label/__name__/values' | jq '.data[] | select(test("trace|span|service_graph"))'
|
||||
```
|
||||
|
||||
**Grafana (:3000)** — verify provisioning actually loaded rather than reading
|
||||
the YAML and assuming:
|
||||
**Grafana (https://grafana.vione.cloud)** — verify provisioning actually loaded
|
||||
rather than reading the YAML and assuming:
|
||||
```bash
|
||||
curl -s -u admin:admin 'http://localhost:3000/api/datasources' | jq '.[].name'
|
||||
curl -s -u admin:admin 'http://localhost:3000/api/search?type=dash-db' | jq '.[].title'
|
||||
G="$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD"
|
||||
curl -s -u "$G" 'https://grafana.vione.cloud/api/datasources' | jq '.[].name'
|
||||
curl -s -u "$G" 'https://grafana.vione.cloud/api/search?type=dash-db' | jq '.[].title'
|
||||
```
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
Reference in New Issue
Block a user