hyperhive/hive-log
Repository files (latest commit first)
Filename Latest commit message Latest commit date
atlas 8cc7f90c98 log: send records natively to journald, keep stdout off-unit
A record written to stdout carries no priority, so journald files the
whole stream at one level and the swarm log store shows `info` whatever
level `tracing` gave it. Under a systemd unit the process's stdout
already *is* the journal, so the fix is to speak the journal protocol
directly and let each record carry its own severity.

New `hive-log` crate holds the one sink chooser, called by `hive-c0re`,
`hive-agent` and `swarm-controller`. It builds the same `EnvFilter`
those binaries always built, then installs exactly one layer — never
both, since a journald layer stacked on the `fmt` layer under a unit
stores every record twice.

The choice is an fstat compare, not a presence test: a child inherits
`$JOURNAL_STREAM` even when its own stdout was redirected elsewhere, so
the variable existing proves nothing. The crate parses `dev:inode` out
of it and compares both numbers against an fstat of stdout, the
descriptor the `fmt` layer writes to by default. No match, unset, or
unparseable takes the `fmt` branch. A journald layer that fails to
construct despite a match falls back to `fmt` and warns through it —
a process must never fail to start because of its logger.
2026-09-21 15:52:57 +02:00
..
src log: send records natively to journald, keep stdout off-unit 2026-09-21 15:52:57 +02:00
Cargo.toml log: send records natively to journald, keep stdout off-unit 2026-09-21 15:52:57 +02:00
README.md log: send records natively to journald, keep stdout off-unit 2026-09-21 15:52:57 +02:00

hive-log

One shared log-sink chooser for the hyperhive daemons: hive_log::init() builds the EnvFilter every binary built before, then installs exactly one sink.

Why one sink and not both

Under a systemd unit the process's stdout already is the journal. Adding a journald layer on top of the fmt layer therefore stores every record twice — once as the native journal entry, once as the text line fmt wrote to a descriptor that leads to the same journal.

Writing to stdout also throws the severity away. A stdout line carries no priority field, so journald files the whole stream at one level and the swarm log store shows info whatever level tracing gave the record. Logging natively carries the level across. That is the reason to prefer journald where a journal exists, and the reason never to run both layers.

How the choice happens

systemd sets $JOURNAL_STREAM to the device and inode numbers of the journal-connected descriptor, in decimal, separated by a colon.

Checking that the variable exists is not enough. A child process inherits $JOURNAL_STREAM from its parent even when the parent redirected that child's stdout somewhere else, so presence alone reports a journal that the process does not actually write to. hive-log instead stats stdout — the descriptor the fmt layer writes to by default, and the one a journald layer would double up on — and compares both numbers against the parsed pair.

A match installs the native journald layer alone. No match, an unset variable, or a value that does not parse installs the fmt layer alone.

When the journald layer fails to build

tracing_journald::layer() validates the journal socket up front and returns an io::Result, so a missing journal surfaces as an error rather than a silent no-op. On that error hive-log installs the fmt layer and emits one warning through it. A process must never fail to start because of its logger, and a logger that drops every record in silence is worse than a degraded one that says so.

Scope

The sink chooser and its tests. Nothing else belongs here: the crate exists so three binaries share one copy of this decision, not as a place for general utilities.