swarm-otel: add an unauthenticated swarm-tier OTLP receiver for swarm-controller to push to
This commit is contained in:
parent
9720bdfad0
commit
2aa33f12d2
2 changed files with 83 additions and 9 deletions
|
|
@ -149,6 +149,35 @@ let
|
||||||
SWARM_CONTROLLER_NAME = config.services.hyperhive.swarm.name;
|
SWARM_CONTROLLER_NAME = config.services.hyperhive.swarm.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# `vcs_metrics`/`hive_jobq_metrics` (this daemon's OTLP push exporters)
|
||||||
|
# read the standard `OTEL_EXPORTER_OTLP_ENDPOINT` var and are silent,
|
||||||
|
# graceful no-ops without it — same shape as `forgeEnv`/`authBridgeEnv`
|
||||||
|
# above, genuinely optional and gated on the option resolving rather
|
||||||
|
# than assumed.
|
||||||
|
#
|
||||||
|
# Gated on THIS HOST running `swarm-otel`, not merely on it existing
|
||||||
|
# somewhere in the swarm: its receiver for a swarm-tier producer binds
|
||||||
|
# `127.0.0.1` only (see `swarm-otel.nix::producerPort`), so reaching it
|
||||||
|
# needs co-location, and this checks that directly rather than assuming
|
||||||
|
# it — the unasserted-co-location gap flagged elsewhere in this codebase
|
||||||
|
# doesn't apply here because there is nothing to assert: a controller on
|
||||||
|
# a host that doesn't run the collector simply exports nothing, the same
|
||||||
|
# graceful absence `forgeEnv` already models.
|
||||||
|
otelSwarmCfg = config.services.hyperhive.swarm.otel;
|
||||||
|
otelEnv = lib.optionalAttrs otelSwarmCfg.enable {
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT = "http://${otelSwarmCfg.domain}:${toString otelSwarmCfg.producerPort}";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Only when THIS host also runs `swarm-otel` — same reasoning and same
|
||||||
|
# shape as `hive-forge/default.nix`'s `ssoLocal`-gated entry: a raw host
|
||||||
|
# systemd unit does not get the bridge's dnsmasq resolution containers
|
||||||
|
# get, so the name that reaches a co-located collector over `otelEnv`
|
||||||
|
# above needs an explicit loopback alias here, or it resolves however
|
||||||
|
# (or however it fails to) off-host DNS says on this host.
|
||||||
|
otelHostsEntry = lib.optionalAttrs otelSwarmCfg.enable {
|
||||||
|
"127.0.0.1" = [ otelSwarmCfg.domain ];
|
||||||
|
};
|
||||||
|
|
||||||
# Wrapped rather than documented: every one of these values is derived
|
# Wrapped rather than documented: every one of these values is derived
|
||||||
# from an option this deployment already set, so making the operator
|
# from an option this deployment already set, so making the operator
|
||||||
# re-supply them on the command line would be asking them to repeat the
|
# re-supply them on the command line would be asking them to repeat the
|
||||||
|
|
@ -450,6 +479,10 @@ in
|
||||||
"swarm-controller-credential"
|
"swarm-controller-credential"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# See `otelHostsEntry`'s own comment: only present, and only ever
|
||||||
|
# `{ "127.0.0.1" = [ ... ]; }`, when this host also runs `swarm-otel`.
|
||||||
|
networking.hosts = otelHostsEntry;
|
||||||
|
|
||||||
users.users.swarm-controller = {
|
users.users.swarm-controller = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "swarm-controller";
|
group = "swarm-controller";
|
||||||
|
|
@ -676,7 +709,8 @@ in
|
||||||
// forgeEnv
|
// forgeEnv
|
||||||
// webhookEnv
|
// webhookEnv
|
||||||
// authBridgeEnv
|
// authBridgeEnv
|
||||||
// swarmNameEnv;
|
// swarmNameEnv
|
||||||
|
// otelEnv;
|
||||||
};
|
};
|
||||||
|
|
||||||
# A systemd credential is a SNAPSHOT: it is materialised into `%d` once,
|
# A systemd credential is a SNAPSHOT: it is materialised into `%d` once,
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,29 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
producerPort = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 4390;
|
||||||
|
description = ''
|
||||||
|
Port the swarm tier's own unauthenticated OTLP/HTTP receiver
|
||||||
|
listens on, at `127.0.0.1` — for a **swarm-level** producer
|
||||||
|
(`swarm-controller`'s vcs/jobq counters today) to push to, on the
|
||||||
|
same host this collector runs on. No authenticator, deliberately:
|
||||||
|
unlike the per-hive receivers, the label this receiver's samples
|
||||||
|
get (`swarm`) is the one the receiver's own *existence*
|
||||||
|
establishes — there is no hive identity to forge or attribute,
|
||||||
|
so the per-hive attribution mechanism does not apply here.
|
||||||
|
|
||||||
|
⚠️ Deliberately NOT derived from `port + (number of hives)`: that
|
||||||
|
range grows every time a hive is added, and a fixed offset from
|
||||||
|
it would silently start colliding once the hive count caught up.
|
||||||
|
Kept as its own reserved value instead, and the assertion below
|
||||||
|
still catches a real collision (including one hive growth
|
||||||
|
eventually causes) rather than starting a collector that quietly
|
||||||
|
drops one receiver's samples.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
domain = lib.mkOption {
|
domain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "otel.${domainBase}";
|
default = "otel.${domainBase}";
|
||||||
|
|
@ -715,6 +738,7 @@ in
|
||||||
derived = lib.attrValues hivePorts;
|
derived = lib.attrValues hivePorts;
|
||||||
others = [
|
others = [
|
||||||
cfg.telemetryPort
|
cfg.telemetryPort
|
||||||
|
cfg.producerPort
|
||||||
otelCfg.collector.port
|
otelCfg.collector.port
|
||||||
]
|
]
|
||||||
++ lib.optional vmCfg.enable vmCfg.port;
|
++ lib.optional vmCfg.enable vmCfg.port;
|
||||||
|
|
@ -846,6 +870,12 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
) hivePorts
|
) hivePorts
|
||||||
|
# The swarm tier's OWN receiver, unauthenticated on
|
||||||
|
# purpose — see `producerPort`'s description for why no
|
||||||
|
# per-hive-shaped attribution applies here.
|
||||||
|
// {
|
||||||
|
"otlp/${swarmTierName}".protocols.http.endpoint = "127.0.0.1:${toString cfg.producerPort}";
|
||||||
|
}
|
||||||
# MERGED with the per-hive receivers, never assigned over
|
# MERGED with the per-hive receivers, never assigned over
|
||||||
# them. A plain assignment here would drop every hive's
|
# them. A plain assignment here would drop every hive's
|
||||||
# receiver and still render a valid config that starts
|
# receiver and still render a valid config that starts
|
||||||
|
|
@ -1044,9 +1074,17 @@ in
|
||||||
# them would acquire the one label a swarm-level service must
|
# them would acquire the one label a swarm-level service must
|
||||||
# not have. Keeping it out of them makes the absence
|
# not have. Keeping it out of them makes the absence
|
||||||
# structural rather than something to remember to strip.
|
# structural rather than something to remember to strip.
|
||||||
// lib.optionalAttrs (cfg.scrapeTargets != { }) {
|
// {
|
||||||
|
# `otlp/${swarmTierName}` is unconditional (see the
|
||||||
|
# receiver above), so this pipeline is too — a swarm-tier
|
||||||
|
# producer must always have somewhere to land, unlike
|
||||||
|
# `prometheus`, which only joins the receiver list once
|
||||||
|
# something has actually declared a scrape target.
|
||||||
"metrics/${swarmTierName}" = {
|
"metrics/${swarmTierName}" = {
|
||||||
receivers = [ "prometheus" ];
|
receivers = [
|
||||||
|
"otlp/${swarmTierName}"
|
||||||
|
]
|
||||||
|
++ lib.optional (cfg.scrapeTargets != { }) "prometheus";
|
||||||
processors = [ "resource/${swarmTierName}" ];
|
processors = [ "resource/${swarmTierName}" ];
|
||||||
exporters = exporterNames;
|
exporters = exporterNames;
|
||||||
};
|
};
|
||||||
|
|
@ -1151,12 +1189,14 @@ in
|
||||||
# an invented one (a sentinel, the local hive's name) would be
|
# an invented one (a sentinel, the local hive's name) would be
|
||||||
# queried as though it meant something.
|
# queried as though it meant something.
|
||||||
#
|
#
|
||||||
# ⚠️ Emitted for the LOG pipeline too, not just the scraped one:
|
# ⚠️ Unconditional, not gated on `scrapeTargets != {} ||
|
||||||
# a processor a pipeline names but the config does not define is
|
# collectLogs` (as it once was): a processor a pipeline names
|
||||||
# a collector that refuses to start, and enabling the log store
|
# but the config does not define is a collector that refuses
|
||||||
# without declaring a scrape target is a perfectly ordinary
|
# to start, and `metrics/${swarmTierName}` now ALWAYS exists
|
||||||
# config.
|
# (its `otlp/${swarmTierName}` receiver is unconditional too,
|
||||||
// lib.optionalAttrs (cfg.scrapeTargets != { } || collectLogs) {
|
# for a swarm-tier producer to push to) — so this processor
|
||||||
|
# has to exist unconditionally right alongside it.
|
||||||
|
// {
|
||||||
"resource/${swarmTierName}".attributes = [
|
"resource/${swarmTierName}".attributes = [
|
||||||
{
|
{
|
||||||
# The metric LABEL, a different namespace from the
|
# The metric LABEL, a different namespace from the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue