swarm-otel: the upstream export is a swarm-otel deploy option
The external export is a setting of the swarm collector, so it moves to services.hyperhive.deploy.swarm-otel.upstream. Renamed: - otel.endpoint -> upstream.endpoint - otel.protocol -> upstream.protocol - otel.headersCredential -> upstream.headersCredential - otel.collector.upstreamHeaderName -> upstream.headerName The unreleased otel.upstreamSignals is declared as upstream.signals. Hosts keep working through the rename warnings. Eval test covers the old paths reaching the new options. Docs and stale comments point at the new paths.
This commit is contained in:
parent
2977dc91d1
commit
41a7974b9e
7 changed files with 206 additions and 144 deletions
|
|
@ -17,7 +17,7 @@
|
|||
let
|
||||
cfg = config.services.hyperhive.swarm.otel;
|
||||
deployCfg = config.services.hyperhive.deploy;
|
||||
otelCfg = config.services.hyperhive.otel;
|
||||
upstream = deployCfg.swarm-otel.upstream;
|
||||
vmCfg = config.services.hyperhive.swarm.victoriametrics;
|
||||
vlCfg = config.services.hyperhive.swarm.victorialogs;
|
||||
hyperhiveCfg = config.services.hyperhive;
|
||||
|
|
@ -273,13 +273,13 @@ let
|
|||
storeSecretPath = "secret/swarm/services/${cfg.clientId}/oidc/client";
|
||||
|
||||
# The operator-configured upstream, named once: the same exporter carries
|
||||
# every signal listed in `upstreamSignals`, so metrics and logs both reach
|
||||
# every signal listed in `upstream.signals`, so metrics and logs both reach
|
||||
# it without a second definition.
|
||||
upstreamExporterName = if otelCfg.protocol == "grpc" then "otlp" else "otlphttp";
|
||||
upstreamExporterName = if upstream.protocol == "grpc" then "otlp" else "otlphttp";
|
||||
upstreamFor =
|
||||
signal:
|
||||
lib.optional (
|
||||
otelCfg.endpoint != "" && builtins.elem signal otelCfg.upstreamSignals
|
||||
upstream.endpoint != "" && builtins.elem signal upstream.signals
|
||||
) upstreamExporterName;
|
||||
# The collector refuses an exporter that no pipeline uses, so the exporter
|
||||
# is only defined when at least one signal goes to it.
|
||||
|
|
@ -310,11 +310,126 @@ let
|
|||
storeRefreshTimer = import ./lib/store-refresh-timer.nix { };
|
||||
in
|
||||
{
|
||||
# What the collector IS to the swarm — the client it is registered as, where
|
||||
# it exports — is `swarm.otel` in ./swarm-otel-service.nix. The secret is a
|
||||
# path on the machine that runs it, so it hangs off the deployment. `enable`
|
||||
# already lives in ./deploy.nix, which also carries the rename.
|
||||
# What the collector IS to the swarm — the client it is registered as — is
|
||||
# `swarm.otel` in ./swarm-otel-service.nix. The secret is a path on the
|
||||
# machine that runs it, and the external export (`upstream`) is the
|
||||
# operator's choice for this deployment, so both hang off the deployment.
|
||||
# `enable` already lives in ./deploy.nix, which also carries the rename.
|
||||
options.services.hyperhive.deploy.swarm-otel = {
|
||||
upstream = {
|
||||
endpoint = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
example = "https://collector.example.com/otel";
|
||||
description = ''
|
||||
Upstream OTLP endpoint: where telemetry ultimately goes, after it
|
||||
has left the swarm.
|
||||
|
||||
Read by the swarm's collector
|
||||
({option}`services.hyperhive.deploy.swarm-otel.enable`), which is the only
|
||||
tier that holds the upstream credential. An agent is handed the
|
||||
*first* hop instead — this hive's own collector — so this value is
|
||||
never given to a container.
|
||||
|
||||
Optional. Leave it empty and the swarm's own metrics store
|
||||
({option}`services.hyperhive.deploy.victoriametrics.enable`) is the
|
||||
destination; that is a complete deployment, not a degraded one.
|
||||
Set both and telemetry goes to both.
|
||||
'';
|
||||
};
|
||||
|
||||
signals = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.enum [
|
||||
"metrics"
|
||||
"logs"
|
||||
]
|
||||
);
|
||||
default = [
|
||||
"metrics"
|
||||
"logs"
|
||||
];
|
||||
example = [ "metrics" ];
|
||||
description = ''
|
||||
Which signals the swarm's collector sends to
|
||||
{option}`services.hyperhive.deploy.swarm-otel.upstream.endpoint`. The
|
||||
swarm's own metrics and log stores always receive everything,
|
||||
whatever this lists.
|
||||
|
||||
Use it when the upstream does not accept a signal, for example one
|
||||
that answers 404 on `/v1/logs`: list only `"metrics"` and the logs
|
||||
pipelines stop exporting upstream. An empty list defines no upstream
|
||||
exporter at all. No pipeline exports traces, so they have no entry.
|
||||
|
||||
Has no effect while
|
||||
{option}`services.hyperhive.deploy.swarm-otel.upstream.endpoint` is empty.
|
||||
'';
|
||||
};
|
||||
|
||||
protocol = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"http/protobuf"
|
||||
"http/json"
|
||||
"grpc"
|
||||
];
|
||||
default = "http/protobuf";
|
||||
description = ''
|
||||
OTLP wire protocol for the **upstream** link, honoured by the
|
||||
swarm collector's exporter.
|
||||
|
||||
Not what agents speak: their first hop is this hive's collector,
|
||||
whose OTLP/HTTP receiver takes protobuf whatever the upstream
|
||||
wants (see `hive-c0re/environment.nix`).
|
||||
'';
|
||||
};
|
||||
|
||||
headersCredential = lib.mkOption {
|
||||
# `str`, not `path`: a `path`-typed relative literal is hash-copied
|
||||
# into the world-readable nix store at eval time, defeating the
|
||||
# point. Keep it a string + require an absolute runtime path so the
|
||||
# secret is only ever read from disk by systemd at start.
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/run/secrets/otel-headers";
|
||||
description = ''
|
||||
Absolute path to an operator-provided secret file holding the
|
||||
upstream auth header as `NAME=value` (e.g.
|
||||
`Authorization=Bearer <token>`).
|
||||
|
||||
**Only the swarm's collector reads this** — the one tier that
|
||||
talks to the upstream. It arrives as an `EnvironmentFile`, so the
|
||||
value is never read by nix, never copied into the store or the
|
||||
generated config, and never passed in argv; and it reaches
|
||||
neither an agent container nor this hive's own collector, which
|
||||
is the point of the tiers existing. Must be absolute.
|
||||
|
||||
Leave null if the upstream needs no auth header; the collector
|
||||
then sends none rather than an empty one.
|
||||
'';
|
||||
};
|
||||
|
||||
headerName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "Authorization";
|
||||
description = ''
|
||||
Name of the HTTP header the collector sends upstream, whose
|
||||
*value* comes from
|
||||
{option}`services.hyperhive.deploy.swarm-otel.upstream.headersCredential`.
|
||||
|
||||
The name is here and the value is not, and that split is forced
|
||||
rather than chosen: the collector models exporter headers as a
|
||||
static map, so rendering them means nix reading the value — the
|
||||
one thing `headersCredential` being a path exists to prevent.
|
||||
A name is public, a value is not.
|
||||
|
||||
⇒ exactly one header is expressible this way. A credential
|
||||
carrying several (`a=1,b=2`) would be read as a single value,
|
||||
which is why the shape is a named header rather than an opaque
|
||||
blob: a second header has to be *declared*, not smuggled.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
clientSecretFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
|
|
@ -816,9 +931,9 @@ in
|
|||
isReadOnly = true;
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (otelCfg.headersCredential != null) {
|
||||
${otelCfg.headersCredential} = {
|
||||
hostPath = otelCfg.headersCredential;
|
||||
// lib.optionalAttrs (upstream.headersCredential != null) {
|
||||
${upstream.headersCredential} = {
|
||||
hostPath = upstream.headersCredential;
|
||||
isReadOnly = true;
|
||||
};
|
||||
}
|
||||
|
|
@ -853,7 +968,7 @@ in
|
|||
{ ... }:
|
||||
{
|
||||
# This tier is the one that resolves an operator-configured
|
||||
# hostname: `otel.endpoint` is an external URL, and reaching it
|
||||
# hostname: `upstream.endpoint` is an external URL, and reaching it
|
||||
# is the entire reason this container holds a credential. The
|
||||
# `/etc/resolv.conf` nixos-containers copies in is a snapshot
|
||||
# taken once at boot, so without this the upstream export
|
||||
|
|
@ -1076,16 +1191,16 @@ in
|
|||
};
|
||||
}
|
||||
// lib.optionalAttrs haveUpstream {
|
||||
${if otelCfg.protocol == "grpc" then "otlp" else "otlphttp"} = {
|
||||
endpoint = otelCfg.endpoint;
|
||||
${if upstream.protocol == "grpc" then "otlp" else "otlphttp"} = {
|
||||
endpoint = upstream.endpoint;
|
||||
}
|
||||
// lib.optionalAttrs (otelCfg.headersCredential != null) {
|
||||
// lib.optionalAttrs (upstream.headersCredential != null) {
|
||||
# Interpolated by the collector from its environment at
|
||||
# runtime, never by nix: `EnvironmentFile` below is what
|
||||
# puts it there, so the value is not read into the store.
|
||||
headers.${otelCfg.collector.upstreamHeaderName} = "\${env:${otelCfg.collector.upstreamHeaderName}}";
|
||||
headers.${upstream.headerName} = "\${env:${upstream.headerName}}";
|
||||
}
|
||||
// lib.optionalAttrs (otelCfg.protocol == "http/json") { encoding = "json"; };
|
||||
// lib.optionalAttrs (upstream.protocol == "http/json") { encoding = "json"; };
|
||||
}
|
||||
// {
|
||||
# `logs_endpoint`, NOT `endpoint`: the latter is a base the
|
||||
|
|
@ -1512,8 +1627,8 @@ in
|
|||
# EnvironmentFile format — so the secret reaches the process as
|
||||
# an environment variable without being read by nix, written to
|
||||
# the store, or passed in argv.
|
||||
(lib.optionalAttrs (otelCfg.headersCredential != null) {
|
||||
serviceConfig.EnvironmentFile = otelCfg.headersCredential;
|
||||
(lib.optionalAttrs (upstream.headersCredential != null) {
|
||||
serviceConfig.EnvironmentFile = upstream.headersCredential;
|
||||
})
|
||||
|
||||
# The other credential, and the other direction: the one above
|
||||
|
|
|
|||
Loading…
Reference in a new issue