From f1edaca9f1d8223fb433e829d776210d85d3de53 Mon Sep 17 00:00:00 2001 From: "Mateusz (Mati) Kepa" Date: Fri, 15 May 2026 16:33:16 +0200 Subject: [PATCH] add support for Gradle prometheus metrics --- internal/handler/gradle.go | 48 +++++++++++++++++++--- internal/handler/gradle_test.go | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/internal/handler/gradle.go b/internal/handler/gradle.go index aed98e7..efc37a8 100644 --- a/internal/handler/gradle.go +++ b/internal/handler/gradle.go @@ -7,7 +7,9 @@ import ( "regexp" "strconv" "strings" + "time" + "github.com/git-pkgs/proxy/internal/metrics" "github.com/git-pkgs/proxy/internal/storage" ) @@ -34,33 +36,39 @@ func NewGradleBuildCacheHandler(proxy *Proxy) *GradleBuildCacheHandler { // Routes returns the HTTP handler for Gradle HttpBuildCache requests. func (h *GradleBuildCacheHandler) Routes() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + rw := &statusCapturingResponseWriter{ResponseWriter: w, status: http.StatusOK} + defer func() { + metrics.RecordRequest("gradle", rw.status, time.Since(start)) + }() + switch r.Method { case http.MethodGet, http.MethodHead, http.MethodPut: default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + http.Error(rw, "method not allowed", http.StatusMethodNotAllowed) return } key, statusCode := h.parseCacheKey(r.URL.Path) if statusCode != http.StatusOK { if statusCode == http.StatusNotFound { - http.NotFound(w, r) + http.NotFound(rw, r) return } - http.Error(w, "invalid cache key", statusCode) + http.Error(rw, "invalid cache key", statusCode) return } if r.Method == http.MethodPut { if h.proxy.GradleReadOnly { - http.Error(w, "gradle build cache is read-only", http.StatusMethodNotAllowed) + http.Error(rw, "gradle build cache is read-only", http.StatusMethodNotAllowed) return } - h.handlePut(w, r, key) + h.handlePut(rw, r, key) return } - h.handleGetOrHead(w, r, key) + h.handleGetOrHead(rw, r, key) }) } @@ -94,36 +102,51 @@ func (h *GradleBuildCacheHandler) handleGetOrHead(w http.ResponseWriter, r *http w.Header().Set("Content-Type", gradleBuildCacheContentType) if r.Method == http.MethodHead { + existsStart := time.Now() exists, err := h.proxy.Storage.Exists(r.Context(), storagePath) + metrics.RecordStorageOperation("read", time.Since(existsStart)) if err != nil { + metrics.RecordStorageError("read") h.proxy.Logger.Error("failed to check gradle build cache entry", "key", key, "error", err) http.Error(w, "failed to read cache entry", http.StatusInternalServerError) return } if !exists { + metrics.RecordCacheMiss("gradle") http.NotFound(w, r) return } + metrics.RecordCacheHit("gradle") + sizeStart := time.Now() if size, err := h.proxy.Storage.Size(r.Context(), storagePath); err == nil && size >= 0 { + metrics.RecordStorageOperation("read", time.Since(sizeStart)) w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + } else if err != nil { + metrics.RecordStorageOperation("read", time.Since(sizeStart)) + metrics.RecordStorageError("read") } w.WriteHeader(http.StatusOK) return } + readStart := time.Now() reader, err := h.proxy.Storage.Open(r.Context(), storagePath) + metrics.RecordStorageOperation("read", time.Since(readStart)) if err != nil { if errors.Is(err, storage.ErrNotFound) { + metrics.RecordCacheMiss("gradle") http.NotFound(w, r) return } + metrics.RecordStorageError("read") h.proxy.Logger.Error("failed to open gradle build cache entry", "key", key, "error", err) http.Error(w, "failed to read cache entry", http.StatusInternalServerError) return } defer func() { _ = reader.Close() }() + metrics.RecordCacheHit("gradle") w.WriteHeader(http.StatusOK) _, _ = io.Copy(w, reader) @@ -138,7 +161,9 @@ func (h *GradleBuildCacheHandler) handlePut(w http.ResponseWriter, r *http.Reque r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize) + storeStart := time.Now() _, hash, err := h.proxy.Storage.Store(r.Context(), storagePath, r.Body) + metrics.RecordStorageOperation("write", time.Since(storeStart)) if err != nil { var maxBytesErr *http.MaxBytesError if errors.As(err, &maxBytesErr) { @@ -146,6 +171,7 @@ func (h *GradleBuildCacheHandler) handlePut(w http.ResponseWriter, r *http.Reque return } + metrics.RecordStorageError("write") h.proxy.Logger.Error("failed to store gradle build cache entry", "key", key, "error", err) http.Error(w, "failed to write cache entry", http.StatusInternalServerError) return @@ -156,3 +182,13 @@ func (h *GradleBuildCacheHandler) handlePut(w http.ResponseWriter, r *http.Reque w.WriteHeader(http.StatusCreated) } + +type statusCapturingResponseWriter struct { + http.ResponseWriter + status int +} + +func (rw *statusCapturingResponseWriter) WriteHeader(code int) { + rw.status = code + rw.ResponseWriter.WriteHeader(code) +} diff --git a/internal/handler/gradle_test.go b/internal/handler/gradle_test.go index f002a51..b688803 100644 --- a/internal/handler/gradle_test.go +++ b/internal/handler/gradle_test.go @@ -4,8 +4,12 @@ import ( "io" "net/http" "net/http/httptest" + "strconv" "strings" "testing" + + "github.com/git-pkgs/proxy/internal/metrics" + "github.com/prometheus/client_golang/prometheus/testutil" ) func TestGradleBuildCacheHandler_PutGetHead(t *testing.T) { @@ -227,3 +231,71 @@ func TestGradleBuildCacheHandler_PutTooLarge(t *testing.T) { t.Fatalf("PUT status = %d, want %d", resp.StatusCode, http.StatusRequestEntityTooLarge) } } + +func TestGradleBuildCacheHandler_RecordsMetrics(t *testing.T) { + proxy, _, _, _ := setupTestProxy(t) + h := NewGradleBuildCacheHandler(proxy) + srv := httptest.NewServer(h.Routes()) + defer srv.Close() + + okBefore := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusOK))) + createdBefore := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusCreated))) + notFoundBefore := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusNotFound))) + hitsBefore := testutil.ToFloat64(metrics.CacheHits.WithLabelValues("gradle")) + missesBefore := testutil.ToFloat64(metrics.CacheMisses.WithLabelValues("gradle")) + + key := "metrics-key" + putReq, err := http.NewRequest(http.MethodPut, srv.URL+"/"+key, strings.NewReader("payload")) + if err != nil { + t.Fatalf("failed to create PUT request: %v", err) + } + putResp, err := http.DefaultClient.Do(putReq) + if err != nil { + t.Fatalf("PUT request failed: %v", err) + } + _ = putResp.Body.Close() + + getResp, err := http.Get(srv.URL + "/" + key) + if err != nil { + t.Fatalf("GET request failed: %v", err) + } + _ = getResp.Body.Close() + + headReq, err := http.NewRequest(http.MethodHead, srv.URL+"/"+key, nil) + if err != nil { + t.Fatalf("failed to create HEAD request: %v", err) + } + headResp, err := http.DefaultClient.Do(headReq) + if err != nil { + t.Fatalf("HEAD request failed: %v", err) + } + _ = headResp.Body.Close() + + missResp, err := http.Get(srv.URL + "/missing-key") + if err != nil { + t.Fatalf("GET miss request failed: %v", err) + } + _ = missResp.Body.Close() + + okAfter := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusOK))) + createdAfter := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusCreated))) + notFoundAfter := testutil.ToFloat64(metrics.RequestsTotal.WithLabelValues("gradle", strconv.Itoa(http.StatusNotFound))) + hitsAfter := testutil.ToFloat64(metrics.CacheHits.WithLabelValues("gradle")) + missesAfter := testutil.ToFloat64(metrics.CacheMisses.WithLabelValues("gradle")) + + if diff := createdAfter - createdBefore; diff != 1 { + t.Fatalf("created requests delta = %.0f, want 1", diff) + } + if diff := okAfter - okBefore; diff != 2 { + t.Fatalf("ok requests delta = %.0f, want 2", diff) + } + if diff := notFoundAfter - notFoundBefore; diff != 1 { + t.Fatalf("not found requests delta = %.0f, want 1", diff) + } + if diff := hitsAfter - hitsBefore; diff != 2 { + t.Fatalf("cache hits delta = %.0f, want 2", diff) + } + if diff := missesAfter - missesBefore; diff != 1 { + t.Fatalf("cache misses delta = %.0f, want 1", diff) + } +}