- New Go backend binary (backend + runner) replacing old scraper/ - Rename SCRAPER_API_URL → BACKEND_API_URL in UI env and docker-compose - Rename scraperFetch → backendFetch across all 19 UI server files - Remove SCRAPER_PROXY env var and proxy transport from browser.Config - Add Meilisearch, Valkey, Caddy to docker-compose - Add docs/: api-endpoints.md, request-flow.mermaid.md, data-flow.mermaid.md
90 lines
2.4 KiB
Go
90 lines
2.4 KiB
Go
// healthcheck is a static binary used by Docker HEALTHCHECK CMD in distroless
|
|
// images (which have no shell, wget, or curl).
|
|
//
|
|
// Two modes:
|
|
//
|
|
// 1. HTTP mode (default):
|
|
// /healthcheck <url>
|
|
// Performs GET <url>; exits 0 if HTTP 2xx/3xx, 1 otherwise.
|
|
// Example: /healthcheck http://localhost:8080/health
|
|
//
|
|
// 2. File-liveness mode:
|
|
// /healthcheck file <path> <max_age_seconds>
|
|
// Reads <path>, parses its content as RFC3339 timestamp, and exits 1 if the
|
|
// timestamp is older than <max_age_seconds>. Used by the runner service which
|
|
// writes /tmp/runner.alive on every successful poll.
|
|
// Example: /healthcheck file /tmp/runner.alive 120
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) > 1 && os.Args[1] == "file" {
|
|
checkFile()
|
|
return
|
|
}
|
|
checkHTTP()
|
|
}
|
|
|
|
// checkHTTP performs a GET request and exits 0 on success, 1 on failure.
|
|
func checkHTTP() {
|
|
url := "http://localhost:8080/health"
|
|
if len(os.Args) > 1 {
|
|
url = os.Args[1]
|
|
}
|
|
resp, err := http.Get(url) //nolint:gosec,noctx
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "healthcheck: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode >= 400 {
|
|
fmt.Fprintf(os.Stderr, "healthcheck: status %d\n", resp.StatusCode)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// checkFile reads a timestamp from a file and exits 1 if it is older than the
|
|
// given max age. Usage: /healthcheck file <path> <max_age_seconds>
|
|
func checkFile() {
|
|
if len(os.Args) < 4 {
|
|
fmt.Fprintln(os.Stderr, "healthcheck file: usage: /healthcheck file <path> <max_age_seconds>")
|
|
os.Exit(1)
|
|
}
|
|
path := os.Args[2]
|
|
maxAgeSec, err := strconv.ParseInt(os.Args[3], 10, 64)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "healthcheck file: invalid max_age_seconds %q: %v\n", os.Args[3], err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "healthcheck file: cannot read %s: %v\n", path, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
ts, err := time.Parse(time.RFC3339, string(data))
|
|
if err != nil {
|
|
// Fallback: use file mtime if content is not a valid timestamp.
|
|
info, statErr := os.Stat(path)
|
|
if statErr != nil {
|
|
fmt.Fprintf(os.Stderr, "healthcheck file: cannot stat %s: %v\n", path, statErr)
|
|
os.Exit(1)
|
|
}
|
|
ts = info.ModTime()
|
|
}
|
|
|
|
age := time.Since(ts)
|
|
if age > time.Duration(maxAgeSec)*time.Second {
|
|
fmt.Fprintf(os.Stderr, "healthcheck file: %s is %.0fs old (max %ds)\n", path, age.Seconds(), maxAgeSec)
|
|
os.Exit(1)
|
|
}
|
|
}
|