refactor: extract metrics-collector from fission-console, bump console to v1.3.12
This commit is contained in:
@@ -52,7 +52,7 @@ spec:
|
||||
serviceAccountName: fission-console
|
||||
containers:
|
||||
- name: console
|
||||
image: naeel/fission-console:v1.3.11
|
||||
image: naeel/fission-console:v1.3.12
|
||||
ports:
|
||||
- containerPort: 8090
|
||||
env:
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -124,12 +124,6 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
uiHandler := http.StripPrefix("/console", ui.Handler())
|
||||
mux.Handle("/console", uiHandler)
|
||||
mux.Handle("/console/", uiHandler)
|
||||
cronHandler := ui.CronHandler()
|
||||
mux.Handle("/cron", cronHandler)
|
||||
mux.Handle("/cron/", cronHandler)
|
||||
mux.Handle("/console/cron", cronHandler)
|
||||
mux.Handle("/console/cron/", cronHandler)
|
||||
|
||||
// Auth: не требует токена — сам проверяет и возвращает namespace
|
||||
mux.HandleFunc("/console/api/auth", s.handleAuth)
|
||||
|
||||
@@ -144,9 +138,6 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/httptriggers", s.handleList(fission.HTTPTrigGVR))
|
||||
mux.HandleFunc("/api/timetriggers", s.handleTimeTriggersRoot)
|
||||
mux.HandleFunc("/api/timetriggers/", s.handleTimeTriggersAction)
|
||||
mux.HandleFunc("/cron/api/metrics", s.handleCronMetrics)
|
||||
mux.HandleFunc("/console/cron/api/metrics", s.handleCronMetrics)
|
||||
|
||||
// Основные /console/api/* маршруты
|
||||
mux.HandleFunc("/console/api/environments", auth(s.handleList(fission.EnvironmentGVR)))
|
||||
mux.HandleFunc("/console/api/packages", auth(s.handleList(fission.PackageGVR)))
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /build
|
||||
COPY go.mod ./
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o metrics-collector ./cmd/server/
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=builder /build/metrics-collector /metrics-collector
|
||||
EXPOSE 8091
|
||||
ENTRYPOINT ["/metrics-collector"]
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"metrics-collector/internal/api"
|
||||
"metrics-collector/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8091"
|
||||
}
|
||||
|
||||
srv := api.NewServer()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
mux.HandleFunc("/metrics", srv.HandleMetrics)
|
||||
mux.Handle("/", ui.Handler())
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("metrics-collector listening on :%s", port)
|
||||
if err := httpSrv.ListenAndServe(); err != nil {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: metrics-collector
|
||||
namespace: fission
|
||||
labels:
|
||||
app: metrics-collector
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: metrics-collector
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: metrics-collector
|
||||
spec:
|
||||
containers:
|
||||
- name: metrics-collector
|
||||
image: naeel/metrics-collector:v0.1.0
|
||||
ports:
|
||||
- containerPort: 8091
|
||||
env:
|
||||
- name: PORT
|
||||
value: "8091"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8091
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8091
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: metrics-collector
|
||||
namespace: fission
|
||||
spec:
|
||||
selector:
|
||||
app: metrics-collector
|
||||
ports:
|
||||
- port: 8091
|
||||
targetPort: 8091
|
||||
@@ -0,0 +1,3 @@
|
||||
module metrics-collector
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,215 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed cron.html
|
||||
var cronPage string
|
||||
//go:embed index.html
|
||||
var page string
|
||||
|
||||
func CronHandler() http.Handler {
|
||||
func Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(cronPage))
|
||||
_, _ = w.Write([]byte(page))
|
||||
})
|
||||
}
|
||||
@@ -228,7 +228,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/cron/api/metrics';
|
||||
const API = '/metrics';
|
||||
const summaryEl = document.getElementById('summary');
|
||||
const historyBody = document.getElementById('history-body');
|
||||
const lastUpdatedEl = document.getElementById('last-updated');
|
||||
Reference in New Issue
Block a user