feat(metrics): wire instrumentation through queue, handlers, middleware
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/shcizo/package-updater/internal/discovery"
|
||||
"github.com/shcizo/package-updater/internal/logging"
|
||||
"github.com/shcizo/package-updater/internal/metrics"
|
||||
"github.com/shcizo/package-updater/internal/updater"
|
||||
)
|
||||
|
||||
@@ -34,11 +35,13 @@ type Handlers struct {
|
||||
version string
|
||||
commit string
|
||||
buildTime string
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// NewHandlers constructs a Handlers value with all dependencies injected.
|
||||
func NewHandlers(f Finder, s Submitter, p Pinger, version, commit, buildTime string) *Handlers {
|
||||
return &Handlers{finder: f, submitter: s, pinger: p, version: version, commit: commit, buildTime: buildTime}
|
||||
// m may be nil, in which case metrics recording is silently skipped.
|
||||
func NewHandlers(f Finder, s Submitter, p Pinger, version, commit, buildTime string, m *metrics.Metrics) *Handlers {
|
||||
return &Handlers{finder: f, submitter: s, pinger: p, version: version, commit: commit, buildTime: buildTime, metrics: m}
|
||||
}
|
||||
|
||||
// Update implements POST /update.
|
||||
@@ -107,9 +110,15 @@ func (h *Handlers) Update(w http.ResponseWriter, r *http.Request) {
|
||||
// Healthz implements GET /healthz.
|
||||
func (h *Handlers) Healthz(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := h.pinger.Ping(r.Context()); err != nil {
|
||||
if h.metrics != nil {
|
||||
h.metrics.DockerPingUp.Set(0)
|
||||
}
|
||||
writeJSON(w, http.StatusServiceUnavailable, HealthResponse{Status: "unhealthy", Docker: "unreachable"})
|
||||
return
|
||||
}
|
||||
if h.metrics != nil {
|
||||
h.metrics.DockerPingUp.Set(1)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, HealthResponse{Status: "ok", Docker: "ok"})
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func decode[T any](t *testing.T, body io.Reader) T {
|
||||
}
|
||||
|
||||
func TestUpdate_ValidationError(t *testing.T) {
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/update",
|
||||
strings.NewReader(`{"tag":"v1.2.3"}`))
|
||||
w := httptest.NewRecorder()
|
||||
@@ -65,7 +65,7 @@ func TestUpdate_ValidationError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate_BadJSON(t *testing.T) {
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/update", strings.NewReader(`not json`))
|
||||
w := httptest.NewRecorder()
|
||||
h.Update(w, req)
|
||||
@@ -74,7 +74,7 @@ func TestUpdate_BadJSON(t *testing.T) {
|
||||
|
||||
func TestUpdate_DiscoveryFailureReturns500(t *testing.T) {
|
||||
finder := &fakeFinder{err: errors.New("daemon unreachable")}
|
||||
h := api.NewHandlers(finder, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(finder, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
body, _ := json.Marshal(api.UpdateRequest{Image: "r/x"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/update", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
@@ -83,7 +83,7 @@ func TestUpdate_DiscoveryFailureReturns500(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate_ZeroMatchesReturns200(t *testing.T) {
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
body, _ := json.Marshal(api.UpdateRequest{Image: "r/x"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/update", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
@@ -97,7 +97,7 @@ func TestUpdate_AllSucceeded200(t *testing.T) {
|
||||
finder := &fakeFinder{jobs: []discovery.Job{
|
||||
{Project: "p", Service: "s", WorkingDir: "/x", ConfigFiles: []string{"/x/c.yml"}},
|
||||
}}
|
||||
h := api.NewHandlers(finder, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(finder, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
body, _ := json.Marshal(api.UpdateRequest{Image: "r/x", Tag: "v1"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/update", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
@@ -119,7 +119,7 @@ func TestUpdate_MixedReturns207(t *testing.T) {
|
||||
{Job: jobs[0], Status: updater.StatusUpdated},
|
||||
{Job: jobs[1], Status: updater.StatusFailed, Error: "boom"},
|
||||
}}
|
||||
h := api.NewHandlers(finder, submitter, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(finder, submitter, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
body, _ := json.Marshal(api.UpdateRequest{Image: "r/x"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/update", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
@@ -133,7 +133,7 @@ func TestUpdate_AllFailedReturns500(t *testing.T) {
|
||||
submitter := &fakeSubmitter{results: []updater.Result{
|
||||
{Job: jobs[0], Status: updater.StatusFailed, Error: "boom"},
|
||||
}}
|
||||
h := api.NewHandlers(finder, submitter, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(finder, submitter, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
body, _ := json.Marshal(api.UpdateRequest{Image: "r/x"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/update", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
@@ -142,7 +142,7 @@ func TestUpdate_AllFailedReturns500(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHealthz_OKWhenDockerUp(t *testing.T) {
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v0.0.0", "abc", "now", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.Healthz(w, req)
|
||||
@@ -150,7 +150,7 @@ func TestHealthz_OKWhenDockerUp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHealthz_503WhenDockerDown(t *testing.T) {
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{err: errors.New("ping fail")}, "v0.0.0", "abc", "now")
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{err: errors.New("ping fail")}, "v0.0.0", "abc", "now", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.Healthz(w, req)
|
||||
@@ -158,7 +158,7 @@ func TestHealthz_503WhenDockerDown(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v1.2.3", "abcdef", "2026-05-22T00:00:00Z")
|
||||
h := api.NewHandlers(&fakeFinder{}, &fakeSubmitter{}, &fakePinger{}, "v1.2.3", "abcdef", "2026-05-22T00:00:00Z", nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/version", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.Version(w, req)
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shcizo/package-updater/internal/logging"
|
||||
"github.com/shcizo/package-updater/internal/metrics"
|
||||
)
|
||||
|
||||
// Auth returns middleware that requires a matching bearer token.
|
||||
@@ -66,8 +68,9 @@ func RequestID(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// RequestLogger emits a structured access-log line per request.
|
||||
func RequestLogger(base *slog.Logger) func(http.Handler) http.Handler {
|
||||
// RequestLogger emits a structured access-log line per request. m may be nil,
|
||||
// in which case metrics recording is silently skipped.
|
||||
func RequestLogger(base *slog.Logger, m *metrics.Metrics) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
@@ -81,6 +84,9 @@ func RequestLogger(base *slog.Logger) func(http.Handler) http.Handler {
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
"client_ip", clientIP(r),
|
||||
)
|
||||
if m != nil {
|
||||
m.HTTPRequests.WithLabelValues(r.URL.Path, strconv.Itoa(sw.status)).Inc()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestRequestID_UsesIncoming(t *testing.T) {
|
||||
func TestRequestLogger_LogsAndDelegates(t *testing.T) {
|
||||
logger := newTestLogger()
|
||||
called := false
|
||||
h := api.RequestLogger(logger)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
h := api.RequestLogger(logger, nil)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user