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 }