Files
DriverVault/API Server/internal/api/status.go
T
tajniak81andClaude Opus 5 3c4eba87c8 Status: latency the panel can actually show
PocketBase and the Web App share a Docker network and answer a health
probe in well under a millisecond. Milliseconds() truncated that to 0,
and omitempty on an int64 dropped the zero from the JSON, so the panel
saw no latencyMs at all and rendered an em dash.

Latency is now a *float64 rounded to one decimal, computed from
Microseconds(). The pointer keeps an absent measurement — the API
Server row, which probes nothing — distinct from a genuinely fast one.
pbProbe follows suit, since it copies straight off svcHealth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:06:01 +02:00

72 lines
2.3 KiB
Go

package api
import (
"context"
"io"
"math"
"net/http"
"sync"
"time"
)
// svcHealth is the health of one upstream service, as shown on the panel.
type svcHealth struct {
Status string `json:"status"` // "ok" | "down"
HTTPStatus int `json:"httpStatus,omitempty"`
URL string `json:"url,omitempty"`
// Fractional, and a pointer so an absent measurement (this process) stays
// distinguishable from a genuinely fast one: neighbours sharing a Docker
// network answer in well under a millisecond.
LatencyMs *float64 `json:"latencyMs,omitempty"`
Error string `json:"error,omitempty"`
}
// latencyMs is elapsed time in milliseconds, kept to one decimal.
func latencyMs(d time.Duration) *float64 {
ms := math.Round(float64(d.Microseconds())/100) / 10
return &ms
}
// healthClient is a short-timeout client for probing upstreams so a hung
// dependency can't stall the status endpoint.
var healthClient = &http.Client{Timeout: 4 * time.Second}
// probe does a GET against url and classifies the result.
func probe(ctx context.Context, url string) svcHealth {
start := time.Now()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return svcHealth{Status: "down", URL: url, Error: err.Error()}
}
resp, err := healthClient.Do(req)
lat := latencyMs(time.Since(start))
if err != nil {
return svcHealth{Status: "down", URL: url, LatencyMs: lat, Error: err.Error()}
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
status := "ok"
if resp.StatusCode >= 400 {
status = "down"
}
return svcHealth{Status: status, LatencyMs: lat, HTTPStatus: resp.StatusCode, URL: url}
}
// GET /api/status — aggregate health of the API Server and its neighbours
// (PocketBase and the Web App), probed server-side. The panel polls this so the
// browser never has to reach PocketBase or the Web App directly.
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
var pbHealth, web svcHealth
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); pbHealth = probe(r.Context(), s.pb.BaseURL()+"/api/health") }()
go func() { defer wg.Done(); web = probe(r.Context(), s.webAppURL()+"/healthz") }()
wg.Wait()
writeJSON(w, http.StatusOK, map[string]any{
"apiServer": map[string]any{"status": "ok"},
"pocketBase": pbHealth,
"webApp": web,
})
}