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:
36
README.md
36
README.md
@@ -80,9 +80,35 @@ services.prometheus.exporters.nixos = {
|
|||||||
|--------|------|--------|-------------|
|
|--------|------|--------|-------------|
|
||||||
| `nixos_flake_input_age_seconds` | Gauge | `input` | Age of flake input in seconds |
|
| `nixos_flake_input_age_seconds` | Gauge | `input` | Age of flake input in seconds |
|
||||||
| `nixos_flake_input_info` | Gauge | `input`, `rev`, `type` | Info gauge with revision and type labels |
|
| `nixos_flake_input_info` | Gauge | `input`, `rev`, `type` | Info gauge with revision and type labels |
|
||||||
| `nixos_flake_info` | Gauge | `current_rev`, `remote_rev` | Info gauge with current and remote flake revisions |
|
| `nixos_flake_info` | Gauge | `current_rev`, `remote_rev`, `nixpkgs_rev`, `nixos_version` | Info gauge with system version details |
|
||||||
| `nixos_flake_revision_behind` | Gauge | | 1 if current system revision differs from remote latest |
|
| `nixos_flake_revision_behind` | Gauge | | 1 if current system revision differs from remote latest |
|
||||||
|
|
||||||
|
### Setting Configuration Revision
|
||||||
|
|
||||||
|
For `current_rev` to show the flake's git revision, you must set `system.configurationRevision` in your flake:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
outputs = { self, nixpkgs, ... }: {
|
||||||
|
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
system.configurationRevision = self.rev or self.dirtyRev or "dirty";
|
||||||
|
}
|
||||||
|
./configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This sets the revision to:
|
||||||
|
- `self.rev` - Git commit hash (only available when tree is clean and committed)
|
||||||
|
- `self.dirtyRev` - Dirty revision (e.g., `abc1234-dirty`) when uncommitted changes exist
|
||||||
|
- `"dirty"` - Fallback when neither is available
|
||||||
|
|
||||||
|
Without this setting, `current_rev` will be `"unknown"` and `nixos_flake_revision_behind` will always be 0.
|
||||||
|
|
||||||
## Example Prometheus Alerts
|
## Example Prometheus Alerts
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -130,13 +156,9 @@ groups:
|
|||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- The `nixos_flake_info` and `nixos_flake_revision_behind` metrics rely on parsing the git hash from `/run/current-system/nixos-version`. The format of this file varies depending on NixOS configuration:
|
- Flake input metrics (`nixos_flake_input_age_seconds`, `nixos_flake_input_info`) reflect the remote flake state, not the currently deployed system. If the deployed system is behind, these will show newer data than what's actually deployed.
|
||||||
- Standard format: `25.11.20260203.e576e3c`
|
|
||||||
- Custom format: `1994-294a625`
|
|
||||||
|
|
||||||
If your system uses a non-standard format that doesn't end with a git hash, the revision comparison may not work correctly.
|
- The `nixos_flake_revision_behind` metric requires `system.configurationRevision` to be set. Without it, the metric will always be 0 since there's no local revision to compare against.
|
||||||
|
|
||||||
- Flake input ages reflect the remote flake state. If the deployed system is behind, these will show newer timestamps than what's actually deployed.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -14,7 +13,12 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"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 {
|
type FlakeCollector struct {
|
||||||
flakeURL string
|
flakeURL string
|
||||||
@@ -74,7 +78,7 @@ func NewFlakeCollector(flakeURL string, checkInterval time.Duration) *FlakeColle
|
|||||||
flakeInfo: prometheus.NewDesc(
|
flakeInfo: prometheus.NewDesc(
|
||||||
"nixos_flake_info",
|
"nixos_flake_info",
|
||||||
"Info gauge with current and remote flake revisions",
|
"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(
|
revisionBehind: prometheus.NewDesc(
|
||||||
"nixos_flake_revision_behind",
|
"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) {
|
func (c *FlakeCollector) collectRevisionBehind(ch chan<- prometheus.Metric, data *flakeMetadata) {
|
||||||
currentRev, err := getCurrentSystemRevision()
|
versionInfo, err := getNixosVersionInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to get current system revision", "error", err)
|
slog.Error("Failed to get NixOS version info", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentRev := versionInfo.ConfigurationRevision
|
||||||
|
if currentRev == "" {
|
||||||
|
currentRev = "unknown"
|
||||||
|
} else if len(currentRev) > 7 {
|
||||||
|
currentRev = currentRev[:7]
|
||||||
|
}
|
||||||
|
|
||||||
remoteRev := data.Revision
|
remoteRev := data.Revision
|
||||||
if len(remoteRev) > 7 {
|
if len(remoteRev) > 7 {
|
||||||
remoteRev = remoteRev[:7]
|
remoteRev = remoteRev[:7]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nixpkgsRev := versionInfo.NixpkgsRevision
|
||||||
|
if len(nixpkgsRev) > 7 {
|
||||||
|
nixpkgsRev = nixpkgsRev[:7]
|
||||||
|
}
|
||||||
|
|
||||||
// Emit flake info metric with revisions
|
// 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
|
behind := 0.0
|
||||||
if currentRev != "" && data.Revision != "" {
|
if currentRev != "unknown" && data.Revision != "" {
|
||||||
if currentRev != remoteRev && !strings.HasPrefix(data.Revision, currentRev) {
|
if currentRev != remoteRev && !strings.HasPrefix(data.Revision, currentRev) {
|
||||||
behind = 1.0
|
behind = 1.0
|
||||||
}
|
}
|
||||||
@@ -209,19 +226,23 @@ func fetchFlakeMetadata(flakeURL string) (*flakeMetadata, error) {
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCurrentSystemRevision() (string, error) {
|
func getNixosVersionInfo() (*nixosVersionInfo, error) {
|
||||||
data, err := os.ReadFile(configRevisionPath)
|
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 err != nil {
|
||||||
if os.IsNotExist(err) {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
// configuration-revision doesn't exist; user hasn't set system.configurationRevision
|
return nil, fmt.Errorf("nixos-version failed: %s", strings.TrimSpace(string(exitErr.Stderr)))
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
return "", err
|
return nil, fmt.Errorf("nixos-version failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rev := strings.TrimSpace(string(data))
|
var info nixosVersionInfo
|
||||||
if len(rev) > 7 {
|
if err := json.Unmarshal(output, &info); err != nil {
|
||||||
rev = rev[:7]
|
return nil, fmt.Errorf("failed to parse nixos-version output: %w", err)
|
||||||
}
|
}
|
||||||
return rev, nil
|
|
||||||
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,46 @@ package collector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetCurrentSystemRevision(t *testing.T) {
|
func TestGetNixosVersionInfo(t *testing.T) {
|
||||||
// Skip if not on NixOS with system.configurationRevision set
|
// Skip if nixos-version command is not available
|
||||||
if _, err := os.Stat(configRevisionPath); os.IsNotExist(err) {
|
if _, err := exec.LookPath("nixos-version"); err != nil {
|
||||||
t.Skip("not running on NixOS with system.configurationRevision set")
|
t.Skip("nixos-version command not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
rev, err := getCurrentSystemRevision()
|
info, err := getNixosVersionInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just check it returns something reasonable
|
t.Logf("NixOS version info: configurationRevision=%s, nixosVersion=%s, nixpkgsRevision=%s",
|
||||||
t.Logf("current system revision: %s", rev)
|
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) {
|
func TestFlakeLocksUnmarshal(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user