101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
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)
|
||
}
|