Files
fission-console/metrics-collector/internal/api/server.go
T

216 lines
6.2 KiB
Go

package api
import (
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
// Metric — один снимок состояния, принятый через POST /metrics.
type Metric struct {
Timestamp string `json:"timestamp"`
Source string `json:"source"`
Hostname string `json:"hostname,omitempty"`
Status string `json:"status,omitempty"`
MemoryTotalMB float64 `json:"memory_total_mb,omitempty"`
MemoryFreeMB float64 `json:"memory_free_mb,omitempty"`
MemoryAvailMB float64 `json:"memory_available_mb,omitempty"`
MemoryUsedMB float64 `json:"memory_used_mb,omitempty"`
MemoryPercent float64 `json:"memory_percent,omitempty"`
CpuLoad1m float64 `json:"cpu_load_1m,omitempty"`
CpuLoad5m float64 `json:"cpu_load_5m,omitempty"`
CpuLoad15m float64 `json:"cpu_load_15m,omitempty"`
DiskTotalGB float64 `json:"disk_total_gb,omitempty"`
DiskFreeGB float64 `json:"disk_free_gb,omitempty"`
DiskUsedGB float64 `json:"disk_used_gb,omitempty"`
UptimeSec float64 `json:"uptime_sec,omitempty"`
Notes string `json:"notes,omitempty"`
}
const maxHistory = 240
// Server хранит историю снимков в памяти.
type Server struct {
mu sync.RWMutex
history []Metric
}
func NewServer() *Server {
return &Server{}
}
// HandleMetrics — единственный публичный эндпоинт.
// GET /metrics → отдаёт историю.
// POST /metrics → сохраняет снимок.
func (s *Server) HandleMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Metrics-Token")
switch r.Method {
case http.MethodOptions:
w.WriteHeader(http.StatusNoContent)
case http.MethodGet:
s.mu.RLock()
history := append([]Metric(nil), s.history...)
s.mu.RUnlock()
latest := Metric{}
if len(history) > 0 {
latest = history[len(history)-1]
}
writeJSON(w, http.StatusOK, map[string]any{
"count": len(history),
"latest": latest,
"history": history,
})
case http.MethodPost:
if !s.authorized(r) {
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "invalid token"})
return
}
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid json"})
return
}
m := normalize(payload)
s.mu.Lock()
s.history = append(s.history, m)
if len(s.history) > maxHistory {
s.history = append([]Metric(nil), s.history[len(s.history)-maxHistory:]...)
}
count := len(s.history)
s.mu.Unlock()
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "stored": count})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
// authorized проверяет METRICS_TOKEN если он задан.
func (s *Server) authorized(r *http.Request) bool {
expected := strings.TrimSpace(os.Getenv("METRICS_TOKEN"))
if expected == "" {
return true
}
return strings.TrimSpace(r.Header.Get("X-Metrics-Token")) == expected
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
// normalize приводит произвольный payload к структуре Metric.
func normalize(p map[string]any) Metric {
now := time.Now().UTC().Format(time.RFC3339)
m := Metric{
Timestamp: now,
Source: textVal(p, "source", "function", "name", "host", "hostname", "instance"),
Hostname: textVal(p, "hostname", "host", "node", "instance"),
Status: textVal(p, "status"),
Notes: textVal(p, "notes", "note", "message"),
}
if m.Source == "" {
m.Source = "function"
}
m.MemoryTotalMB = numVal(p, "memory_total_mb", "total_memory_mb", "mem_total_mb")
m.MemoryFreeMB = numVal(p, "memory_free_mb", "free_memory_mb", "mem_free_mb", "free_mb")
m.MemoryAvailMB = numVal(p, "memory_available_mb", "available_memory_mb", "mem_available_mb")
m.MemoryUsedMB = numVal(p, "memory_used_mb", "used_memory_mb", "mem_used_mb", "used_mb")
m.MemoryPercent = numVal(p, "memory_percent", "mem_percent", "memory_usage_percent")
if m.MemoryUsedMB == 0 && m.MemoryTotalMB > 0 && m.MemoryFreeMB > 0 {
m.MemoryUsedMB = m.MemoryTotalMB - m.MemoryFreeMB
}
if m.MemoryAvailMB == 0 && m.MemoryFreeMB > 0 {
m.MemoryAvailMB = m.MemoryFreeMB
}
if m.MemoryPercent == 0 && m.MemoryTotalMB > 0 && m.MemoryUsedMB > 0 {
m.MemoryPercent = (m.MemoryUsedMB / m.MemoryTotalMB) * 100
}
m.CpuLoad1m = numVal(p, "cpu_load_1m", "loadavg_1m", "load_1m", "load1")
m.CpuLoad5m = numVal(p, "cpu_load_5m", "loadavg_5m", "load_5m", "load5")
m.CpuLoad15m = numVal(p, "cpu_load_15m", "loadavg_15m", "load_15m", "load15")
m.DiskTotalGB = numVal(p, "disk_total_gb", "total_disk_gb")
m.DiskFreeGB = numVal(p, "disk_free_gb", "free_disk_gb")
m.DiskUsedGB = numVal(p, "disk_used_gb", "used_disk_gb")
if m.DiskUsedGB == 0 && m.DiskTotalGB > 0 && m.DiskFreeGB > 0 {
m.DiskUsedGB = m.DiskTotalGB - m.DiskFreeGB
}
m.UptimeSec = numVal(p, "uptime_sec", "uptime", "uptime_seconds")
if ts := textVal(p, "timestamp", "ts", "collected_at"); ts != "" {
m.Timestamp = ts
}
return m
}
func textVal(p map[string]any, keys ...string) string {
for _, k := range keys {
if v, ok := p[k]; ok {
if s := strings.TrimSpace(anyToString(v)); s != "" {
return s
}
}
}
return ""
}
func numVal(p map[string]any, keys ...string) float64 {
for _, k := range keys {
if v, ok := p[k]; ok {
if f, ok := anyToFloat64(v); ok {
return f
}
}
}
return 0
}
func anyToString(v any) string {
switch t := v.(type) {
case string:
return t
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case json.Number:
return t.String()
default:
return ""
}
}
func anyToFloat64(v any) (float64, bool) {
switch t := v.(type) {
case float64:
return t, true
case float32:
return float64(t), true
case int:
return float64(t), true
case int64:
return float64(t), true
case json.Number:
f, err := t.Float64()
return f, err == nil
case string:
f, err := strconv.ParseFloat(strings.TrimSpace(t), 64)
return f, err == nil
default:
return 0, false
}
}