pulse-lets-go/internal/api/monitoring.go
Maksim Totmin 40b8afb150 refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00

99 lines
3.3 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,
})
}
writeJSON(w, http.StatusOK, models.ZabbixResponse{Data: data})
}
// 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_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, ".", "_"), "-", "_")
}