fix: use nixos-version --json for configuration revision

- Use `nixos-version --json` command instead of reading files directly
- Add nixpkgs_rev and nixos_version labels to nixos_flake_info metric
- Show "unknown" for current_rev when system.configurationRevision not set
- Document configurationRevision setup in README

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 00:55:23 +01:00
parent 86eaeb4b2a
commit 04eba77ac0
4 changed files with 99 additions and 33 deletions

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"sync"
@@ -14,7 +13,12 @@ import (
"github.com/prometheus/client_golang/prometheus"
)
const configRevisionPath = "/run/current-system/configuration-revision"
// nixosVersionInfo holds the parsed output of nixos-version --json
type nixosVersionInfo struct {
ConfigurationRevision string `json:"configurationRevision"`
NixosVersion string `json:"nixosVersion"`
NixpkgsRevision string `json:"nixpkgsRevision"`
}
type FlakeCollector struct {
flakeURL string
@@ -74,7 +78,7 @@ func NewFlakeCollector(flakeURL string, checkInterval time.Duration) *FlakeColle
flakeInfo: prometheus.NewDesc(
"nixos_flake_info",
"Info gauge with current and remote flake revisions",
[]string{"current_rev", "remote_rev"}, nil,
[]string{"current_rev", "remote_rev", "nixpkgs_rev", "nixos_version"}, nil,
),
revisionBehind: prometheus.NewDesc(
"nixos_flake_revision_behind",
@@ -164,22 +168,35 @@ func (c *FlakeCollector) collectInputMetrics(ch chan<- prometheus.Metric, data *
}
func (c *FlakeCollector) collectRevisionBehind(ch chan<- prometheus.Metric, data *flakeMetadata) {
currentRev, err := getCurrentSystemRevision()
versionInfo, err := getNixosVersionInfo()
if err != nil {
slog.Error("Failed to get current system revision", "error", err)
slog.Error("Failed to get NixOS version info", "error", err)
return
}
currentRev := versionInfo.ConfigurationRevision
if currentRev == "" {
currentRev = "unknown"
} else if len(currentRev) > 7 {
currentRev = currentRev[:7]
}
remoteRev := data.Revision
if len(remoteRev) > 7 {
remoteRev = remoteRev[:7]
}
nixpkgsRev := versionInfo.NixpkgsRevision
if len(nixpkgsRev) > 7 {
nixpkgsRev = nixpkgsRev[:7]
}
// Emit flake info metric with revisions
ch <- prometheus.MustNewConstMetric(c.flakeInfo, prometheus.GaugeValue, 1, currentRev, remoteRev)
ch <- prometheus.MustNewConstMetric(c.flakeInfo, prometheus.GaugeValue, 1,
currentRev, remoteRev, nixpkgsRev, versionInfo.NixosVersion)
behind := 0.0
if currentRev != "" && data.Revision != "" {
if currentRev != "unknown" && data.Revision != "" {
if currentRev != remoteRev && !strings.HasPrefix(data.Revision, currentRev) {
behind = 1.0
}
@@ -209,19 +226,23 @@ func fetchFlakeMetadata(flakeURL string) (*flakeMetadata, error) {
return &data, nil
}
func getCurrentSystemRevision() (string, error) {
data, err := os.ReadFile(configRevisionPath)
func getNixosVersionInfo() (*nixosVersionInfo, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "nixos-version", "--json")
output, err := cmd.Output()
if err != nil {
if os.IsNotExist(err) {
// configuration-revision doesn't exist; user hasn't set system.configurationRevision
return "", nil
if exitErr, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("nixos-version failed: %s", strings.TrimSpace(string(exitErr.Stderr)))
}
return "", err
return nil, fmt.Errorf("nixos-version failed: %w", err)
}
rev := strings.TrimSpace(string(data))
if len(rev) > 7 {
rev = rev[:7]
var info nixosVersionInfo
if err := json.Unmarshal(output, &info); err != nil {
return nil, fmt.Errorf("failed to parse nixos-version output: %w", err)
}
return rev, nil
return &info, nil
}

View File

@@ -2,23 +2,46 @@ package collector
import (
"encoding/json"
"os"
"os/exec"
"testing"
)
func TestGetCurrentSystemRevision(t *testing.T) {
// Skip if not on NixOS with system.configurationRevision set
if _, err := os.Stat(configRevisionPath); os.IsNotExist(err) {
t.Skip("not running on NixOS with system.configurationRevision set")
func TestGetNixosVersionInfo(t *testing.T) {
// Skip if nixos-version command is not available
if _, err := exec.LookPath("nixos-version"); err != nil {
t.Skip("nixos-version command not available")
}
rev, err := getCurrentSystemRevision()
info, err := getNixosVersionInfo()
if err != nil {
t.Fatal(err)
}
// Just check it returns something reasonable
t.Logf("current system revision: %s", rev)
t.Logf("NixOS version info: configurationRevision=%s, nixosVersion=%s, nixpkgsRevision=%s",
info.ConfigurationRevision, info.NixosVersion, info.NixpkgsRevision)
}
func TestNixosVersionInfoUnmarshal(t *testing.T) {
jsonData := `{
"configurationRevision": "138aeb9e450890efd7ca3be6159b1eb22256c2e3",
"nixosVersion": "25.11.20260203.e576e3c",
"nixpkgsRevision": "e576e3c9cf9bad747afcddd9e34f51d18c855b4e"
}`
var info nixosVersionInfo
if err := json.Unmarshal([]byte(jsonData), &info); err != nil {
t.Fatal(err)
}
if info.ConfigurationRevision != "138aeb9e450890efd7ca3be6159b1eb22256c2e3" {
t.Errorf("expected configurationRevision 138aeb9e450890efd7ca3be6159b1eb22256c2e3, got %s", info.ConfigurationRevision)
}
if info.NixosVersion != "25.11.20260203.e576e3c" {
t.Errorf("expected nixosVersion 25.11.20260203.e576e3c, got %s", info.NixosVersion)
}
if info.NixpkgsRevision != "e576e3c9cf9bad747afcddd9e34f51d18c855b4e" {
t.Errorf("expected nixpkgsRevision e576e3c9cf9bad747afcddd9e34f51d18c855b4e, got %s", info.NixpkgsRevision)
}
}
func TestFlakeLocksUnmarshal(t *testing.T) {