Skip to content

feat(api): expose host system logs at /api/v2/system/logs - #3423

Merged
vpetersson merged 13 commits into
masterfrom
agent/anthias-claude/f2738da58d1c
Nov 3, 2026
Merged

vpetersson merged 13 commits into
masterfrom
agent/anthias-claude/f2738da58d1c

Conversation

@vpetersson-bot

@vpetersson-bot vpetersson-bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Issues Fixed

Anthias has no way to surface host/system logs. bin/collect_debug.sh shells out to journalctl on bare metal, but that is a manual operator script — nothing in the app or API can read the journal.

Description

Adds GET /api/v2/system/logs: a snapshot-only journal read with one contract on both supported platforms. No follow on either — the bare-metal backend runs inside the host agent's single-threaded subscriber_loop, where a streaming read would queue reboot/shutdown behind it forever, and offering a live tail on Balena alone would mean two divergent contracts.

Parameters: count (default 500, hard cap 2000), unit, container, since, until.

Balena — supervisor POST /v2/journal-logs. No compose change and no new privilege; anthias-server already carries io.balena.features.supervisor-api. Requests format: json so the payload is shaped identically to bare metal and one normaliser serves both backends.

Bare metal — anthias-host-agent over hostcmd, now accepting a JSON payload with a correlation ID alongside the original bare-string commands (so rollout order does not matter). The reply goes onto a correlation-keyed Redis list with a TTL and is collected with a bounded BLPOP — a blocking pop rather than a completion pub/sub, because it has no subscribe-before-publish race and matches the anthias.reply.<id> pattern already used on the viewer bus.

Nothing in the path can block subscriber_loop: bounded by lines, by bytes and by a subprocess timeout, and every failure path still writes exactly one reply so the container's pop resolves instead of timing out.

Also here:

  • SupplementaryGroups=systemd-journal on the host-agent unit, plus a hard refusal when the system journal is unreadable. journalctl exits 0 for a caller outside that group and hands back the service account's own user journal with every system record missing — and under --output=json it does not even print its stderr hint, so there is no signal to key off. The agent asks the kernel (os.access) instead, which also honours the ACL journald grants to adm.
  • Journald persistence and size caps in the Ansible role, off by default behind anthias_journal_persistent. See the correction at the bottom: a player writes 19-28 MiB of journal a day, and Raspberry Pi OS pins Storage=volatile to keep that off the SD card deliberately. The endpoint does not need persistence, so the role no longer overrides that choice. When an operator does opt in, 99-anthias.conf wins over 40-rpi-volatile-storage.conf and is followed by journalctl --flush, because a restart alone leaves the journal in /run until the next boot.
  • persistent is reported in the response rather than assumed, measured from which of /var/log/journal and /run/log/journal holds the live journal. Both cheaper probes give a wrong "yes" on hardware we ship on.
  • Secret-looking values (psk, passphrase, tokens, Authorization) are blanked on the way out, keeping the key visible.

Note for operators: persistence is opt-in (anthias_journal_persistent: true). When enabled, SystemMaxUse=64M is enforced by vacuuming, so on an install whose journal already exceeds it the first journald restart discards the excess. A QA x86 board had grown to 1.8 GB and came back at 56 MB. Override anthias_journal_max_use where the longer history matters more than the card.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

End-to-end on the QA rig (Pi 5 / aarch64 and x86, both Debian 13), host agent and server overlaid from this commit with hashes verified in place: snapshot, every filter, parameter rejection, five concurrent requests each getting their own result, reply TTL, 2000-line read in 0.49s / 0.80s with the next host command answered ~250ms later, and redaction of a real logger-injected line. Both boards restored to their pinned images afterwards.

Verified against the real OS permission state on both boards: with the supplementary group withheld, journalctl -u docker.service returns 0 lines and exit code 0; with it, 50. The agent now refuses that case with an actionable error instead of serving a silently empty view.

The Balena backend was exercised over real HTTP inside the server container against a supervisor stub — correct URL, version and API key, correct body, follow: false, identical normalised output, redaction, and the 4 MiB cap holding on a streamed 9 MB response. It has not been run against a real balenaOS supervisor; the rig has no balena device.

🤖 Generated with Claude Code


Review follow-ups

