Files
learning/internal/handler/health.go
Grendgi 5ad2a8a33e
Some checks failed
CI / hygiene (push) Successful in 2s
Build and Deploy / build-and-deploy (push) Successful in 27s
CI / test (push) Failing after 19s
feat: expose learning video health detail
2026-06-17 16:03:25 +03:00

101 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"context"
"net/http"
"strconv"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"learning-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}
}
// Healthz — liveness. Не дёргает БД; жив если процесс отвечает.
func (h *HealthHandler) Healthz(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
// Readyz — readiness. Один Ping к БД с таймаутом — если не отвечает,
// k8s выкидывает pod из service-балансира.
func (h *HealthHandler) Readyz(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := h.pool.Ping(ctx); err != nil {
writeError(w, http.StatusServiceUnavailable, "db not ready")
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(), 3*time.Second)
defer cancel()
components := []componentProbe{
h.probePostgres(ctx),
h.probeVideoStorage(ctx),
h.probeVideoMetadata(ctx),
}
writeJSON(w, http.StatusOK, 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) probeVideoStorage(ctx context.Context) componentProbe {
start := time.Now()
if err := h.store.Check(ctx); err != nil {
return componentProbe{Name: "video_storage", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
}
return componentProbe{Name: "video_storage", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
}
func (h *HealthHandler) probeVideoMetadata(ctx context.Context) componentProbe {
start := time.Now()
var videos, broken int
err := h.pool.QueryRow(ctx, `
SELECT
COUNT(*) FILTER (WHERE video_key <> '')::int,
COUNT(*) FILTER (WHERE video_key <> '' AND btrim(video_key) = '')::int
FROM lessons`,
).Scan(&videos, &broken)
if err != nil {
return componentProbe{Name: "video_metadata", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
}
if broken > 0 {
return componentProbe{
Name: "video_metadata",
Status: "down",
LatencyMs: time.Since(start).Milliseconds(),
Error: "videos=" + intString(videos) + " broken_video_keys=" + intString(broken),
}
}
return componentProbe{Name: "video_metadata", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
}
func intString(v int) string {
return strconv.Itoa(v)
}