Commit graph hyperhive/nix/host-modules/otel.nix
Author SHA1 Message Date
atlas
ece21b0f7f feat(#3554): let a hive-owned service declare a scrape target 2026-08-23 22:45:56 +02:00
atlas
738cc413e7 otel: wait for the telemetry client secret instead of racing it
`LoadCredential` naming a missing path is fatal at unit start, and this hive's
secret is minted by authelia's first-boot generator inside its own container —
nothing orders a host unit against that.

nixpkgs sets `Restart = "always"` on the collector with no `RestartSec`, so
that failure is instant: the unit burns systemd's 5-starts-in-10s allowance in
well under a second, lands in `start-limit-hit`, and stops retrying entirely.
`Restart = always` reads like it makes this self-healing and does the opposite
— a slow-failing unit retries until the secret appears, a fast-failing one
exhausts its limit before the thing it waits for can exist.

A oneshot converts the fast failure into a slow one, which is what that restart
policy is actually good at. Copied from `hive-forge-oidc-secret.service`, which
already solves this for the forge: bounded wait, then fail loudly naming the
file — never skip, because a skip yields a collector that starts and ships
nothing.

`TimeoutStartSec` exceeds the wait on purpose: `DefaultTimeoutStartSec` is 90s
and would kill the unit before it could emit that message.

The ordering against authelia's container is conditional — on a hive that does
not host the provider the secret is operator-provided, and naming a unit that
does not exist orders nothing, silently. The wait itself still applies there,
so a file that arrives late is tolerated rather than fatal.
2026-08-19 15:34:40 +02:00
atlas
9bd2b9e9e6 otel: a hive always authenticates — drop the unauthenticated mode
mara, reviewing this PR: "hives always require an identity, swarm controller
and auth is not optional."

So `requireHiveIdentity` is gone rather than defaulted, and with it every
branch that had to describe an unauthenticated collector. The swarm tier now
serves per-hive receivers only, and `/` answers 404 because there is no
swarm-wide inbox to route to. A hive with no credential is a build error, not
a quieter mode.

`hivePortBase` goes too: with per-hive receivers unconditional, `port` IS the
base of the range. That keeps one documented knob instead of adding a second,
and its advice ("move it if something else claims that range") still holds.

Two assertions replace the toggle — an empty hive roster, and a null
`authelia.url`. The second matters because a guessed issuer URL evaluates
cleanly, deploys cleanly, and then refuses every hive at runtime.

⚠️ `cfg.port` is deliberately no longer compared against the derived range in
the collision assertion: it is now the range's first element, so listing it
would make that assertion fire on every config.

This also retires the asymmetry guard added earlier in review — the state it
protected against (auth off on one side, credential still set on the other)
is no longer representable.
2026-08-19 15:27:09 +02:00
atlas
7da7915150 otel: refuse a half-configured escape hatch instead of 404ing silently
Turning ingest auth off without clearing a hive's credential leaves that
hive's collector authenticating and addressing its own path, while an
unauthenticated swarm tier serves one catch-all and forwards the URI
unchanged. The receiver is asked for a path it does not serve, so telemetry
stops with 404s and retries — no 401, no assertion, nothing in any log
naming auth.

Only reachable by overriding one side without the other, since both defaults
derive from the same flag. That is what makes it worth a build error rather
than a caveat: an operator who flips the documented escape hatch has no
reason to suspect the sending half.

Found in review by argus.
2026-08-19 15:27:09 +02:00
atlas
cb787997bd otel: the hive tier presents its own identity to the swarm collector
The receiving half authenticates per hive, so this half has to prove which
hive it is. It mints a token against the swarm's authelia with this hive's
client and posts to that hive's path on the collector's gateway name.

Holding a credential is what decides whether this tier authenticates —
`clientSecretFile` non-null — rather than a second switch that could
disagree with it. The default is the secret this host's own authelia
minted, which is right exactly when the IdP runs here; a hive that is not
that host names wherever the file landed, the same manual-copy shape the
identities option already documents as unsolved.

Two things that a diff will not explain:

`endpoint_params.audience` is not redundant with the client's registered
audience. Registering only makes an audience permissible; a token minted
without asking for one carries `aud: []` and every receiver refuses it,
with a config that reads correctly at both ends.

`client_secret_file` keeps the secret out of nix altogether — the
collector opens the file itself. It is a real key of this extension,
checked against the shipped binary with a deliberate typo rejected in the
same run, so "accepted" is distinguishable from "ignores everything". The
path comes from systemd's `CREDENTIALS_DIRECTORY`, so nothing hardcodes a
`/run/credentials` layout.

An assertion covers the one deployment where this can go wrong silently:
a host running both tiers with ingest authenticated and no credential to
present would 401 against a collector on the same machine.
2026-08-19 15:27:09 +02:00
atlas
0f801c645f otel: build the hive tier's collector from contrib
The hive tier has to *present* a credential to the swarm tier, and the
upstream collector build ships no auth extensions at all — `oauth2client`
exists only in contrib. So this is what makes authenticated egress
expressible, not a preference between two equivalent packages.

It also matches the swarm tier, which has been on contrib since it was
written. Two tiers of the same pipeline built from different component
sets is a difference nobody would predict from reading either module.

Not a build-farm cost: contrib is fetched, not compiled.

No behaviour change on its own — nothing names an extension yet. The
config that does lands separately, because that is the commit whose
failure mode needs a running collector to detect: `otelcol validate`
accepts a receiver naming an absent extension and the process then dies
at startup, so a green build proves nothing about it.
2026-08-19 15:27:09 +02:00
atlas
d2fb4bff79 feat(#3125): reshape the hive-to-swarm OTEL hop by domain
Drops swarm.otel.url (a loopback default an operator had to override on a
split host) in favor of swarm.otel.domain -- the same
gateway.localNames + nginx-vhost-through-the-gateway shape every other
swarm service (authelia, grafana, victoriametrics, ui) already uses. The
hive tier's exporter now reaches it as https://<domain> unconditionally,
resolved locally by dnsmasq on a co-located host and over the real network
otherwise, instead of a config knob nobody sets until they hit the silent
drop.

Costs CA trust on the hive tier: otel.nix wires
lib/hive-ca-trust.nix's trustBundle with hostUnit = true on the
opentelemetry-collector host unit, the same flag #3441/#3442 added for
swarm-controller and hive-c0re.

mara, #3125 comment 58363: "go c".
2026-08-18 21:02:18 +02:00
atlas
98ab0ad59f style: drop tracker tags from the collector modules' comments
The pre-push lint refuses them, and rightly: a comment that names an
issue number ages into a pointer at a closed thread. The constraint each
one carried is stated directly instead.
2026-08-18 21:02:18 +02:00
atlas
094a54e785 feat(#3125): the hive tier forwards to the swarm tier and holds nothing
The hive collector's only exporter becomes the swarm's collector, and
the upstream credential, the metrics-store exporter and the choice of
destination all move one tier up.

Its assertion goes with them: 'endpoint or a local store' was the right
rule while this tier picked the destination, and is the wrong one now.
A hive that runs no swarm services has neither, forwards to a swarm
collector elsewhere, and is correctly configured — the rule that
replaces it lives in swarm-otel.nix, where the destinations are.

The option descriptions here described a topology with one collector in
it: endpoint and protocol are not what agents are handed (they get the
derived first hop, see hive-c0re/environment.nix), and the credential is
not read by this tier.
2026-08-18 21:02:18 +02:00
atlas
16d578e692 docs(#3265): observability.md still said the endpoint was required
Review catch: this PR relaxed the `otel.endpoint` assertion and staled the
canonical OTEL reference in the same stroke — `docs/observability.md` is
what CLAUDE.md points readers at for "what OTEL options are available",
and it still said required-full-stop while the new swarm/services.md
section said a local store satisfies it.

Also corrects the option's own description in otel.nix, which said the
same thing and renders into the generated options doc. Grepping the
reviewer's phrasing did not find that one; grepping the claim did.

Records the second destination where the "endpoint is where telemetry
ultimately goes" paragraph makes its claim, rather than only in the new
section a reader may not reach.
2026-08-16 22:27:05 +02:00
atlas
c364d262e5 feat(#3265): feed the store from the collector, and derive the pair
Review feedback: the metrics pair had nothing writing into it, and it sat
outside the switch that turns on every other swarm-wide service.

The collector now exports to VictoriaMetrics as well as upstream — a
fan-out, not a choice: a local store is for looking at this swarm, an
upstream is for whoever aggregates across swarms.

That makes a local store a complete destination on its own, so
`otel.endpoint` is no longer required when it runs here; a hive with
neither is still refused. The assertion only ever relaxes, so every
config that evaluated before still does.

`enableRequiredServices` now derives both halves, alongside matrix,
authelia and nats. They derive together because a store with no UI is
unreadable and a UI with no store is empty.
2026-08-16 22:27:05 +02:00
atlas
c38778b885 docs(otel): validateConfigFile is a parser, not a wiring check
The comment said "It parses; it does not prove a sample arrives",
which understates the gap in the direction that matters: it reads as
if a green build proves the collector *starts* and only runtime
delivery is unverified.

Measured while probing ingest-auth options for #3283: `otelcol
validate` ACCEPTS a receiver naming an auth extension that is absent
from the build, and the collector then dies at startup with `Failed
to start component`. So the check does not prove this config starts
at all.

Comment-only; no evaluated config changes.

Refs #3283.
2026-08-15 12:13:48 +02:00
atlas
513eb5729d docs(otel): one path, and stop describing a forwarding that no longer happens
The headersCredential section still said hive-c0re forwards the file
into each agent container - that is exactly the behaviour this change
removes, so the doc contradicted the code rather than merely lagging
it. Same for the collector section, which presented the two paths the
change collapses.

Also drops the dangling collector.enable reference in the network
section.
2026-08-15 11:46:24 +02:00
atlas
9549cdf9bd feat(otel): one path out of the hive, not two
mara: 'there should only be the one via otel-collector'. Collapses
collector.enable away - enabling otel runs the collector, and agents
always export to it.

The argument for it is the same one the collector exists for: as long
as the direct path stays selectable, the credential-in-settings.json
hole stays selectable. An option that can reintroduce a hole is a hole
with extra steps. HYPERHIVE_OTEL_HEADERS_CREDENTIAL is now never
forwarded to containers at all rather than conditionally.

Two consequences, both deliberate:

- the enable option's own promise changes. It said each harness exports
  directly so telemetry survives anything host-side being down; that is
  now false for every hive rather than for opted-in ones. Stated in the
  option text rather than left for a reader to discover.
- the collector-requires-headersCredential assertion is gone. It was
  correct only under the shape it was written for: when the collector
  was opt-in FOR the credential, opting in without one was pure
  indirection. With one path it isn't - an upstream needing no auth
  header is legitimate, and the assertion would have rejected it. The
  upstream header block is now omitted entirely in that case, rather
  than rendering an ${env:...} reference nothing sets.
2026-08-15 11:46:24 +02:00
atlas
1fc267b880 feat(otel): the collector's upstream exporter honours otel.protocol
The module hardcoded otlphttp, so a hive with protocol = "grpc" would
have had its agents' protocol respected end to end before the collector
and silently rewritten to HTTP after it. Splitting the path in two makes
the upstream half the one that has to keep honouring the option; the
agent half is pinned to the receiver's protocol and derived, not
configured.
2026-08-15 11:46:24 +02:00
atlas
9ad4db842a feat(otel): the collector unit, off by default
Renders services.opentelemetry-collector on the host: OTLP/HTTP receiver
on the bridge address, otlphttp exporter to otel.endpoint, and the
upstream credential delivered as EnvironmentFile so the collector
interpolates it at runtime and nix never sees the value.

Three things worth knowing, each measured rather than assumed:

- network.exposeHostPorts already exists and is wired (it opens the port
  on the bridge interface only), so bridge reachability costs nothing.
- validateConfigFile defaults to isStorePath configFile, which is null on
  the settings path - so upstream's default is OFF for exactly the way
  this module configures it. Set true: it runs otelcol validate at build
  time. It parses, it does not prove delivery.
- headersCredential's file is already NAME=value, i.e. EnvironmentFile
  format, verified against a real settings.json rather than the doc.

An assertion refuses collector.enable with no headersCredential: the
collector exists to be the only holder of that token, and without one it
is indirection that reads as security.
2026-08-15 11:46:24 +02:00
atlas
b8af1525ff feat(otel): options for a host-side collector, off by default
Adds services.hyperhive.otel.collector.{enable,port}. No implementation
yet - this commit only declares the surface, so the option docs and the
default-off invariant can be reviewed on their own.

The reason the collector exists is the credential: without it every agent
needs headersCredential to reach the upstream, and the harness delivers
that token into the agent's own settings.json where the agent can read
it. The option text states the cost too - the collector becomes a
dependency in an export path that is currently direct.
2026-08-15 11:46:24 +02:00
müde
4a48ce5024 refactor: nix/host-modules + nix/agent-modules layout, update doc paths 2026-07-13 22:05:49 +02:00
Renamed from nix/modules/otel.nix (Browse further)