Six findings from a review pass, all fixed and re-verified on the Pi 5 and x86 boards:

  • redact() was quadratic. A 48 KB line (journald's default LineMax) took about a minute and pinned a request worker; measured 6.3 s at 16 KB. A lookbehind means only a real token start can begin a match — 48 KB is now 0.003 s, with every redaction case unchanged.
  • The Balena path kept the first 4 MB while the host agent kept the last. journalctl emits oldest-first, so the supervisor backend was discarding exactly the entries an operator opened the log to read. It now trims from the front, and bounds total bytes read as well as bytes kept.
  • container returned an empty 200 on Balena. It means a name on both platforms, but the supervisor only matches CONTAINER_ID_FULL. It is now resolved via /v2/containerId, an ID is passed straight through, and an unknown service fails loudly instead of looking like a quiet device.
  • A stopped host agent blocked every request for the full 30 s BLPOP. PUBLISH reports its subscriber count; zero means no reply is coming. Now 502s in ~0.02 s.
  • text=True decoded journalctl output with the locale encoding and strict errors — and a systemd unit has no LANG, so that is ASCII. One non-ASCII byte would have raised out of process_message and killed subscriber_loop, the exact failure this feature is built to avoid.
  • count help_text claimed "most recent first"; entries are oldest-first. It is published in the OpenAPI schema.

Chasing the first SonarCloud finding also turned up a real gap: creating /var/log/journal ourselves skipped the a+ tmpfiles line that grants the adm group its read ACL, so adm-based journal access would silently not have worked on a host where we made the directory. The role now runs systemd-tmpfiles --create, the other half of the recipe documented in journald.conf(5).


Hardware QA follow-up

Full integration run across the QA rig — x86_64, aarch64 (Pi 3/4/5) and armv7l (Pi 2, Pi 3 A+), Debian 13 and Raspbian 13 — turned up one defect in this branch, now fixed here.

The blocking pop was not bounded by its own timeout. BLPOP ran on the shared connect_to_redis() client, which takes redis-py's defaults: socket_timeout=5 with Retry(retries=10). A 30s pop cannot survive a 5s socket read, so the socket expired first and the retry ladder re-issued the pop ten more times. Measured at 59.6s on x86 and 58.2s on a Pi against a host agent that was subscribed but not answering — roughly twice REPLY_TIMEOUT_SECONDS, which bounded nothing. It also made the reply is None branch unreachable, so the case surfaced as "Could not reach the host agent over Redis", pointing an operator at a healthy broker.

The path is reachable on a normal upgrade: with the container updated before the host agent, the old agent is still subscribed, so PUBLISH reports a non-zero subscriber count and the fast-fail for a stopped agent does not apply.

The blocking pop now gets its own client — socket outliving the pop timer, no retries — verified on hardware: 30.05s and the accurate "Check that anthias-host-agent is running and up to date" message. A stopped agent still fails in ~0.015s, and slow-but-completing reads are unaffected (six queued 2000-line reads on a Pi 2, waits to 17.7s, all delivered in full).

Also confirmed on hardware, alongside 24/24 API checks on five boards:

  • The Ansible role is idempotent, and 99-anthias.conf beats Raspberry Pi OS's 40-rpi-volatile-storage.conf — verified both by systemd-analyze cat-config and by the journal physically moving out of /run.
  • persistent measured rather than inferred is doing real work: a Pi 5 with 8 stale files in /var/log/journal while journald wrote to /run correctly reported false. Both cheaper probes would have lied on that board.
  • Reboot test — a line written before a reboot issued through /api/v2/reboot was readable through the API afterwards, and previous-boot entries stayed reachable.
  • Truncation keeps the newest entries (120 × 47 KB lines: capped at 3.90 MiB, oldest dropped), and redact() is linear — 1.40s for a 2000-line read containing 87 × 47 KB lines.
  • 2000-line read latency: x86 0.48s, Pi 5 0.30s, Pi 4 0.81s, Pi 3 1.70s, Pi 2 4.20s.

Two notes for reviewers, neither a code change:

  • The permission claim above is narrower than written. On these images the install user is already in adm, and journald grants adm journal access by ACL, so journalctl -u docker.service returns rows without SupplementaryGroups=systemd-journal. Withholding adm as well reproduces the silent empty read exactly as described, so the grant is worth keeping as defence in depth — it just isn't what makes logs readable on a stock install.
  • The SystemMaxUse warning is real and worth repeating in release notes: on the x86 board the first journald restart took the journal from 1.8 GB to 56 MB, discarding about 3.7 months of history.

since/until already cover relative and absolute ranges (-5m, -2h&-1h, @<epoch>), but two sharp edges are worth knowing for any UI built on this: the API's own timestamp is not valid input (the allowlist has no .), and absolute timestamps are read in the device's local timezone while the API returns UTC — the mismatch yields a 200 with an empty list rather than an error. Both are left as-is here and tracked for the follow-up that surfaces this in the web UI.


Correction: persistent journald is no longer enabled by default

The first version of this branch wrote Storage=persistent on every install, overriding the Storage=volatile that Raspberry Pi OS pins in 40-rpi-volatile-storage.conf. That override was wrong. The vendor setting exists to keep journald off the SD card, and a player's journal is not small:

Board Journal written
Pi 5 (aarch64) 19-25 MiB/day — ~7-9 GiB/year
Pi 2 (armv7l) 21-28 MiB/day — ~8-10 GiB/year
x86 27 MiB/day

Measured over 1h/6h/24h windows on idle QA players carrying the normal Anthias stack, before any real content churn. The bulk is our own containers: anthias-server and anthias-celery together account for roughly half the entries. It is also the write pattern SD cards handle worst — small appends with a periodic fsync, amplified by the card's internal block management.

The endpoint never needed it. The host agent reads the volatile journal in /run perfectly well. Verified on a Pi 5 left on the image default: GET /api/v2/system/logs returns 500 entries with persistent: false, and unit, container and since filters all work — 24/24 of the API suite passes with journald writing nothing to the card. Persistence only buys history that survives a reboot, and persistent in the response already tells a client which one it got, so a device on the default is honest about showing current-boot-only logs rather than silently truncated ones.

So it is now anthias_journal_persistent, defaulting to false:

  • With the flag off the role writes nothing and leaves the image default intact — verified on a Pi 5, where a full role run skips all four persistence tasks and leaves Storage=volatile with the journal still in /run.
  • It also removes 99-anthias.conf when the flag is off, so flipping it back restores the vendor setting instead of stranding our override. Verified by enabling, confirming Storage=persistent with the journal moved to /var/log/journal, then disabling and watching it converge back to volatile and /run.
  • Creating /var/log/journal moved inside the conditional too. On an image without the Pi vendor drop-in, Debian's Storage=auto means creating that directory is itself the persistence switch.
  • SupplementaryGroups=systemd-journal on the host-agent unit is unaffected — that is what actually makes the journal readable, and it costs nothing in writes.

Both tunables moved from vars/ to a new defaults/, because role vars outrank inventory and group_vars: a per-device override set the normal way would have been silently ignored from where they were.

Anthias had no way to read the host journal from inside the container.
This adds a snapshot-only endpoint with one contract on both platforms.

- Balena: supervisor POST /v2/journal-logs. No compose or privilege
  change -- the supervisor-api label is already present. Requests
  format=json so the payload matches bare metal and one normaliser
  serves both.
- Bare metal: anthias-host-agent over hostcmd, with correlation IDs,
  line and byte caps, and a Redis TTL on replies. Bare-string hostcmd
  payloads still work, so rollout order does not matter.
- Nothing in the path can block subscriber_loop: no follow on either
  platform, every read bounded by lines, bytes and a subprocess
  timeout, and every failure path still writes a reply.
- Grant the host agent journal access with
  SupplementaryGroups=systemd-journal, and refuse the request outright
  when the system journal is unreadable -- journalctl exits 0 and
  returns the service account's own user journal otherwise.
- Make journald persistent and size-capped in the Ansible role.
- Redact secret-looking values (psk, passphrase, tokens) on the way
  out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vpetersson-bot
vpetersson-bot requested a review from a team as a code owner October 26, 2026 06:43
Copilot AI lite review requested due to automatic review settings October 26, 2026 06:43
@vpetersson-bot vpetersson-bot self-assigned this Oct 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

Comment thread src/anthias_server/api/views/v2.py Fixed
Comment thread src/anthias_server/api/views/v2.py Fixed
Copilot AI lite review requested due to automatic review settings October 26, 2026 07:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@vpetersson-bot
vpetersson-bot requested a lite review from Copilot October 26, 2026 07:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@vpetersson-bot

Copy link
Copy Markdown
Contributor Author

The SonarCloud gate failure comes from three ansible:S2612 ("granting access to others") findings in ansible/roles/anthias/tasks/main.yml, on lines 84, 94 and 105. All three are false positives, and I'm not changing the modes:

  • /var/log/journal, mode 2755 (root:systemd-journal). This is the mode that systemd's own tmpfiles.d/systemd.conf creates. Others need x on the directory so that unprivileged users can still read their own user-<uid>.journal, which journald grants through ACLs. The journal files themselves are 0640. Tightening the directory would break journalctl --user and gain nothing.
  • /etc/systemd/journald.conf.d, mode 0755. This is the standard mode for a config directory under /etc/systemd. Nothing secret is stored there.
  • 99-anthias.conf, mode 0644. It only holds Storage= and size caps. Like every other journald.conf drop-in, it is meant to be world-readable.

The new code introduces no actual vulnerabilities. CodeQL and the rest of CI pass, and the three CodeQL exception-exposure alerts were fixed in b069f54. Someone with SonarCloud access needs to mark these three findings as Safe to clear the gate.

@vpetersson-bot
vpetersson-bot requested a lite review from Copilot October 26, 2026 08:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

Copilot AI lite review requested due to automatic review settings October 26, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@vpetersson-bot
vpetersson-bot requested a lite review from Copilot October 26, 2026 08:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

vpetersson-bot and others added 4 commits October 26, 2026 09:04
CodeQL flagged str(exc) flowing into API responses. JournalUnavailable
and JournalRequestError now carry a composed user_message, which the
view and serializer return instead. Supervisor and Redis transport
errors are logged rather than interpolated into the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
SonarCloud S2612 flagged the three new `mode:` values. All three match
what the OS itself ships, so they are annotated rather than tightened
-- but chasing the first one down found a real gap: creating
/var/log/journal ourselves skips the `a+` tmpfiles line that grants
the `adm` group its read ACL, so adm-based journal access silently
would not work on a host where we made the directory.

- Run `systemd-tmpfiles --create --prefix=/var/log/journal`, the other
  half of the recipe documented in journald.conf(5)
- NOSONAR with reasons: systemd pins /var/log/journal to 2755 and
  resets anything narrower; 0755/0644 match /etc/systemd's own files

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
- redact(): the key=value pattern was quadratic. A 48 KB line
  (journald's default LineMax) took ~a minute and pinned a request
  worker; measured 6.3s at 16 KB. A lookbehind makes only a real token
  start a match, taking it back to linear -- 48 KB now 0.003s.
- Balena truncation kept the FIRST 4 MB while the host agent kept the
  last. journalctl emits oldest-first, so the supervisor path was
  discarding exactly the entries an operator opened the log to read.
  Trim from the front, and bound total bytes read as well as kept.
- `container` means a name on both platforms, but the supervisor only
  matches CONTAINER_ID_FULL, so forwarding the name returned an empty
  200 on Balena. Resolve it via /v2/containerId, pass an ID straight
  through, and fail loudly on an unknown service.
- A stopped host agent made every request block for the full 30s
  BLPOP. PUBLISH reports its subscriber count; zero means no reply is
  coming, so fail now.
- journalctl output was decoded with `text=True`, i.e. the locale
  encoding with strict errors -- and a systemd unit has no LANG, so
  that is ASCII. One non-ASCII byte would raise out of
  process_message and kill subscriber_loop, the exact failure this
  feature is built to avoid.
- count help_text claimed "most recent first"; entries are oldest
  first. It is published in the OpenAPI schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
They were sitting in the comment block above each `mode:`, where the
analyzer does not see them -- SonarCloud kept reporting all three.
Inline is also what the working examples in roles/system/tasks use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vpetersson-bot
vpetersson-bot force-pushed the agent/anthias-claude/f2738da58d1c branch from 388697b to 84190b8 Compare October 26, 2026 09:05
Translate supervisor body-read failures into the existing 502 response and always close the response. Request full journal fields on bare metal while draining stdout and stderr into bounded buffers under one subprocess deadline; kill and reap the child on failure.

Add regressions for streaming failures, long messages, output caps, UTF-8 decoding, and timeout cleanup. Existing redaction performance and newest-entry truncation regressions also pass.

Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 26, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

`host_agent.subprocess` is an implicit re-export, which mypy rejects
under no_implicit_reexport -- it broke run-mypy on the branch. It is
the same module object as the stdlib `subprocess` the test already
imports, and _run_journalctl resolves `subprocess.Popen` through its
own globals, so patching the module directly is identical in effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 26, 2026 10:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

vpetersson-bot and others added 2 commits October 26, 2026 19:50
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 26, 2026 19:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

vpetersson
vpetersson previously approved these changes Oct 27, 2026
The BLPOP in the bare-metal journal backend ran on the shared
connect_to_redis() client, which takes redis-py's defaults:
socket_timeout=5 with Retry(retries=10). A 30s blocking pop cannot
survive a 5s socket read, so the socket expired first, the retry
ladder re-issued the pop ten more times, and the call raised
redis.TimeoutError about a minute later instead of returning None.

Two things were wrong as a result:

- REPLY_TIMEOUT_SECONDS bounded nothing. The real bound was the retry
  ladder, at roughly twice the intended budget. Measured end to end
  on the QA rig at 59.6s on the x86 board and 58.2s on a Pi, against
  a host agent that was subscribed but not answering.
- The `reply is None` branch was unreachable, so that case surfaced
  as "Could not reach the host agent over Redis" — pointing an
  operator at the broker while the broker was healthy. The accurate
  message, telling them to check that anthias-host-agent is running
  and up to date, could never be reached.

The path is reachable on a normal upgrade: when the container is
updated before the host agent, the old agent is still subscribed, so
PUBLISH reports a non-zero subscriber count and the existing
fast-fail for a stopped agent does not apply.

Give the blocking pop its own client with a socket that outlives the
pop timer and no retries. A blocking pop is not idempotent the way a
GET is — re-issuing it after a socket timeout restarts the wait from
zero, which is how one bounded request became a minute-long stall.

The same redis-py defaults are already documented as a trap in
lib/cec_client.py and celery_tasks.py; they are right for the short
round trips every other call site makes, so they are widened here
rather than in the shared helper. connect_to_redis() grows a kwargs
passthrough so this stays the single place that knows where redis
lives; every existing caller is unaffected.

Slow-but-completing reads are unaffected: the pop now waits its full
budget in one attempt rather than restarting, and a stopped agent
still fails in ~0.02s via the subscriber-count check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 27, 2026 09:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

connect_to_redis() now takes a kwargs passthrough, and
lib.system_logs._reply_connection uses it to widen the socket for its
blocking pop. The session-wide stub in conftest was a zero-argument
lambda, so any test reaching the real helper would have hit a
TypeError rather than the fake. mypy caught it as a signature
mismatch; the runtime break was the same bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 27, 2026 09:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

vpetersson
vpetersson previously approved these changes Oct 27, 2026
Raspberry Pi OS pins Storage=volatile in
40-rpi-volatile-storage.conf to keep journald off the SD card. That is
a deliberate choice, not an oversight, and this role was overriding it
on every install — the 99- prefix on our drop-in existed specifically
to win that fight.

A player writes more journal than the feature is worth paying for.
Measured on idle QA boards running the normal Anthias stack: 19-28 MiB
a day, roughly 7-10 GiB a year, before any real content churn. It is
also the shape of write SD cards handle worst — small appends with a
periodic fsync, amplified by the card's internal block management.

/api/v2/system/logs never needed it. The host agent reads the volatile
journal in /run just as well; verified on Pi 2/3/4/5 before the role
ran, where the endpoint returned entries with persistent=false. The
only thing persistence adds is history that survives a reboot, and the
API already reports which one answered so a device on the default is
honest about showing current-boot-only logs.

So: anthias_journal_persistent, defaulting to false. When it is off the
role writes nothing and leaves the image default intact — and it now
removes our drop-in if it is present, so flipping the flag back
actually restores the vendor setting instead of stranding the
override. Creating /var/log/journal moved inside the conditional too:
on an image without the Pi vendor drop-in, Debian's Storage=auto means
creating that directory is itself the persistence switch.

Both tunables move from vars/ to defaults/, because role vars outrank
inventory and group_vars — a per-device override set the normal way
would have been silently ignored from where they were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 27, 2026 09:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

Picks up 77d2c39 (#3427), which ruff-formats the Python snippet in
website/content/agents.md. That file is untouched by this PR, but the
branch predates the fix, so `ruff format --check .` failed in CI on a
file this change never modified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings November 2, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

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.

4 participants