Repository navigation
v0.3.0
The hub runs the department's telemetry in production, with one spoke on the
contract (hostname, labels, templates pinned by tag). This release hardens the
stack around that contract and makes the checks prove the data paths, not just
the config syntax. 1.0 waits for a second consumer to confirm the contract.
Upgrade
- Point every spoke's
OTEL_EXPORTER_OTLP_ENDPOINTatotel.<domain>. The
oldotlp.<domain>name is gone. - Set
COMPOSE_FILEin.envto name the overlays this host runs
(compose.yml:compose.tunnel.ymlin production).just up-tunnelis gone;
just upruns the exposure guards whenever the tunnel overlay is active. - Remove the orphaned
alertmanager_datavolume when convenient. Alerting is
Grafana-managed (ADR 0002). - Re-vendor the templates on each spoke at
v0.3.0(bootstrap.shprints
the commands): the Alloy agent gains a memory limiter, keeps only the four
cAdvisor metrics this stack reads, and drops its own histogram buckets; the
overlay declares theegressnetwork it joins. - Loki streams keep their old label set until they age out (30 days).
- Set
projectas well asenvin every sender's
OTEL_RESOURCE_ATTRIBUTES. A sender that omits either is now counted as
unknownand raisesProjectsUncoveredinstead of arriving unattributed
and unnoticed.
Added
- Per-user Grafana logins through Cloudflare Access:
GRAFANA_JWT_AUTH
withCF_ACCESS_TEAM_DOMAINandCF_ACCESS_AUD. The exposure guards
enforce the pair, and the smoke test boots with it on. - Durable export queue: the collector's send queues live on a file-backed
otel_queuevolume, so telemetry buffered during an outage survives a
collector restart. A one-shototel-queue-initservice owns the volume for
the collector, so a plaindocker compose upworks anddown --volumes
reclaims it. - Self-monitoring covers every service: Prometheus scrapes Grafana, Loki
and Tempo too.HostDiskFilling(full within 3 days at the current rate)
andPrometheusCardinalityHigh(over 100k active series) warn ahead of the
80% disk backstop. - Per-project ingest counters: the gateway counts what arrives and emits
telemetry_{datapoints,logs,spans}_totalby project and environment
(metricsandspaneventsare counted once, without labels). The rules
matchtelemetry_.+_total;ProjectTelemetrySilentandProjectsUncovered
key on those instead of scanning every project-labelled series, so their cost is flat in
fleet size, and a project that sends only logs or only traces is covered at
last: it reached no Prometheus series before. Stack Health gains an Ingest
by Project panel. grafana_access_team_domainjoins the OpenTofu outputs, so neither
Access value is copied out of the dashboard by hand. Grafana builds its JWK
set URL from the team name, and an unset one fetches signing keys from a
claimable subdomain.infra/generate-imports.shemits OpenTofuimportblocks for the
tunnel, DNS records and Access app built by hand, so the first plan does
not create duplicates.just smokeproves the stack works, not that it boots: every dashboard
and alert rule provisioned, uid for uid, none paused; contact points carry
the exact URLs given; Grafana honours the JWT settings and refuses a forged
token; every scrape target is up; a metric and a log round-trip through
the collector into Prometheus and Loki with theproject,envand
departmentlabels. WithSMOKE_ALERTS=1(on in CI) it also stops a
scrape target and waits forTargetDownto fire.just restore-check
rehearses backup and restore on the smoke volumes.just checkislintplusvalidate.lintcovers compose files,
the rendered alert templates, YAML formatting, workflows, shell scripts,
the demo's Python, OpenTofu formatting, dashboard JSON and the datasource
uids it names, and git history for secrets.validateruns each stack
config through the exact image the stack uses.just hooksinstalls git
hooks via prek: gitleaks at commit, a Conventional Commits check on the
message,checkat push,infra-validateat push wheninfra/changed.- Dependabot watches the spoke images in
templates/and the Cloudflare
provider ininfra/. Patch bumps arrive grouped.
Changed
- Loki indexes only the identity labels (
service.name,department,
project,env,host.name). Everything else is structured metadata, so
a sender restart no longer mints a new stream. - Half the series are gone. Prometheus drops the histogram buckets from
the Grafana, Loki and Tempo self-scrapes, the hub's node-exporter runs the
same nine-collector allowlist the spokes' Alloy does, and the agent keeps
only the four cAdvisor metrics this stack reads and drops its own buckets.
Measured on the hub with one spoke: 14.1k active series to 7.0k, 438
samples/s to 206. Nothing that a dashboard, alert or runbook reads was
dropped;_sumand_countsurvive, so latency is still there to explore.
PrometheusCardinalityHigh's 100k ceiling is ~14x the baseline now. - Dashboards are provisioned, not editable:
dashboards/*.jsonis the
source of truth; UI saves are off. - Tempo stores traces and nothing else: its metrics generator is gone.
RED comes from the applications' own OTLP metrics (ADR 0002). - CI runs on pull requests only and validates
infra/on its own workflow.
lintandvalidate+smokerun on separate jobs.just demo-buildis
gone, and nothing in CI builds the demo image;just demois the check. just smokeandjust demoeach run under their own compose project and
Grafana port (compose.sandbox.yml), so neither can touch a running stack.- Image bumps: Grafana 13.1.4, Loki 3.7.6, Tempo 3.0.3, Prometheus 3.13.2,
the collector 0.156.0, node-exporter 1.12.1, cloudflared 2026.8.2, and the
demo's Python dependencies. Therelab-apidashboard is dropped; the
$servicepicker on Service Health covers it.
Fixed
HighErrorRatemeasured the wrong thing twice: it counted all spans,
so child spans diluted the ratio, and it aggregated by job alone, so a
healthy prod service masked a broken staging one. It now reads the HTTP
server metrics, keyed on job, project and env.- Both onboarding templates and the demo set
envbut notprojectin
OTEL_RESOURCE_ATTRIBUTES, so an app that followed them shipped telemetry
no alert or dashboard could attribute to a project. - Trace links from the latency panel resolved to nothing: exemplars carry
traceID, the datasource looked fortrace_id. - The Infrastructure Logs dashboard queried labels this stack never set. The
GPU dashboard's host picker keyed oninstance, identical on every host. - The spoke overlay joined an
egressnetwork it never declared, so the
documentedup -dfailed on a host without one.just lintrenders both
spoke overlays now. - With
GRAFANA_JWT_AUTH=trueand no team domain, Grafana fetched signing
keys from a placeholdercloudflareaccess.comsubdomain any customer could
claim. There is no fallback now, and the guards refuse to start. - Checks that could pass without checking: the exposure-guard self-test
dropped the status of its positive half; the smoke provisioning check
accepted an empty dashboard list;just backupexited 0 with the stack
still paused when the unpause failed;bootstrap.shprinted the hash of
empty input for a template missing from the tag;generate-imports.sh
reported a failed DNS API call as "no record".bootstrap.shnow reads
its rule back from Grafana after the restart, and says so when the admin
password is missing or rejected instead of blaming the rule. TargetDownandHostDiskSpaceLowcould never fire on the state they
watch:up == 0and a 0% free ratio both evaluate to 0, and the threshold
node fires on value > 0.HostDiskFillinghad the same defect from a
negative projection. All three carryboolnow, and the smoke alert
round trip guards the contract.just backuplost tar's exit status behind the gzip pipe; a partial
archive reported success.just restore-checkno longer inherits the
host'sCOMPOSE_FILE.- The stack-health dashboard's host panels averaged spoke node metrics into
the hub's CPU, memory and disk. They are scoped to the hub'snodejob. - Grafana and the collector wait for Prometheus to report ready instead of
racing it on a cold start. The demo load generator's error rate matches
its advertised one in ten.
Security
- Every service drops all capabilities, runs with
no-new-privileges, and
carriesmem_limitandpids_limitsized from observed usage.
node-exporter and cloudflared run read-only; the demo runs asnobody. - Loki, Tempo, Prometheus and node-exporter sit on an internal
backend
network only Grafana and the collector join. cloudflared stays outside it,
so an ingress edited in the Cloudflare dashboard cannot reach a backend
with no authentication of its own. GRAFANA_COOKIE_SECUREmarks the session cookie Secure with strict
SameSite. With the tunnel overlay,just uprefuses to start without it,
anhttps://root URL, non-default credentials, a webhook URL, and with
.envor the OpenTofu state readable by other local users.
just lintruns those guards both ways.- Hub images are digest-pinned, GitHub Actions are pinned to commit SHAs, the
tunnel token reaches cloudflared via environment rather than argv, the
smoke test's ingest token reaches curl through its stdin config, and
bootstrap.shprintssha256sum -clines for the files a spoke vendors.