Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
3 KiB
Go
118 lines
3 KiB
Go
package collector
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
func init() {
|
|
registerCollector("corosync", func(logger *slog.Logger) Collector {
|
|
return newCorosyncCollector(logger)
|
|
})
|
|
}
|
|
|
|
type corosyncCollector struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func newCorosyncCollector(logger *slog.Logger) *corosyncCollector {
|
|
return &corosyncCollector{logger: logger}
|
|
}
|
|
|
|
type clusterConfigNodesResponse struct {
|
|
Data []clusterConfigNodeEntry `json:"data"`
|
|
}
|
|
|
|
type clusterConfigNodeEntry struct {
|
|
Node string `json:"node"`
|
|
NodeID string `json:"nodeid"`
|
|
QuorumVotes string `json:"quorum_votes"`
|
|
}
|
|
|
|
var (
|
|
clusterQuorateDesc = prometheus.NewDesc(
|
|
prometheus.BuildFQName(namespace, "cluster", "quorate"),
|
|
"Whether the cluster is quorate.",
|
|
nil,
|
|
nil,
|
|
)
|
|
clusterNodesTotalDesc = prometheus.NewDesc(
|
|
prometheus.BuildFQName(namespace, "cluster", "nodes_total"),
|
|
"Total number of nodes in the cluster.",
|
|
nil,
|
|
nil,
|
|
)
|
|
clusterExpectedVotesDesc = prometheus.NewDesc(
|
|
prometheus.BuildFQName(namespace, "cluster", "expected_votes"),
|
|
"Total expected votes in the cluster.",
|
|
nil,
|
|
nil,
|
|
)
|
|
nodeOnlineDesc = prometheus.NewDesc(
|
|
prometheus.BuildFQName(namespace, "", "node_online"),
|
|
"Whether a node is online.",
|
|
[]string{"name", "nodeid"},
|
|
nil,
|
|
)
|
|
)
|
|
|
|
func (c *corosyncCollector) Update(client *Client, ch chan<- prometheus.Metric) error {
|
|
// Fetch cluster status for quorate, node count, and node online state.
|
|
statusBody, err := client.Get("/cluster/status")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get /cluster/status: %w", err)
|
|
}
|
|
|
|
var statusResp clusterStatusResponse
|
|
if err := json.Unmarshal(statusBody, &statusResp); err != nil {
|
|
return fmt.Errorf("failed to parse /cluster/status response: %w", err)
|
|
}
|
|
|
|
var nodeCount int
|
|
for _, entry := range statusResp.Data {
|
|
switch entry.Type {
|
|
case "cluster":
|
|
ch <- prometheus.MustNewConstMetric(clusterQuorateDesc, prometheus.GaugeValue, float64(entry.Quorate))
|
|
case "node":
|
|
nodeCount++
|
|
ch <- prometheus.MustNewConstMetric(
|
|
nodeOnlineDesc,
|
|
prometheus.GaugeValue,
|
|
float64(entry.Online),
|
|
entry.Name,
|
|
strconv.Itoa(entry.NodeID),
|
|
)
|
|
}
|
|
}
|
|
|
|
ch <- prometheus.MustNewConstMetric(clusterNodesTotalDesc, prometheus.GaugeValue, float64(nodeCount))
|
|
|
|
// Fetch cluster config nodes for expected votes.
|
|
configBody, err := client.Get("/cluster/config/nodes")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get /cluster/config/nodes: %w", err)
|
|
}
|
|
|
|
var configResp clusterConfigNodesResponse
|
|
if err := json.Unmarshal(configBody, &configResp); err != nil {
|
|
return fmt.Errorf("failed to parse /cluster/config/nodes response: %w", err)
|
|
}
|
|
|
|
var expectedVotes float64
|
|
for _, node := range configResp.Data {
|
|
votes, err := strconv.ParseFloat(node.QuorumVotes, 64)
|
|
if err != nil {
|
|
c.logger.Warn("failed to parse quorum_votes", "node", node.Node, "err", err)
|
|
continue
|
|
}
|
|
expectedVotes += votes
|
|
}
|
|
|
|
ch <- prometheus.MustNewConstMetric(clusterExpectedVotesDesc, prometheus.GaugeValue, expectedVotes)
|
|
|
|
return nil
|
|
}
|