Compare commits

..
2 changed files with 41 additions and 129 deletions

View file

@ -181,33 +181,6 @@ in
addresses its siblings by name.
'';
};
scrapeTargets = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = lib.literalExpression ''{ forgejo = "127.0.0.1:3000"; }'';
description = ''
Prometheus exposition endpoints this collector scrapes, as
`<job name> = "<host>:<port>"`.
**A service declares its own entry, from its own module, under its
own `enable`.** That is what puts the scraper and the target on the
same host by construction rather than by luck: an entry exists only
where the service that named it runs. Do not assemble the list here.
Every swarm service being co-located is a property of the all-local
deployment, not a guarantee and that is precisely the case where
the difference is invisible until a swarm splits across hosts.
Samples land in a swarm-level pipeline that stamps `swarm` and
**never** `hive`: a swarm service does not belong to a hive, and
`hive` stays a property of which authenticated receiver accepted a
push, not something a scrape can acquire.
Empty by default, in which case no scrape receiver, processor or
pipeline is emitted at all an enabled scraper with nothing to
scrape is the inert configuration this option exists to avoid.
'';
};
};
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
@ -414,32 +387,15 @@ in
# an auth claim yields nothing — silently, with a healthy
# startup), and one receiver holding many credentials never
# reveals which one matched.
receivers =
lib.mapAttrs' (
h: p:
lib.nameValuePair "otlp/${h}" {
protocols.http = {
endpoint = "127.0.0.1:${toString p}";
auth.authenticator = "oidc/${h}";
};
}
) hivePorts
# MERGED with the per-hive receivers, never assigned over
# them. A plain assignment here would drop every hive's
# receiver and still render a valid config that starts
# cleanly — the collector has no opinion about how many
# pipelines it was supposed to have.
#
# Only emitted when a service has actually declared a
# target: a `prometheus` receiver with nothing to scrape is
# the shape this whole issue is about, a config that renders
# and deploys perfectly while adding no data.
// lib.optionalAttrs (cfg.scrapeTargets != { }) {
prometheus.config.scrape_configs = lib.mapAttrsToList (job: target: {
job_name = job;
static_configs = [ { targets = [ target ]; } ];
}) cfg.scrapeTargets;
};
receivers = lib.mapAttrs' (
h: p:
lib.nameValuePair "otlp/${h}" {
protocols.http = {
endpoint = "127.0.0.1:${toString p}";
auth.authenticator = "oidc/${h}";
};
}
) hivePorts;
exporters =
lib.optionalAttrs vmCfg.enable {
@ -496,28 +452,14 @@ in
# `exporterNames` is shared by every pipeline — where a
# sample goes is a property of this tier, not of the hive
# that sent it.
service.pipelines =
lib.mapAttrs' (
h: _:
lib.nameValuePair "metrics/${h}" {
receivers = [ "otlp/${h}" ];
processors = [ "resource/${h}" ];
exporters = exporterNames;
}
) hivePorts
# Its OWN pipeline, and that separation is the ruling, not a
# tidiness choice: every `resource/<hive>` above UPSERTS a
# `hive` key, so a scraped swarm sample routed through any of
# them would acquire the one label a swarm-level service must
# not have. Keeping it out of them makes the absence
# structural rather than something to remember to strip.
// lib.optionalAttrs (cfg.scrapeTargets != { }) {
"metrics/swarm" = {
receivers = [ "prometheus" ];
processors = [ "resource/swarm" ];
exporters = exporterNames;
};
};
service.pipelines = lib.mapAttrs' (
h: _:
lib.nameValuePair "metrics/${h}" {
receivers = [ "otlp/${h}" ];
processors = [ "resource/${h}" ];
exporters = exporterNames;
}
) hivePorts;
}
// {
extensions = lib.mapAttrs' (
@ -565,51 +507,36 @@ in
# processor is the whole attribution boundary — the value
# is a constant per receiver, so it says which hive
# authenticated, not which hive claimed to be sending.
processors =
lib.mapAttrs' (
h: _:
lib.nameValuePair "resource/${h}" {
attributes = [
{
key = "hive";
value = h;
action = "upsert";
}
# Stamped here rather than on a separate upstream-only
# pipeline, which would double the pipeline count to
# withhold one constant label from the local store. It
# is redundant there — one VictoriaMetrics per swarm, so
# every series in it already belongs to this swarm — but
# a constant label multiplies no series, and it means
# what LEAVES and what STAYS have the same shape.
#
# Upstream is where it stops being redundant: that is the
# one hop where several swarms can land in one store, and
# samples that cannot name their swarm collide there
# exactly as hives collided here before per-hive
# receivers existed.
{
key = "swarm";
value = swarmDisplayName;
action = "upsert";
}
];
}
) hivePorts
# The swarm tier's own stamp: `swarm` and deliberately NO
# `hive`. A scraped swarm service belongs to the swarm, not to
# any one hive, so there is no honest value to put there — and
# an invented one (a sentinel, the local hive's name) would be
# queried as though it meant something.
// lib.optionalAttrs (cfg.scrapeTargets != { }) {
"resource/swarm".attributes = [
processors = lib.mapAttrs' (
h: _:
lib.nameValuePair "resource/${h}" {
attributes = [
{
key = "hive";
value = h;
action = "upsert";
}
# Stamped here rather than on a separate upstream-only
# pipeline, which would double the pipeline count to
# withhold one constant label from the local store. It
# is redundant there — one VictoriaMetrics per swarm, so
# every series in it already belongs to this swarm — but
# a constant label multiplies no series, and it means
# what LEAVES and what STAYS have the same shape.
#
# Upstream is where it stops being redundant: that is the
# one hop where several swarms can land in one store, and
# samples that cannot name their swarm collide there
# exactly as hives collided here before per-hive
# receivers existed.
{
key = "swarm";
value = swarmDisplayName;
action = "upsert";
}
];
};
}
) hivePorts;
};
};

View file

@ -113,21 +113,6 @@ in
}
];
# This store publishes its own health as prometheus metrics on the same
# listener it serves queries on, so the swarm's collector can scrape it
# with no exporter and no extra port.
#
# Declared here rather than in the collector's module because that is the
# rule the option carries: an entry exists only where the service that
# named it runs, which is what keeps scraper and target on one host by
# construction instead of by luck.
#
# The loopback literal introduces no new assumption — it is the address
# this module already pins the listener to, and the same one the
# collector's `otlphttp/victoriametrics` exporter already writes to. If
# that reach is ever wrong, it is wrong for the write path first.
services.hyperhive.swarm.otel.scrapeTargets.victoriametrics = "127.0.0.1:${toString cfg.port}";
services.nginx.virtualHosts."${cfg.domain}" = (gatewayCfg.lib.tlsFor cfg.domain) // {
listen = gatewayCfg.lib.listen;
extraConfig = gatewayCfg.lib.securityHeaders;