Skip to content

docs(monitoring): say in config.alloy why an edit needs a deploy - #420

Merged
PBernaerts merged 1 commit into
mainfrom
docs/alloy-config-dep6-header
Aug 21, 2026
Merged

docs(monitoring): say in config.alloy why an edit needs a deploy#420
PBernaerts merged 1 commit into
mainfrom
docs/alloy-config-dep6-header

Conversation

@PBernaerts

Copy link
Copy Markdown
Owner

Merge this AFTER #419, not before. It is deliberately a content-only change
to monitoring/alloy/config.alloy, and that ordering is what turns it into the
proof that #419 works.

What it adds

A header comment on config.alloy explaining that Alloy reads the file once at
startup, that it is a single-file bind mount whose inode a git pull swaps, and
that what actually applies an edit is the DEP-6 homelab.config-sha label added
in #419. The warning belongs where somebody is standing when they edit the file,
not only in the Ansible role defaults.

The consequence is worth stating there too: these stages decide which log lines
reach Loki at all, so a stage that silently never loaded keeps discarding lines
we meant to keep, and the empty query reads as health.

Why it exists as its own PR

#419 adds a labels: block to a service that had none. That is a change to the
service definition, so the first deploy after #419 recreates alloy no matter
what the digest does. Proving the fix with that deploy proves only that Compose
noticed a new label.

The digest path needs a deploy where the ONLY thing that changed is the bytes of
config.alloy. That is exactly this PR. Before #419 this change would have
recreated nothing at all.

Proof sequence on the server, after both are merged

  1. Read the baseline: docker inspect alloy --format '{{.Created}}' and the
    homelab.config-sha label.
  2. Pull main with fix(monitoring): make a deploy actually apply an Alloy config change #419, run deploy.yml. Alloy recreates (new label block).
  3. Run deploy.yml again with no changes: changed=0 (DEP-4).
  4. Pull main with this PR, run deploy.yml, no --force-recreate. Alloy
    must recreate and the label digest must differ from step 3. That is the
    proof.
  5. Run deploy.yml again: changed=0.

Re-read the baseline immediately before step 4 rather than trusting an older
reading. On a shared checkout another session's correct, unrelated deploy can
move it, and reasoning after the fact about which deploy caused a recreate is
exactly the ambiguity this sequence exists to avoid.

Checks

  • alloy validate against the pinned grafana/alloy:v1.18.1, exit 0, so the
    comment does not break the config that carries every log line to Loki
  • pre-commit run --all-files green

Alloy reads this file once at startup and it is a single-file bind mount, so
a git pull replaces it by rename and the container keeps the old inode. Plain
`docker compose up -d` applies nothing. Since #419 the DEP-6 hash label does
apply it, and this header says so at the place somebody is standing when they
edit the file, rather than only in the role defaults.

It matters here more than for most configs: these stages decide which log
lines reach Loki at all, so a stage that silently never loaded keeps
discarding lines we meant to keep, and the empty query reads as health.
@PBernaerts
PBernaerts merged commit 2c3a219 into main Aug 21, 2026
9 checks passed
@PBernaerts
PBernaerts deleted the docs/alloy-config-dep6-header branch August 21, 2026 12:41
PBernaerts added a commit that referenced this pull request Aug 21, 2026
…g edits (#422)

Two more single-file bind mounts were missing from the DEP-6 hash list, so
ansible-playbook deploy.yml never applied an edit to either. Same shape as
#419 fixed for Alloy and #328 for Alertmanager: a git pull replaces the file
by rename, the container keeps the old inode, and plain `up -d` no-ops,
because Compose reacts to image, env and mount definition changes but never
to the bytes of a mounted file. The play still reports changed and nothing
errors, so forgetting the hand-run force-recreate looks exactly like success.

loki-config.yml. Loki has no reload endpoint at all. Only a separate
-runtime-config.file reloads live, and only for the limits and KV store
components, which we do not use, so every setting in this file is read once
at process start. It holds retention_period and reject_old_samples_max_age,
so a retention edit that silently never loaded either deletes logs we
believed we were keeping or fills the disk. Both look like a healthy system
right up until the damage is done.

tautulli-exporter/main.py. It is bind-mounted over /app/main.py and Python
reads the source once when the process starts. It is a vendored patch of
upstream carrying five local behaviour fixes, two of which define metrics we
dashboard on, so an edit that never loaded leaves the exporter serving the
old wrong numbers while every health signal stays green.

Recreating either is cheap. Loki is down for seconds, well inside LokiDown's
5m and LokiLogIngestionStalled's 20m; Alloy's loki.write retries a refused
connection, the ingester WAL sits on the persistent ${APPDATA}/loki mount
and replays anything unflushed, and nothing in the project depends_on loki.
The exporter loses a few seconds of scrape, covered twice over by its
patched 120s startup grace and by ProbeDown's 5m window.

monitoring/socket-proxy/haproxy.cfg is deliberately NOT included. It is
startup-only too, but DEP-6 recreates one service and that one needs two:
restart-socket-consumers.service recreates socket-proxy-monitoring and then
runs `docker restart alloy`, because Alloy's docker tailers have to
reconnect through the fresh proxy, and Compose depends_on orders startup
without restarting dependents. Wiring the label alone would reproduce the
first half of that unit and skip the half that keeps Loki ingesting, on the
exact path that once went dark for 29h with a 20m detection lag. Proving it
either way means recreating socket-proxy on the live server and watching
ingestion recover, which needs its own session. The exposure left open is
small: that file is a vendored copy of the image's own template, its
documented edit trigger is an image bump, and an image pin change already
recreates the container by itself.

Proof of this PR needs two deploys. The labels: blocks are new, so the first
deploy after this merges recreates both services whatever the digest does,
and proves only that Compose noticed a new label. A follow-up content-only
edit is what tests the fingerprint path, the way #420 did for config.alloy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant