Files
futriix/internal/metrics/collector.go
T

267 lines
8.1 KiB
Go
Raw Normal View History

2026-09-20 22:10:08 +00:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/metrics/collector.go
// Назначение: Периодический сбор метрик из storage и cluster и публикация
// их в Prometheus-реестре. Работает на Linux и OpenIndiana.
package metrics
import (
"fmt"
"time"
)
// CollectorDeps зависимости коллектора
type CollectorDeps struct {
Storage StorageStatsProvider
Coordinator CoordinatorStatsProvider
Logger Logger
Interval time.Duration
}
// StorageStatsProvider интерфейс для получения статистики storage
type StorageStatsProvider interface {
GetStats() map[string]interface{}
GetDatabaseCount() int
GetTotalDocuments() int64
}
// CoordinatorStatsProvider интерфейс для получения статистики cluster
type CoordinatorStatsProvider interface {
GetClusterStatus() ClusterStatusLike
GetActiveNodes() []NodeInfoLike
GetAllNodes() []NodeInfoLike
IsLeader() bool
GetCurrentTerm() uint64
}
// ClusterStatusLike минимальный интерфейс статуса кластера
type ClusterStatusLike interface {
GetTotalNodes() int
GetActiveNodes() int
GetFailedNodes() int
GetLeaderID() string
GetHealth() string
}
// NodeInfoLike минимальный интерфейс узла
type NodeInfoLike interface {
GetID() string
GetIP() string
GetPort() int
GetStatus() string
GetLastSeen() int64
}
// Logger минимальный интерфейс логгера
type Logger interface {
Debug(msg string)
Info(msg string)
Warn(msg string)
Error(msg string)
}
// Collector собирает метрики и публикует в реестр
type Collector struct {
deps CollectorDeps
registry *Registry
// Метрики
mStorageDatabases *MetricFamily
mStorageDocuments *MetricFamily
mClusterNodes *MetricFamily
mClusterLeader *MetricFamily
mClusterTerm *MetricFamily
mNodeLastSeen *MetricFamily
mHTTPRequests *MetricFamily
mHTTPDuration *MetricFamily
mReplicationTotal *MetricFamily
mReplicationFailed *MetricFamily
mBackpressureLevel *MetricFamily
mMigrationTasks *MetricFamily
stop chan struct{}
}
// NewCollector создаёт новый коллектор
func NewCollector(deps CollectorDeps) *Collector {
if deps.Interval <= 0 {
deps.Interval = 15 * time.Second
}
r := DefaultRegistry()
c := &Collector{
deps: deps,
registry: r,
stop: make(chan struct{}),
}
c.mStorageDatabases = r.RegisterGauge("futriis_storage_databases_total",
"Total number of databases")
c.mStorageDocuments = r.RegisterGauge("futriis_storage_documents_total",
"Total number of documents across all collections")
c.mClusterNodes = r.RegisterGauge("futriis_cluster_nodes",
"Number of cluster nodes by status")
c.mClusterLeader = r.RegisterGauge("futriis_cluster_has_leader",
"Whether a leader is elected (1) or not (0)")
c.mClusterTerm = r.RegisterGauge("futriis_cluster_raft_term",
"Current Raft term")
c.mNodeLastSeen = r.RegisterGauge("futriis_node_last_seen_seconds",
"Unix timestamp of last node contact")
c.mHTTPRequests = r.RegisterCounter("futriis_http_requests_total",
"Total number of HTTP requests")
c.mHTTPDuration = r.RegisterHistogram("futriis_http_request_duration_seconds",
"HTTP request duration in seconds",
[]float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5})
c.mReplicationTotal = r.RegisterCounter("futriis_replication_total",
"Total replication operations")
c.mReplicationFailed = r.RegisterCounter("futriis_replication_failed_total",
"Failed replication operations")
c.mBackpressureLevel = r.RegisterGauge("futriis_backpressure_level",
"Current backpressure level (0=none,4=critical)")
c.mMigrationTasks = r.RegisterGauge("futriis_migration_tasks",
"Migration tasks by status")
return c
}
// Start запускает периодический сбор метрик
func (c *Collector) Start() {
go c.loop()
if c.deps.Logger != nil {
c.deps.Logger.Info(fmt.Sprintf("Prometheus metrics collector started (interval=%v)", c.deps.Interval))
}
}
// Stop останавливает коллектор
func (c *Collector) Stop() {
close(c.stop)
}
func (c *Collector) loop() {
ticker := time.NewTicker(c.deps.Interval)
defer ticker.Stop()
c.collect()
for {
select {
case <-c.stop:
return
case <-ticker.C:
c.collect()
}
}
}
// collect собирает все метрики
func (c *Collector) collect() {
c.collectStorage()
c.collectCluster()
}
func (c *Collector) collectStorage() {
if c.deps.Storage == nil {
return
}
defer func() {
if r := recover(); r != nil && c.deps.Logger != nil {
c.deps.Logger.Error(fmt.Sprintf("metrics: storage collect panic: %v", r))
}
}()
c.mStorageDatabases.WithLabels().Set(float64(c.deps.Storage.GetDatabaseCount()))
c.mStorageDocuments.WithLabels().Set(float64(c.deps.Storage.GetTotalDocuments()))
stats := c.deps.Storage.GetStats()
if dbs, ok := stats["databases"].([]map[string]interface{}); ok {
for _, db := range dbs {
name, _ := db["name"].(string)
docs, _ := db["documents"].(int64)
size, _ := db["size_bytes"].(int64)
c.registry.RegisterGauge("futriis_database_documents_total",
"Documents per database").WithLabels(LabelPair{"database", name}).Set(float64(docs))
c.registry.RegisterGauge("futriis_database_size_bytes",
"Size of database in bytes").WithLabels(LabelPair{"database", name}).Set(float64(size))
}
}
}
func (c *Collector) collectCluster() {
if c.deps.Coordinator == nil {
return
}
defer func() {
if r := recover(); r != nil && c.deps.Logger != nil {
c.deps.Logger.Error(fmt.Sprintf("metrics: cluster collect panic: %v", r))
}
}()
status := c.deps.Coordinator.GetClusterStatus()
if status != nil {
c.mClusterNodes.WithLabels(LabelPair{"status", "total"}).Set(float64(status.GetTotalNodes()))
c.mClusterNodes.WithLabels(LabelPair{"status", "active"}).Set(float64(status.GetActiveNodes()))
c.mClusterNodes.WithLabels(LabelPair{"status", "failed"}).Set(float64(status.GetFailedNodes()))
if status.GetLeaderID() != "" {
c.mClusterLeader.WithLabels().Set(1)
} else {
c.mClusterLeader.WithLabels().Set(0)
}
c.registry.RegisterGauge("futriis_cluster_health",
"Cluster health status (1=healthy, 0.5=degraded, 0=critical)").
WithLabels(LabelPair{"health", status.GetHealth()}).Set(1)
}
if c.deps.Coordinator.IsLeader() {
c.mClusterLeader.WithLabels(LabelPair{"role", "self"}).Set(1)
} else {
c.mClusterLeader.WithLabels(LabelPair{"role", "self"}).Set(0)
}
c.mClusterTerm.WithLabels().Set(float64(c.deps.Coordinator.GetCurrentTerm()))
for _, node := range c.deps.Coordinator.GetActiveNodes() {
c.mNodeLastSeen.WithLabels(
LabelPair{"node_id", node.GetID()},
LabelPair{"ip", node.GetIP()},
).Set(float64(node.GetLastSeen()) / 1000.0)
}
}
// HTTPRequestObserved регистрирует факт HTTP-запроса
func (c *Collector) HTTPRequestObserved(method, path string, status int, duration time.Duration) {
c.mHTTPRequests.WithLabels(
LabelPair{"method", method},
LabelPair{"path", path},
LabelPair{"status", fmt.Sprintf("%d", status)},
).Inc()
c.mHTTPDuration.WithLabels(
LabelPair{"method", method},
LabelPair{"path", path},
).Observe(duration.Seconds())
}
// ReplicationObserved регистрирует операцию репликации
func (c *Collector) ReplicationObserved(success bool) {
c.mReplicationTotal.WithLabels().Inc()
if !success {
c.mReplicationFailed.WithLabels().Inc()
}
}
// SetBackpressureLevel устанавливает уровень backpressure
func (c *Collector) SetBackpressureLevel(level int) {
c.mBackpressureLevel.WithLabels().Set(float64(level))
}
// SetMigrationTasks устанавливает количество задач миграции
func (c *Collector) SetMigrationTasks(status string, count int) {
c.mMigrationTasks.WithLabels(LabelPair{"status", status}).Set(float64(count))
}