Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/metrics/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
32 changes: 27 additions & 5 deletions app/metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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")
Expand Down
6 changes: 4 additions & 2 deletions app/metrics/plugins/example/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
package example

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"

Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions app/metrics/plugins/example/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
8 changes: 7 additions & 1 deletion docs/metrics-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand Down Expand Up @@ -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)
}
Expand Down
Loading