204 lines
6.3 KiB
Go
204 lines
6.3 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type CronMetric 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"`
|
|
}
|
|
|
|
var (
|
|
cronMetricMu sync.RWMutex
|
|
cronMetricHistory []CronMetric
|
|
cronMetricMaxKeep = 240
|
|
)
|
|
|
|
func (s *Server) handleCronMetrics(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
cronMetricMu.RLock()
|
|
history := append([]CronMetric(nil), cronMetricHistory...)
|
|
cronMetricMu.RUnlock()
|
|
latest := CronMetric{}
|
|
if len(history) > 0 {
|
|
latest = history[len(history)-1]
|
|
}
|
|
writeAnyJSON(w, http.StatusOK, map[string]any{
|
|
"count": len(history),
|
|
"latest": latest,
|
|
"history": history,
|
|
})
|
|
case http.MethodPost:
|
|
if !allowCronIngest(r) {
|
|
writeJSONError(w, http.StatusUnauthorized, "invalid cron token")
|
|
return
|
|
}
|
|
var payload map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid json payload")
|
|
return
|
|
}
|
|
sample := normalizeCronMetric(payload)
|
|
cronMetricMu.Lock()
|
|
cronMetricHistory = append(cronMetricHistory, sample)
|
|
if len(cronMetricHistory) > cronMetricMaxKeep {
|
|
cronMetricHistory = append([]CronMetric(nil), cronMetricHistory[len(cronMetricHistory)-cronMetricMaxKeep:]...)
|
|
}
|
|
count := len(cronMetricHistory)
|
|
cronMetricMu.Unlock()
|
|
writeAnyJSON(w, http.StatusOK, map[string]any{"ok": true, "stored": count, "latest": sample})
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func allowCronIngest(r *http.Request) bool {
|
|
expected := strings.TrimSpace(os.Getenv("CRON_TOKEN"))
|
|
if expected == "" {
|
|
return true
|
|
}
|
|
return strings.TrimSpace(r.Header.Get("X-Cron-Token")) == expected
|
|
}
|
|
|
|
func normalizeCronMetric(payload map[string]any) CronMetric {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
metric := CronMetric{
|
|
Timestamp: now,
|
|
Source: textValue(payload, "source", "function", "name", "host", "hostname", "instance"),
|
|
Hostname: textValue(payload, "hostname", "host", "node", "instance"),
|
|
Status: textValue(payload, "status"),
|
|
Notes: textValue(payload, "notes", "note", "message"),
|
|
}
|
|
if metric.Source == "" {
|
|
metric.Source = "cron"
|
|
}
|
|
|
|
metric.MemoryTotalMB = numberValue(payload, "memory_total_mb", "total_memory_mb", "mem_total_mb", "mem_total")
|
|
metric.MemoryFreeMB = numberValue(payload, "memory_free_mb", "free_memory_mb", "mem_free_mb", "free_mb")
|
|
metric.MemoryAvailMB = numberValue(payload, "memory_available_mb", "available_memory_mb", "mem_available_mb", "avail_memory_mb")
|
|
metric.MemoryUsedMB = numberValue(payload, "memory_used_mb", "used_memory_mb", "mem_used_mb", "used_mb")
|
|
metric.MemoryPercent = numberValue(payload, "memory_percent", "mem_percent", "memory_usage_percent")
|
|
if metric.MemoryUsedMB == 0 && metric.MemoryTotalMB > 0 && metric.MemoryFreeMB > 0 {
|
|
metric.MemoryUsedMB = metric.MemoryTotalMB - metric.MemoryFreeMB
|
|
}
|
|
if metric.MemoryAvailMB == 0 && metric.MemoryFreeMB > 0 {
|
|
metric.MemoryAvailMB = metric.MemoryFreeMB
|
|
}
|
|
if metric.MemoryPercent == 0 && metric.MemoryTotalMB > 0 && metric.MemoryUsedMB > 0 {
|
|
metric.MemoryPercent = (metric.MemoryUsedMB / metric.MemoryTotalMB) * 100
|
|
}
|
|
|
|
metric.CpuLoad1m = numberValue(payload, "cpu_load_1m", "loadavg_1m", "load_1m", "load1")
|
|
metric.CpuLoad5m = numberValue(payload, "cpu_load_5m", "loadavg_5m", "load_5m", "load5")
|
|
metric.CpuLoad15m = numberValue(payload, "cpu_load_15m", "loadavg_15m", "load_15m", "load15")
|
|
|
|
metric.DiskTotalGB = numberValue(payload, "disk_total_gb", "total_disk_gb", "disk_total_mb", "disk_total")
|
|
metric.DiskFreeGB = numberValue(payload, "disk_free_gb", "free_disk_gb", "disk_free_mb", "disk_free")
|
|
metric.DiskUsedGB = numberValue(payload, "disk_used_gb", "used_disk_gb", "disk_used_mb", "disk_used")
|
|
if metric.DiskUsedGB == 0 && metric.DiskTotalGB > 0 && metric.DiskFreeGB > 0 {
|
|
metric.DiskUsedGB = metric.DiskTotalGB - metric.DiskFreeGB
|
|
}
|
|
|
|
if uptime := numberValue(payload, "uptime_sec", "uptime", "uptime_seconds"); uptime > 0 {
|
|
metric.UptimeSec = uptime
|
|
}
|
|
if ts := textValue(payload, "timestamp", "ts", "collected_at"); ts != "" {
|
|
metric.Timestamp = ts
|
|
}
|
|
return metric
|
|
}
|
|
|
|
func textValue(payload map[string]any, keys ...string) string {
|
|
for _, key := range keys {
|
|
if value, ok := payload[key]; ok {
|
|
text := strings.TrimSpace(toString(value))
|
|
if text != "" {
|
|
return text
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func numberValue(payload map[string]any, keys ...string) float64 {
|
|
for _, key := range keys {
|
|
if value, ok := payload[key]; ok {
|
|
if number, ok := toFloat64(value); ok {
|
|
return number
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func toString(value any) string {
|
|
switch v := value.(type) {
|
|
case string:
|
|
return v
|
|
case []byte:
|
|
return string(v)
|
|
case float64:
|
|
return strconv.FormatFloat(v, 'f', -1, 64)
|
|
case float32:
|
|
return strconv.FormatFloat(float64(v), 'f', -1, 32)
|
|
case int:
|
|
return strconv.FormatInt(int64(v), 10)
|
|
case int64:
|
|
return strconv.FormatInt(v, 10)
|
|
case json.Number:
|
|
return v.String()
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func toFloat64(value any) (float64, bool) {
|
|
switch v := value.(type) {
|
|
case float64:
|
|
return v, true
|
|
case float32:
|
|
return float64(v), true
|
|
case int:
|
|
return float64(v), true
|
|
case int64:
|
|
return float64(v), true
|
|
case json.Number:
|
|
f, err := v.Float64()
|
|
return f, err == nil
|
|
case string:
|
|
if strings.TrimSpace(v) == "" {
|
|
return 0, false
|
|
}
|
|
n, err := strconv.ParseFloat(strings.TrimSpace(v), 64)
|
|
if err == nil {
|
|
return n, true
|
|
}
|
|
return 0, false
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|