feat: add subscription collector (info, status, next_due_timestamp)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-20 11:35:36 +00:00
parent 5e61f224c4
commit 7708a64408
3 changed files with 170 additions and 0 deletions

View file

@ -0,0 +1 @@
{"data":{"status":"active","level":"b","productname":"Proxmox VE Basic Subscription","nextduedate":"2027-02-03","regdate":"2025-02-03","key":"pve2b-test","sockets":2,"checktime":1773896474}}

128
collector/subscription.go Normal file
View file

@ -0,0 +1,128 @@
package collector
import (
"encoding/json"
"fmt"
"log/slog"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
)
func init() {
registerCollector("subscription", func(logger *slog.Logger) Collector {
return newSubscriptionCollector(logger)
})
}
type subscriptionCollector struct {
logger *slog.Logger
mu sync.Mutex
nodes []string
}
func newSubscriptionCollector(logger *slog.Logger) *subscriptionCollector {
return &subscriptionCollector{logger: logger}
}
func (c *subscriptionCollector) SetNodes(nodes []string) {
c.mu.Lock()
defer c.mu.Unlock()
c.nodes = nodes
}
type subscriptionResponse struct {
Data subscriptionData `json:"data"`
}
type subscriptionData struct {
Status string `json:"status"`
Level string `json:"level"`
NextDueDate string `json:"nextduedate"`
}
var (
subscriptionInfoDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "subscription", "info"),
"Subscription information for a node.",
[]string{"id", "level"},
nil,
)
subscriptionStatusDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "subscription", "status"),
"Subscription status for a node.",
[]string{"id", "status"},
nil,
)
subscriptionNextDueDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "subscription", "next_due_timestamp_seconds"),
"Next due date of the subscription as a Unix timestamp.",
[]string{"id"},
nil,
)
)
func (c *subscriptionCollector) Update(client *Client, ch chan<- prometheus.Metric) error {
c.mu.Lock()
nodes := make([]string, len(c.nodes))
copy(nodes, c.nodes)
c.mu.Unlock()
var (
wg sync.WaitGroup
errs []error
emu sync.Mutex
)
sem := make(chan struct{}, client.MaxConcurrent())
for _, node := range nodes {
wg.Add(1)
go func(node string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if err := c.collectNode(client, ch, node); err != nil {
emu.Lock()
errs = append(errs, err)
emu.Unlock()
}
}(node)
}
wg.Wait()
if len(errs) > 0 {
return fmt.Errorf("subscription collection errors: %v", errs)
}
return nil
}
func (c *subscriptionCollector) collectNode(client *Client, ch chan<- prometheus.Metric, node string) error {
body, err := client.Get(fmt.Sprintf("/nodes/%s/subscription", node))
if err != nil {
return fmt.Errorf("failed to get subscription for node %s: %w", node, err)
}
var resp subscriptionResponse
if err := json.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to parse subscription response for node %s: %w", node, err)
}
id := "node/" + node
ch <- prometheus.MustNewConstMetric(subscriptionInfoDesc, prometheus.GaugeValue, 1, id, resp.Data.Level)
ch <- prometheus.MustNewConstMetric(subscriptionStatusDesc, prometheus.GaugeValue, 1, id, resp.Data.Status)
if resp.Data.NextDueDate != "" {
t, err := time.Parse("2006-01-02", resp.Data.NextDueDate)
if err != nil {
c.logger.Warn("failed to parse nextduedate", "node", node, "err", err)
} else {
ch <- prometheus.MustNewConstMetric(subscriptionNextDueDesc, prometheus.GaugeValue, float64(t.Unix()), id)
}
}
return nil
}

View file

@ -0,0 +1,41 @@
package collector
import (
"log/slog"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func TestSubscriptionCollector(t *testing.T) {
client := newTestClient(t, map[string]string{
"/nodes/node01/subscription": "node_subscription.json",
})
collector := newSubscriptionCollector(slog.Default())
collector.SetNodes([]string{"node01"})
adapter := &testCollectorAdapter{client: client, collector: collector}
reg := prometheus.NewRegistry()
reg.MustRegister(adapter)
expected := `
# HELP pve_subscription_info Subscription information for a node.
# TYPE pve_subscription_info gauge
pve_subscription_info{id="node/node01",level="b"} 1
# HELP pve_subscription_next_due_timestamp_seconds Next due date of the subscription as a Unix timestamp.
# TYPE pve_subscription_next_due_timestamp_seconds gauge
pve_subscription_next_due_timestamp_seconds{id="node/node01"} 1.8016128e+09
# HELP pve_subscription_status Subscription status for a node.
# TYPE pve_subscription_status gauge
pve_subscription_status{id="node/node01",status="active"} 1
`
if err := testutil.GatherAndCompare(reg, strings.NewReader(expected),
"pve_subscription_info", "pve_subscription_status", "pve_subscription_next_due_timestamp_seconds",
); err != nil {
t.Errorf("unexpected metrics: %s", err)
}
}