226 lines
7.8 KiB
Nix
226 lines
7.8 KiB
Nix
{lib}: let
|
|
inherit (lib) flatten mapAttrs mapAttrsToList filter groupBy length unique attrByPath splitString;
|
|
|
|
# Generic function to aggregate any attribute across nodes
|
|
aggregateFromNodes = {
|
|
nodes,
|
|
attributePath, # e.g. "homelab.monitoring.endpoints" or "homelab.backups.jobs"
|
|
enhancer ? null, # optional function to enhance each item with node context
|
|
}: let
|
|
# Extract the attribute from each node using the path
|
|
getNestedAttr = path: config: let
|
|
pathList = splitString "." path;
|
|
in
|
|
attrByPath pathList [] config;
|
|
|
|
# Get all items from all nodes
|
|
allItems = flatten (mapAttrsToList
|
|
(nodeName: nodeConfig: let
|
|
items = getNestedAttr attributePath nodeConfig.config;
|
|
baseEnhancer = item:
|
|
item
|
|
// {
|
|
_nodeName = nodeName;
|
|
_nodeConfig = nodeConfig;
|
|
_nodeAddress = nodeConfig.config.networking.hostName or nodeName;
|
|
};
|
|
finalEnhancer =
|
|
if enhancer != null
|
|
then (item: enhancer (baseEnhancer item))
|
|
else baseEnhancer;
|
|
in
|
|
map finalEnhancer items)
|
|
nodes);
|
|
in {
|
|
# Raw aggregated data
|
|
all = allItems;
|
|
|
|
# Common grouping patterns
|
|
byNode = groupBy (item: item._nodeName) allItems;
|
|
byType = groupBy (item: item.type or "unknown") allItems;
|
|
byService = groupBy (item: item.service or "unknown") allItems;
|
|
|
|
# Utility functions for filtering
|
|
filterBy = predicate: filter predicate allItems;
|
|
ofType = type: filter (item: (item.type or "") == type) allItems;
|
|
|
|
count = length allItems;
|
|
countBy = fn: mapAttrs (key: items: length items) (groupBy fn allItems);
|
|
};
|
|
|
|
# Specialized aggregators for common use cases
|
|
aggregators = {
|
|
monitoring = nodes: let
|
|
# Aggregate metrics endpoints
|
|
metricsAgg = aggregateFromNodes {
|
|
inherit nodes;
|
|
attributePath = "homelab.monitoring.metrics";
|
|
enhancer = endpoint:
|
|
endpoint
|
|
// {
|
|
_fullAddress = "${endpoint.host or endpoint._nodeAddress}:${toString endpoint.port}";
|
|
_metricsUrl = "http://${endpoint.host or endpoint._nodeAddress}:${toString endpoint.port}${endpoint.path or "/metrics"}";
|
|
_type = "metrics";
|
|
};
|
|
};
|
|
# Aggregate health checks
|
|
healthChecksAgg = aggregateFromNodes {
|
|
inherit nodes;
|
|
attributePath = "homelab.monitoring.healthChecks";
|
|
enhancer = check: let
|
|
# Compute the actual host and URL
|
|
actualHost =
|
|
if check.useExternalDomain or false
|
|
then "${check.subdomain}.${check._nodeConfig.config.homelab.externalDomain or "example.com"}"
|
|
else check.host or check._nodeAddress;
|
|
portPart =
|
|
if check.port != null
|
|
then ":${toString check.port}"
|
|
else "";
|
|
url = "${check.protocol or "http"}://${actualHost}${portPart}${check.path or "/"}";
|
|
in
|
|
check
|
|
// {
|
|
_actualHost = actualHost;
|
|
_url = url;
|
|
_type = "health-check";
|
|
# Merge default labels with node context
|
|
labels =
|
|
(check.labels or {})
|
|
// {
|
|
node = check._nodeName;
|
|
environment = check._nodeConfig.config.homelab.environment or "unknown";
|
|
};
|
|
};
|
|
};
|
|
in
|
|
metricsAgg
|
|
// healthChecksAgg
|
|
// {
|
|
# Metrics-specific aggregations
|
|
allMetrics = metricsAgg.all;
|
|
metricsByNode = metricsAgg.byNode;
|
|
metricsByJobName = groupBy (m: m.jobName or "unknown") metricsAgg.all;
|
|
|
|
# Health checks-specific aggregations
|
|
allHealthChecks = healthChecksAgg.all;
|
|
healthChecksByNode = healthChecksAgg.byNode;
|
|
healthChecksByGroup = groupBy (hc: hc.group or "default") healthChecksAgg.all;
|
|
healthChecksByProtocol = groupBy (hc: hc.protocol or "http") healthChecksAgg.all;
|
|
|
|
# Filtered health checks
|
|
externalHealthChecks = filter (hc: hc.useExternalDomain or false) healthChecksAgg.all;
|
|
internalHealthChecks = filter (hc: !(hc.useExternalDomain or false)) healthChecksAgg.all;
|
|
enabledHealthChecks = filter (hc: hc.enabled or true) healthChecksAgg.all;
|
|
|
|
# Summary statistics
|
|
summary = {
|
|
totalMetrics = length metricsAgg.all;
|
|
totalHealthChecks = length healthChecksAgg.all;
|
|
healthChecksByGroup =
|
|
mapAttrs (group: checks: length checks)
|
|
(groupBy (hc: hc.group or "default") healthChecksAgg.all);
|
|
healthChecksByProtocol =
|
|
mapAttrs (protocol: checks: length checks)
|
|
(groupBy (hc: hc.protocol or "http") healthChecksAgg.all);
|
|
externalChecksCount = length (filter (hc: hc.useExternalDomain or false) healthChecksAgg.all);
|
|
internalChecksCount = length (filter (hc: !(hc.useExternalDomain or false)) healthChecksAgg.all);
|
|
};
|
|
};
|
|
|
|
# Promtail log configurations
|
|
# logs = nodes:
|
|
# aggregateFromNodes {
|
|
# inherit nodes;
|
|
# attributePath = "homelab.logging.sources";
|
|
# enhancer = logSource:
|
|
# logSource
|
|
# // {
|
|
# # Add log-specific computed fields
|
|
# _logPath = logSource.path or "/var/log/${logSource.service}.log";
|
|
# _labels =
|
|
# (logSource.labels or {})
|
|
# // {
|
|
# node = logSource._nodeName;
|
|
# service = logSource.service or "unknown";
|
|
# };
|
|
# };
|
|
# };
|
|
|
|
# Reverse proxy configurations
|
|
reverseProxy = nodes:
|
|
aggregateFromNodes {
|
|
inherit nodes;
|
|
attributePath = "homelab.reverseProxy.entries";
|
|
enhancer = entry:
|
|
entry
|
|
// {
|
|
# Add proxy-specific computed fields
|
|
_upstream = "http://${entry.host or entry._nodeAddress}:${toString entry.port}";
|
|
_fqdn = "${entry.subdomain or entry.service}.${entry.domain or "local"}";
|
|
};
|
|
};
|
|
|
|
# Backup jobs with enhanced aggregation
|
|
backups = nodes: let
|
|
baseAgg = aggregateFromNodes {
|
|
inherit nodes;
|
|
attributePath = "homelab.backups.jobs";
|
|
enhancer = backup:
|
|
backup
|
|
// {
|
|
_sourceNode = backup._nodeName;
|
|
_backupId = "${backup._nodeName}-${backup.name}";
|
|
_jobFqdn = "${backup.name}.${backup._nodeName}";
|
|
};
|
|
};
|
|
|
|
# Get all unique backends across all nodes
|
|
allBackends = let
|
|
allBackendConfigs =
|
|
mapAttrsToList
|
|
(nodeName: nodeConfig:
|
|
attrByPath ["homelab" "backups" "backends"] {} nodeConfig.config)
|
|
nodes;
|
|
enabledBackends = flatten (map (backends:
|
|
filter (name: backends.${name} != null) (lib.attrNames backends))
|
|
allBackendConfigs);
|
|
in
|
|
unique enabledBackends;
|
|
in
|
|
baseAgg
|
|
// {
|
|
# Backup-specific aggregations
|
|
byBackend = groupBy (job: job.backend) baseAgg.all;
|
|
allBackends = allBackends;
|
|
|
|
# Enhanced summary
|
|
summary = {
|
|
totalJobs = length baseAgg.all;
|
|
jobsByBackend =
|
|
mapAttrs (backend: jobs: length jobs)
|
|
(groupBy (job: job.backend) baseAgg.all);
|
|
jobsByNode = baseAgg.countBy (job: job._nodeName);
|
|
availableBackends = allBackends;
|
|
backendsInUse = unique (map (job: job.backend) baseAgg.all);
|
|
};
|
|
};
|
|
};
|
|
in {
|
|
inherit aggregateFromNodes aggregators;
|
|
|
|
# Convenience function to create a module that provides global aggregations
|
|
mkGlobalModule = attributeName: aggregatorFn: {
|
|
lib,
|
|
nodes,
|
|
...
|
|
}: {
|
|
options.homelab.global.${attributeName} = lib.mkOption {
|
|
type = lib.types.attrs;
|
|
readOnly = true;
|
|
description = "Globally aggregated ${attributeName} from all nodes";
|
|
};
|
|
|
|
config.homelab.global.${attributeName} = aggregatorFn nodes;
|
|
};
|
|
}
|