health.go
⎇
Raw
1package handler
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "strings"
8 "time"
9
10 "vidarchive/internal/util"
11)
12
13// healthProbeTimeout caps the external version lookups so the endpoint always
14// answers promptly, even when a tool is wedged.
15const healthProbeTimeout = 3 * time.Second
16
17type healthResponse struct {
18 Status string `json:"status"`
19 Dependencies map[string]string `json:"dependencies"`
20}
21
22// Health reports whether the app can reach its database and external tools. It
23// returns 503 when the database is unreachable, so a container healthcheck or
24// orchestrator can act on it. A missing yt-dlp/ffmpeg is reported but does not
25// fail the check: the UI still works, only downloads are affected.
26func (h *Handler) Health(w http.ResponseWriter, r *http.Request) {
27 // Bound the whole probe: a health check that can hang is worse than useless,
28 // since an orchestrator reads a stuck request as "still starting".
29 ctx, cancel := context.WithTimeout(r.Context(), healthProbeTimeout)
30 defer cancel()
31
32 resp := healthResponse{Status: "ok", Dependencies: make(map[string]string, 4)}
33
34 // The database is probed first because it alone decides the status code, and
35 // it must get the budget before a wedged tool can spend it — otherwise a slow
36 // yt-dlp reports the database as unreachable and the container is restarted
37 // for the wrong reason.
38 status := http.StatusOK
39 if err := h.downloadSvc.Ping(ctx); err != nil {
40 resp.Status = "unhealthy"
41 resp.Dependencies["database"] = "unreachable"
42 status = http.StatusServiceUnavailable
43 } else {
44 resp.Dependencies["database"] = "ok"
45 }
46
47 resp.Dependencies["yt-dlp"] = toolVersion(ctx, h.cfg.YTDLPPath, "--version")
48 resp.Dependencies["ffmpeg"] = toolVersion(ctx, h.cfg.FFmpegPath, "-version")
49 resp.Dependencies["ffprobe"] = toolVersion(ctx, h.cfg.FFprobePath, "-version")
50
51 w.Header().Set("Content-Type", "application/json")
52 w.WriteHeader(status)
53 _ = json.NewEncoder(w).Encode(resp)
54}
55
56// toolVersion returns the first line of the tool's version output. It reports
57// "not installed" when the binary is missing and "timed out" when it doesn't
58// answer within ctx.
59func toolVersion(ctx context.Context, path string, versionArg string) string {
60 out, err := util.KillableCommand(ctx, path, versionArg).Output()
61 if err != nil {
62 if ctx.Err() != nil {
63 return "timed out"
64 }
65 return "not installed"
66 }
67 line, _, _ := strings.Cut(strings.TrimSpace(string(out)), "\n")
68 // ffmpeg prints "ffmpeg version N-x ..."; keep the version token only.
69 if fields := strings.Fields(line); len(fields) >= 3 && fields[1] == "version" {
70 return fields[2]
71 }
72 return line
73}
74