Files
files/internal/handler/health.go
Grendgi 1144b11ca3
All checks were successful
CI / hygiene (push) Successful in 1s
Build and Deploy / build-and-deploy (push) Successful in 34s
CI / test (push) Successful in 32s
feat: expose files storage health detail
2026-06-17 15:47:14 +03:00

107 lines
3.3 KiB
Go

package handler
import (
"context"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"files-service/internal/storage"
)
type HealthHandler struct {
pool *pgxpool.Pool
store *storage.Storage
}
func NewHealthHandler(pool *pgxpool.Pool, store *storage.Storage) *HealthHandler {
return &HealthHandler{pool: pool, store: store}
}
func (h *HealthHandler) Healthz(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (h *HealthHandler) Readyz(w http.ResponseWriter, r *http.Request) {
if err := h.pool.Ping(r.Context()); err != nil {
writeInternalError(w, r, err, "database unavailable")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}
func (h *HealthHandler) Detail(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
components := []componentProbe{
h.probePostgres(ctx),
h.probeStorage(ctx),
h.probeTrash(ctx),
h.probeStorageMetadata(ctx),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"components": components})
}
type componentProbe struct {
Name string `json:"name"`
Status string `json:"status"`
LatencyMs int64 `json:"latency_ms"`
Error string `json:"error,omitempty"`
}
func (h *HealthHandler) probePostgres(ctx context.Context) componentProbe {
start := time.Now()
if err := h.pool.Ping(ctx); err != nil {
return componentProbe{Name: "postgres", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
}
return componentProbe{Name: "postgres", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
}
func (h *HealthHandler) probeStorage(ctx context.Context) componentProbe {
start := time.Now()
if err := h.store.Check(ctx); err != nil {
return componentProbe{Name: "minio_storage", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
}
return componentProbe{Name: "minio_storage", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
}
func (h *HealthHandler) probeTrash(ctx context.Context) componentProbe {
start := time.Now()
var due int
if err := h.pool.QueryRow(ctx, `
SELECT COUNT(*)::int
FROM files_nodes
WHERE purge_after IS NOT NULL
AND purge_after <= now()
`).Scan(&due); err != nil {
return componentProbe{Name: "trash_purger", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
}
if due > 0 {
return componentProbe{Name: "trash_purger", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: "purge_due=" + strconv.Itoa(due)}
}
return componentProbe{Name: "trash_purger", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
}
func (h *HealthHandler) probeStorageMetadata(ctx context.Context) componentProbe {
start := time.Now()
var activeObjects int
if err := h.pool.QueryRow(ctx, `
SELECT COUNT(*)::int
FROM files_nodes
WHERE deleted_at IS NULL
AND storage_key IS NOT NULL
`).Scan(&activeObjects); err != nil {
return componentProbe{Name: "storage_metadata", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
}
return componentProbe{
Name: "storage_metadata",
Status: "ok",
LatencyMs: time.Since(start).Milliseconds(),
}
}