pulse-lets-go/internal/api/monitoring.go
Maksim Totmin 727fd1195a feat: add route_fallbacks metric to Prometheus and Zabbix monitoring
- Prometheus: new pulse_route_fallbacks_total counter
- Zabbix: extend response with route_requests_total, route_fallbacks_total, uptime_seconds
- Docs: update Prometheus metrics table and Zabbix response example
2026-06-25 23:27:12 +07:00

109 lines
3.7 KiB
Go

package api
import (
"fmt"
"net/http"
"strings"
"github.com/pulse-lets-go/internal/models"
)
// handleZabbix — GET /api/monitoring/zabbix — возвращает JSON для Zabbix LLD + items.
func (a *API) handleZabbix(w http.ResponseWriter, r *http.Request) {
nodes := a.engine.GetAllNodes()
data := make([]models.ZabbixNode, 0, len(nodes))
for _, n := range nodes {
data = append(data, models.ZabbixNode{
NodeID: n.NodeID,
Status: n.Status,
ActiveCalls: n.ActiveCalls,
MaxCalls: n.MaxCalls,
IdleCPU: n.IdleCPU,
LoadAvg: n.LoadAvg,
CallFailureRate: n.CallFailureRate,
Score: n.Score,
Disabled: n.Disabled,
IsStale: n.IsStale,
SecondsAgo: n.SecondsAgo,
})
}
stats := a.engine.GetHealthStats()
writeJSON(w, http.StatusOK, models.ZabbixResponse{
Data: data,
RouteRequests: stats.RouteRequests,
RouteFallbacks: stats.RouteFallbacks,
UptimeSeconds: stats.UptimeSeconds,
})
}
// handlePrometheus — GET /api/monitoring/prometheus — text/plain метрики для Prometheus.
func (a *API) handlePrometheus(w http.ResponseWriter, r *http.Request) {
nodes := a.engine.GetAllNodes()
var sb strings.Builder
// Общие метрики (counter gauge)
stats := a.engine.GetHealthStats()
fmt.Fprintf(&sb, "# HELP pulse_route_requests_total Total number of route requests.\n")
fmt.Fprintf(&sb, "# TYPE pulse_route_requests_total counter\n")
fmt.Fprintf(&sb, "pulse_route_requests_total %d\n", stats.RouteRequests)
fmt.Fprintf(&sb, "# HELP pulse_nodes_total Total nodes registered.\n")
fmt.Fprintf(&sb, "# TYPE pulse_nodes_total gauge\n")
fmt.Fprintf(&sb, "pulse_nodes_total %d\n", stats.TotalNodes)
fmt.Fprintf(&sb, "# HELP pulse_nodes_healthy Healthy nodes count.\n")
fmt.Fprintf(&sb, "# TYPE pulse_nodes_healthy gauge\n")
fmt.Fprintf(&sb, "pulse_nodes_healthy %d\n", stats.HealthyNodes)
fmt.Fprintf(&sb, "# HELP pulse_route_fallbacks_total Total number of fallback route requests.\n")
fmt.Fprintf(&sb, "# TYPE pulse_route_fallbacks_total counter\n")
fmt.Fprintf(&sb, "pulse_route_fallbacks_total %d\n", stats.RouteFallbacks)
fmt.Fprintf(&sb, "# HELP pulse_uptime_seconds Uptime in seconds.\n")
fmt.Fprintf(&sb, "# TYPE pulse_uptime_seconds gauge\n")
fmt.Fprintf(&sb, "pulse_uptime_seconds %d\n", stats.UptimeSeconds)
fmt.Fprintf(&sb, "\n")
for _, n := range nodes {
id := sanitizePromLabel(n.NodeID)
fmt.Fprintf(&sb, "pulse_node_active_calls{node=\"%s\"} %d\n", id, n.ActiveCalls)
fmt.Fprintf(&sb, "pulse_node_max_calls{node=\"%s\"} %d\n", id, n.MaxCalls)
fmt.Fprintf(&sb, "pulse_node_idle_cpu{node=\"%s\"} %.2f\n", id, n.IdleCPU)
fmt.Fprintf(&sb, "pulse_node_load_avg{node=\"%s\"} %.2f\n", id, n.LoadAvg)
fmt.Fprintf(&sb, "pulse_node_call_failure_rate{node=\"%s\"} %.2f\n", id, n.CallFailureRate)
fmt.Fprintf(&sb, "pulse_node_score{node=\"%s\"} %.2f\n", id, n.Score)
fmt.Fprintf(&sb, "pulse_node_seconds_ago{node=\"%s\"} %.2f\n", id, n.SecondsAgo)
disabledVal := 0
staleVal := 0
statusVal := 1 // ok
if n.Disabled {
disabledVal = 1
}
if n.IsStale {
staleVal = 1
}
if n.Status != "ok" {
statusVal = 0
}
fmt.Fprintf(&sb, "pulse_node_disabled{node=\"%s\"} %d\n", id, disabledVal)
fmt.Fprintf(&sb, "pulse_node_stale{node=\"%s\"} %d\n", id, staleVal)
fmt.Fprintf(&sb, "pulse_node_status_ok{node=\"%s\"} %d\n", id, statusVal)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(sb.String()))
}
// sanitizePromLabel заменяет символы, недопустимые в Prometheus-лейблах.
func sanitizePromLabel(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, ".", "_"), "-", "_")
}