feat(swarm-grafana): provision the log store as a datasource, with logs drilldown

Grafana could reach the swarm's metrics and not its logs, so the store that
landed with the collector pipeline had no reader.

Adds the VictoriaLogs datasource plugin and the Logs Drilldown app, and
provisions the datasource beside the metrics one. The drilldown matters as much
as the connection: an unfamiliar log stream is explorable without writing a
LogsQL query first, which is the difference between a store you can query and
one you can use.
This commit is contained in:
atlas 2026-08-24 18:27:44 +02:00
commit 17288589ef

View file

@ -22,6 +22,7 @@ let
tlsCfg = hyperhiveCfg.tls;
autheliaCfg = hyperhiveCfg.swarm.authelia;
vmCfg = hyperhiveCfg.swarm.victoriametrics;
vlCfg = hyperhiveCfg.swarm.victorialogs;
swarmDomain = hyperhiveCfg.swarm.domain;
caTrust = import ./lib/hive-ca-trust.nix { inherit lib tlsCfg gatewayCfg; };
@ -32,6 +33,11 @@ let
# than an error.
datasourceUid = "swarm-victoriametrics";
# The logs store's own uid. Separate from the metrics one and spelled once for
# the same reason: a dashboard panel names it, and a second literal would be
# free to drift into a panel that renders empty rather than erroring.
logsDatasourceUid = "swarm-victorialogs";
# The shipped dashboards carry `@datasourceUid@` where a real deployment needs
# the uid above. They are substituted here rather than committed with the
# literal so the single binding stays single.
@ -174,6 +180,21 @@ in
'';
};
logsDatasourceUrl = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:${toString vlCfg.port}";
defaultText = lib.literalExpression ''"http://127.0.0.1:''${toString services.hyperhive.swarm.victorialogs.port}"'';
description = ''
Where the provisioned logs datasource points. Same reasoning as
{option}`services.hyperhive.swarm.grafana.datasourceUrl`: the store
binds loopback, so a Grafana elsewhere could not reach it anyway.
Provisioned unconditionally, like the metrics datasource the store
being off is a deployment choice rather than a reason to withhold the
connection, and an operator whose logs live elsewhere sets this.
'';
};
oidc = {
clientId = lib.mkOption {
type = lib.types.str;
@ -211,8 +232,24 @@ in
plugins = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ pkgs.grafanaPlugins.grafana-metricsdrilldown-app ];
defaultText = lib.literalExpression "[ pkgs.grafanaPlugins.grafana-metricsdrilldown-app ]";
default = [
pkgs.grafanaPlugins.grafana-metricsdrilldown-app
# Reading the swarm's logs needs both halves: the datasource that can
# speak LogsQL, and the browser that makes an unfamiliar log stream
# explorable without writing a query first.
#
# ⚠️ The app's plugin id is `grafana-lokiexplore-app` and its name is
# "Grafana Logs Drilldown" — Grafana renamed the product and kept the
# id. Searching nixpkgs for "logsdrilldown" finds nothing.
pkgs.grafanaPlugins.victoriametrics-logs-datasource
pkgs.grafanaPlugins.grafana-lokiexplore-app
];
defaultText = lib.literalExpression ''
[
pkgs.grafanaPlugins.grafana-metricsdrilldown-app
pkgs.grafanaPlugins.victoriametrics-logs-datasource
pkgs.grafanaPlugins.grafana-lokiexplore-app
]'';
example = lib.literalExpression "[ pkgs.grafanaPlugins.grafana-piechart-panel ]";
description = ''
Grafana plugins to install, as packages. Declarative rather than
@ -608,6 +645,21 @@ in
access = "proxy";
isDefault = true;
}
{
name = "VictoriaLogs";
# The plugin's own id, read from its `plugin.json` rather
# than guessed. A `type` Grafana does not recognise is
# provisioned without complaint and every query against it
# fails at use, far from anything naming this file.
type = "victoriametrics-logs-datasource";
uid = logsDatasourceUid;
url = cfg.logsDatasourceUrl;
access = "proxy";
# Exactly one datasource may claim this, and metrics has it:
# two defaults is a coin toss over which one an untyped panel
# gets.
isDefault = false;
}
];
};