9298216ce0
- PROJ-40: /api/health mit Version+Uptime, /metrics Prometheus-Format (mails_last_60min/24h/7d/30d, mails_total, storage_bytes, tenants_total, users_total, uptime_seconds) — Token-Schutz optional konfigurierbar - PROJ-41: GET /api/admin/stats/timeseries (30-Tage tagesgenau, Tenant-scoped) + SVG-Balkendiagramm im Dashboard (Mail-Eingang letzte 30 Tage) - storage.DBQueryRow() Helper für Metrics-Queries ohne Pool-Exposition - config.MetricsConfig (enabled, token) in config.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.2 KiB
Go
93 lines
3.2 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// handleHealth returns a simple health check response.
|
|
// GET /api/health (public, no auth)
|
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"status": "ok",
|
|
"version": s.appVersion,
|
|
"uptime_sec": int64(time.Since(s.startTime).Seconds()),
|
|
})
|
|
}
|
|
|
|
// handleMetrics serves Prometheus-compatible metrics.
|
|
// GET /metrics (public or token-protected)
|
|
//
|
|
// Metrics are DB-backed gauges; no in-process counters needed.
|
|
// Format: https://prometheus.io/docs/instrumenting/exposition_formats/
|
|
func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
|
if !s.metricsCfg.Enabled {
|
|
writeError(w, http.StatusNotFound, "metrics disabled")
|
|
return
|
|
}
|
|
|
|
// Optional token protection
|
|
if s.metricsCfg.Token != "" {
|
|
tok := extractBearerToken(r)
|
|
if tok == "" {
|
|
// Also accept ?token= query param for scraper convenience
|
|
tok = r.URL.Query().Get("token")
|
|
}
|
|
if tok != s.metricsCfg.Token {
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="archivmail metrics"`)
|
|
writeError(w, http.StatusUnauthorized, "invalid metrics token")
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx := r.Context()
|
|
var sb strings.Builder
|
|
|
|
metric := func(name, help, typ string, val interface{}) {
|
|
fmt.Fprintf(&sb, "# HELP %s %s\n", name, help)
|
|
fmt.Fprintf(&sb, "# TYPE %s %s\n", name, typ)
|
|
fmt.Fprintf(&sb, "%s %v\n", name, val)
|
|
}
|
|
|
|
// ── Mail counts ───────────────────────────────────────────────────────
|
|
activity, _ := s.store.MailActivityStats(ctx)
|
|
if activity != nil {
|
|
metric("archivmail_mails_last_60min", "Mails received in the last 60 minutes", "gauge", activity.Last60Min)
|
|
metric("archivmail_mails_last_24h", "Mails received in the last 24 hours", "gauge", activity.Last24h)
|
|
metric("archivmail_mails_last_7d", "Mails received in the last 7 days", "gauge", activity.Last7d)
|
|
metric("archivmail_mails_last_30d", "Mails received in the last 30 days", "gauge", activity.Last30d)
|
|
}
|
|
|
|
// Total mails + storage bytes from DB
|
|
var totalMails int64
|
|
var totalBytes int64
|
|
if s.store != nil {
|
|
_ = s.store.DBQueryRow(ctx,
|
|
`SELECT COUNT(*), COALESCE(SUM(size_bytes),0) FROM emails`,
|
|
).Scan(&totalMails, &totalBytes)
|
|
}
|
|
metric("archivmail_mails_total", "Total number of archived mails", "gauge", totalMails)
|
|
metric("archivmail_storage_bytes", "Total size of archived mails in bytes", "gauge", totalBytes)
|
|
|
|
// Tenant count
|
|
var tenantCount int64
|
|
if s.tenantStore != nil {
|
|
_ = s.store.DBQueryRow(ctx, `SELECT COUNT(*) FROM tenants`).Scan(&tenantCount)
|
|
}
|
|
metric("archivmail_tenants_total", "Total number of tenants", "gauge", tenantCount)
|
|
|
|
// User count
|
|
var userCount int64
|
|
_ = s.store.DBQueryRow(ctx, `SELECT COUNT(*) FROM users WHERE active = true`).Scan(&userCount)
|
|
metric("archivmail_users_total", "Total number of active users", "gauge", userCount)
|
|
|
|
// Process uptime
|
|
metric("archivmail_uptime_seconds", "Process uptime in seconds", "gauge", int64(time.Since(s.startTime).Seconds()))
|
|
|
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(sb.String())) //nolint:errcheck
|
|
}
|