diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 9c5fa21..0b4b8a1 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -8,15 +8,12 @@ import ( "net/http" "strconv" "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" ) // NewGoCertRouter takes in an environment struct, passes it along to any handlers that will need -// access to it, then builds and returns it for a server to consume -func NewGoCertRouter(env *Environment) http.Handler { +// access to it, and takes an http.Handler that will be used to handle metrics. +// then builds and returns it for a server to consume +func NewGoCertRouter(env *Environment, metricsHandler http.Handler) http.Handler { router := http.NewServeMux() router.HandleFunc("GET /certificate_requests", GetCertificateRequests(env)) router.HandleFunc("POST /certificate_requests", PostCertificateRequest(env)) @@ -26,15 +23,15 @@ func NewGoCertRouter(env *Environment) http.Handler { router.HandleFunc("POST /certificate_requests/{id}/certificate/reject", RejectCertificate(env)) router.HandleFunc("DELETE /certificate_requests/{id}/certificate", DeleteCertificate(env)) - v1 := http.NewServeMux() - v1.HandleFunc("GET /status", HealthCheck) - v1.Handle("/api/v1/", http.StripPrefix("/api/v1", router)) - reg := prometheus.NewRegistry() - reg.MustRegister(collectors.NewGoCollector()) - prometheusHandler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{}) - v1.Handle("/api/v1/metrics", prometheusHandler) + monitoringMux := http.NewServeMux() + monitoringMux.HandleFunc("/status", HealthCheck) + monitoringMux.Handle("/metrics", metricsHandler) + + api := http.NewServeMux() + api.Handle("/api/v1/", http.StripPrefix("/api/v1", router)) + api.Handle("/", monitoringMux) - return logging(v1) + return logging(api) } // the health check endpoint simply returns a http.StatusOK diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 2cd4816..4e3b320 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -10,6 +10,7 @@ import ( server "github.com/canonical/gocert/internal/api" "github.com/canonical/gocert/internal/certdb" + metrics "github.com/canonical/gocert/internal/metrics" ) const ( @@ -107,7 +108,8 @@ func TestGoCertRouter(t *testing.T) { } env := &server.Environment{} env.DB = testdb - ts := httptest.NewTLSServer(server.NewGoCertRouter(env)) + metricsHandler := metrics.NewPrometheusHandler() + ts := httptest.NewTLSServer(server.NewGoCertRouter(env, metricsHandler)) defer ts.Close() client := ts.Client() @@ -323,7 +325,7 @@ func TestGoCertRouter(t *testing.T) { { desc: "metrics endpoint success", method: "GET", - path: "/api/v1/metrics", + path: "/metrics", data: "", response: "go_goroutines", status: http.StatusOK, @@ -345,7 +347,7 @@ func TestGoCertRouter(t *testing.T) { t.Fatal(err) } switch path := tC.path; path { - case "/api/v1/metrics": + case "/metrics": if res.StatusCode != tC.status || !strings.Contains(string(resBody), tC.response) { t.Errorf("expected response did not match.\nExpected vs Received status code: %d vs %d\nExpected vs Received body: \n%s\nvs\n%s\n", tC.status, res.StatusCode, tC.response, string(resBody)) } diff --git a/internal/api/server.go b/internal/api/server.go index dc3a051..dc77c0d 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -11,6 +11,7 @@ import ( "time" "github.com/canonical/gocert/internal/certdb" + metrics "github.com/canonical/gocert/internal/metrics" "gopkg.in/yaml.v3" ) @@ -85,7 +86,8 @@ func NewServer(configFile string) (*http.Server, error) { env := &Environment{} env.DB = db - router := NewGoCertRouter(env) + metricsHandler := metrics.NewPrometheusHandler() + router := NewGoCertRouter(env, metricsHandler) s := &http.Server{ Addr: fmt.Sprintf(":%d", config.Port), diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..416e19d --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,32 @@ +package metrics + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type MetricsHandler interface { + http.Handler +} + +// PrometheusHandler implements the MetricsHandler interface. +type PrometheusHandler struct { + registry *prometheus.Registry +} + +// Returns a new PrometheusHandler. +func NewPrometheusHandler() MetricsHandler { + registry := prometheus.NewRegistry() + registry.MustRegister(collectors.NewGoCollector()) + return &PrometheusHandler{ + registry: registry, + } +} + +// ServeHTTP implements the http.Handler interface, allowing the PrometheusHandler to handle HTTP requests. +func (p *PrometheusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + promhttp.HandlerFor(p.registry, promhttp.HandlerOpts{}).ServeHTTP(w, r) +} diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go new file mode 100644 index 0000000..2a1bdfb --- /dev/null +++ b/internal/metrics/metrics_test.go @@ -0,0 +1,33 @@ +package metrics_test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + metrics "github.com/canonical/gocert/internal/metrics" +) + +// TestPrometheusHandler tests that the Prometheus metrics handler responds correctly to an HTTP request. +func TestPrometheusHandler(t *testing.T) { + handler := metrics.NewPrometheusHandler() + + request, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatalf("could not create request: %v", err) + } + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + + if status := recorder.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + if recorder.Body.String() == "" { + t.Errorf("handler returned an empty body") + } + if !strings.Contains(recorder.Body.String(), "go_goroutines") { + t.Errorf("handler returned an empty body") + } +}