diff --git a/app/metrics/builtin.go b/app/metrics/builtin.go index 74fe5243..a0c633c3 100644 --- a/app/metrics/builtin.go +++ b/app/metrics/builtin.go @@ -141,12 +141,18 @@ func RecordDuration(integration string, d time.Duration) { h.Observe(d.Seconds()) } +func writePromType(w http.ResponseWriter, name, metricType string) { + fmt.Fprintf(w, "# TYPE %s %s\n", name, metricType) +} + // WriteProm emits all Prometheus metrics to w in text format. func WriteProm(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/plain; version=0.0.4") + writePromType(w, "authtranslator_requests_total", "counter") requestCounts.Do(func(kv expvar.KeyValue) { fmt.Fprintf(w, "authtranslator_requests_total{integration=%q} %s\n", kv.Key, kv.Value.String()) }) + writePromType(w, "authtranslator_request_duration_seconds", "histogram") durationHistsMu.Lock() type histSnapshot struct { name string @@ -160,12 +166,15 @@ func WriteProm(w http.ResponseWriter) { for _, hs := range hists { hs.h.writeProm(w, hs.name) } + writePromType(w, "authtranslator_rate_limit_events_total", "counter") rateLimitCounts.Do(func(kv expvar.KeyValue) { fmt.Fprintf(w, "authtranslator_rate_limit_events_total{integration=%q} %s\n", kv.Key, kv.Value.String()) }) + writePromType(w, "authtranslator_auth_failures_total", "counter") authFailureCounts.Do(func(kv expvar.KeyValue) { fmt.Fprintf(w, "authtranslator_auth_failures_total{integration=%q} %s\n", kv.Key, kv.Value.String()) }) + writePromType(w, "authtranslator_internal_responses_total", "counter") internalResponseCounts.Do(func(kv expvar.KeyValue) { parts := strings.SplitN(kv.Key, metricKeySeparator, 3) if len(parts) != 3 { @@ -174,6 +183,7 @@ func WriteProm(w http.ResponseWriter) { integ, code, reason := parts[0], parts[1], parts[2] fmt.Fprintf(w, "authtranslator_internal_responses_total{integration=%q,code=%q,reason=%q} %s\n", integ, code, reason, kv.Value.String()) }) + writePromType(w, "authtranslator_upstream_responses_total", "counter") upstreamStatusCounts.Do(func(kv expvar.KeyValue) { parts := strings.SplitN(kv.Key, metricKeySeparator, 2) if len(parts) != 2 { diff --git a/app/metrics/metrics_test.go b/app/metrics/metrics_test.go index 70b9228e..a0f7ea6f 100644 --- a/app/metrics/metrics_test.go +++ b/app/metrics/metrics_test.go @@ -45,8 +45,22 @@ func TestMetricsHandlerEmpty(t *testing.T) { if ct := rr.Header().Get("Content-Type"); ct != "text/plain; version=0.0.4" { t.Fatalf("expected content type text/plain; version=0.0.4, got %s", ct) } - if body := rr.Body.String(); body != "" { - t.Fatalf("expected empty body, got %q", body) + + body := rr.Body.String() + for _, line := range []string{ + "# TYPE authtranslator_requests_total counter", + "# TYPE authtranslator_request_duration_seconds histogram", + "# TYPE authtranslator_rate_limit_events_total counter", + "# TYPE authtranslator_auth_failures_total counter", + "# TYPE authtranslator_internal_responses_total counter", + "# TYPE authtranslator_upstream_responses_total counter", + } { + if !strings.Contains(body, line) { + t.Fatalf("missing metric type line %q in %q", line, body) + } + } + if strings.Contains(body, `{integration="`) { + t.Fatalf("expected no metric samples, got %q", body) } } @@ -73,9 +87,17 @@ func TestMetricsHandlerOutput(t *testing.T) { } body := rr.Body.String() - lines := strings.Split(strings.TrimSpace(body), "\n") - if len(lines) < 27 { - t.Fatalf("expected at least 27 metrics lines, got %d", len(lines)) + for _, line := range []string{ + "# TYPE authtranslator_requests_total counter", + "# TYPE authtranslator_request_duration_seconds histogram", + "# TYPE authtranslator_rate_limit_events_total counter", + "# TYPE authtranslator_auth_failures_total counter", + "# TYPE authtranslator_internal_responses_total counter", + "# TYPE authtranslator_upstream_responses_total counter", + } { + if !strings.Contains(body, line) { + t.Fatalf("missing metric type line %q in %q", line, body) + } } if !strings.Contains(body, `authtranslator_requests_total{integration="foo"} 2`) { t.Fatal("missing foo request metric") diff --git a/app/metrics/plugins/example/plugin.go b/app/metrics/plugins/example/plugin.go index 0ec37bcf..c35562d3 100644 --- a/app/metrics/plugins/example/plugin.go +++ b/app/metrics/plugins/example/plugin.go @@ -3,10 +3,8 @@ package example import ( - "bytes" "encoding/json" "fmt" - "io" "net/http" "sync" @@ -47,6 +45,10 @@ func (t *tokenCounter) OnResponse(integ, caller string, r *http.Request, resp *h func (t *tokenCounter) WriteProm(w http.ResponseWriter) { t.mu.Lock() defer t.mu.Unlock() + if len(t.totals) == 0 { + return + } + fmt.Fprintln(w, "# TYPE authtranslator_tokens_total counter") for caller, total := range t.totals { fmt.Fprintf(w, "authtranslator_tokens_total{caller=%q} %d\n", caller, total) } diff --git a/app/metrics/plugins/example/plugin_test.go b/app/metrics/plugins/example/plugin_test.go index d879116e..aa48fd2e 100644 --- a/app/metrics/plugins/example/plugin_test.go +++ b/app/metrics/plugins/example/plugin_test.go @@ -32,6 +32,9 @@ func TestTokenCounter(t *testing.T) { rr := httptest.NewRecorder() metrics.Handler(rr, httptest.NewRequest(http.MethodGet, "/metrics", nil), "", "") + if !strings.Contains(rr.Body.String(), "# TYPE authtranslator_tokens_total counter") { + t.Fatalf("token metric type missing: %s", rr.Body.String()) + } if !strings.Contains(rr.Body.String(), `authtranslator_tokens_total{caller="caller"} 42`) { t.Fatalf("token metric missing: %s", rr.Body.String()) } diff --git a/docs/metrics-plugins.md b/docs/metrics-plugins.md index 57961710..c197294d 100644 --- a/docs/metrics-plugins.md +++ b/docs/metrics-plugins.md @@ -22,7 +22,9 @@ type Plugin interface { `OnRequest` fires just before the proxy forwards a request upstream and `OnResponse` runs once the upstream reply is received. `WriteProm` lets a plugin -append custom Prometheus metrics. The integration name is passed so you can +append custom Prometheus metrics. For custom families, emit the matching +Prometheus metadata lines such as `# TYPE ... counter` so functions like +`rate()` are recognized correctly. The integration name is passed so you can apply per-service logic. --- @@ -97,6 +99,10 @@ func (t *tokenCounter) OnResponse(integ, caller string, r *http.Request, resp *h func (t *tokenCounter) WriteProm(w http.ResponseWriter) { t.mu.Lock() defer t.mu.Unlock() + if len(t.totals) == 0 { + return + } + fmt.Fprintln(w, "# TYPE authtranslator_tokens_total counter") for caller, total := range t.totals { fmt.Fprintf(w, "authtranslator_tokens_total{caller=%q} %d\n", caller, total) }