feat: add nixos_flake_info metric with current and remote revisions
Add a new info metric that exposes the current system's flake revision and the latest remote revision as labels. This makes it easier to see exactly which revision is deployed vs available. Also adds version constant to Go code and extracts it in flake.nix, providing a single source of truth for the version. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,10 @@ Follow semantic versioning:
|
|||||||
- **Minor** (0.x.0): Non-breaking changes adding features
|
- **Minor** (0.x.0): Non-breaking changes adding features
|
||||||
- **Major** (x.0.0): Breaking changes
|
- **Major** (x.0.0): Breaking changes
|
||||||
|
|
||||||
|
Update the `const version` in `main.go`. The Nix build extracts the version from there automatically.
|
||||||
|
|
||||||
|
**When to bump**: If any Go code has changed, bump the version before committing. Do this automatically when asked to commit. On feature branches, only bump once per branch (check if version has already been bumped compared to master).
|
||||||
|
|
||||||
## Key Design Decisions
|
## Key Design Decisions
|
||||||
|
|
||||||
- Go chosen for mature Prometheus client library and static binary output
|
- Go chosen for mature Prometheus client library and static binary output
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ 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_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 |
|
||||||
|
|
||||||
## Example Prometheus Alerts
|
## Example Prometheus Alerts
|
||||||
@@ -129,7 +130,7 @@ groups:
|
|||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
- The `nixos_flake_revision_behind` metric relies on parsing the git hash from `/run/current-system/nixos-version`. The format of this file varies depending on NixOS configuration:
|
- 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:
|
||||||
- Standard format: `25.11.20260203.e576e3c`
|
- Standard format: `25.11.20260203.e576e3c`
|
||||||
- Custom format: `1994-294a625`
|
- Custom format: `1994-294a625`
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type FlakeCollector struct {
|
|||||||
|
|
||||||
inputAge *prometheus.Desc
|
inputAge *prometheus.Desc
|
||||||
inputInfo *prometheus.Desc
|
inputInfo *prometheus.Desc
|
||||||
|
flakeInfo *prometheus.Desc
|
||||||
revisionBehind *prometheus.Desc
|
revisionBehind *prometheus.Desc
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -75,6 +76,11 @@ func NewFlakeCollector(flakeURL string, checkInterval time.Duration) *FlakeColle
|
|||||||
"Info gauge with revision and type labels",
|
"Info gauge with revision and type labels",
|
||||||
[]string{"input", "rev", "type"}, nil,
|
[]string{"input", "rev", "type"}, nil,
|
||||||
),
|
),
|
||||||
|
flakeInfo: prometheus.NewDesc(
|
||||||
|
"nixos_flake_info",
|
||||||
|
"Info gauge with current and remote flake revisions",
|
||||||
|
[]string{"current_rev", "remote_rev"}, nil,
|
||||||
|
),
|
||||||
revisionBehind: prometheus.NewDesc(
|
revisionBehind: prometheus.NewDesc(
|
||||||
"nixos_flake_revision_behind",
|
"nixos_flake_revision_behind",
|
||||||
"1 if current system revision differs from remote latest, 0 if match",
|
"1 if current system revision differs from remote latest, 0 if match",
|
||||||
@@ -86,6 +92,7 @@ func NewFlakeCollector(flakeURL string, checkInterval time.Duration) *FlakeColle
|
|||||||
func (c *FlakeCollector) Describe(ch chan<- *prometheus.Desc) {
|
func (c *FlakeCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||||
ch <- c.inputAge
|
ch <- c.inputAge
|
||||||
ch <- c.inputInfo
|
ch <- c.inputInfo
|
||||||
|
ch <- c.flakeInfo
|
||||||
ch <- c.revisionBehind
|
ch <- c.revisionBehind
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +175,17 @@ func (c *FlakeCollector) collectRevisionBehind(ch chan<- prometheus.Metric, data
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remoteRev := data.Revision
|
||||||
|
if len(remoteRev) > 7 {
|
||||||
|
remoteRev = remoteRev[:7]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit flake info metric with revisions
|
||||||
|
ch <- prometheus.MustNewConstMetric(c.flakeInfo, prometheus.GaugeValue, 1, currentRev, remoteRev)
|
||||||
|
|
||||||
behind := 0.0
|
behind := 0.0
|
||||||
if currentRev != "" && data.Revision != "" {
|
if currentRev != "" && data.Revision != "" {
|
||||||
// Compare short hashes
|
if currentRev != remoteRev && !strings.HasPrefix(data.Revision, currentRev) {
|
||||||
remoteShort := data.Revision
|
|
||||||
if len(remoteShort) > 7 {
|
|
||||||
remoteShort = remoteShort[:7]
|
|
||||||
}
|
|
||||||
if currentRev != remoteShort && !strings.HasPrefix(data.Revision, currentRev) {
|
|
||||||
behind = 1.0
|
behind = 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
let
|
let
|
||||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
|
|
||||||
|
# Extract version from main.go
|
||||||
|
mainGo = builtins.readFile ./main.go;
|
||||||
|
lines = builtins.filter (x: builtins.isString x) (builtins.split "\n" mainGo);
|
||||||
|
versionLine = builtins.head (builtins.filter (line: builtins.match ".*const version = .*" line != null) lines);
|
||||||
|
version = builtins.head (builtins.match ".*\"([0-9.]+)\".*" versionLine);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = forAllSystems (system:
|
packages = forAllSystems (system:
|
||||||
@@ -18,7 +24,7 @@
|
|||||||
{
|
{
|
||||||
default = pkgs.buildGoModule {
|
default = pkgs.buildGoModule {
|
||||||
pname = "nixos-exporter";
|
pname = "nixos-exporter";
|
||||||
version = "0.1.0";
|
inherit version;
|
||||||
src = ./.;
|
src = ./.;
|
||||||
vendorHash = "sha256-NnvB20rORPS5QF5enbb5KpWaKZ70ybSgfd7wjk21/Cg=";
|
vendorHash = "sha256-NnvB20rORPS5QF5enbb5KpWaKZ70ybSgfd7wjk21/Cg=";
|
||||||
|
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -15,6 +15,8 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const version = "0.2.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg, err := config.Parse()
|
cfg, err := config.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,7 +61,7 @@ func main() {
|
|||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
slog.Info("Starting server", "addr", cfg.ListenAddr)
|
slog.Info("Starting server", "version", version, "addr", cfg.ListenAddr)
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
slog.Error("Server error", "error", err)
|
slog.Error("Server error", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user