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:
parent
5e61f224c4
commit
7708a64408
3 changed files with 170 additions and 0 deletions
1
collector/fixtures/node_subscription.json
Normal file
1
collector/fixtures/node_subscription.json
Normal 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
128
collector/subscription.go
Normal 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
|
||||||
|
}
|
||||||
41
collector/subscription_test.go
Normal file
41
collector/subscription_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue