From 62e208d850f85903e1ab6d60d80b3ab1df4d3d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sat, 18 Apr 2026 02:11:54 +0200 Subject: [PATCH] Add Prometheus metrics + Grafana panels for Overpass proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four metrics surfaced via the existing /metrics endpoint: - overpass_cache_events_total{result=hit|miss|coalesced} - overpass_cache_size (gauge) - overpass_upstream_duration_seconds (histogram) - overpass_upstream_requests_total{status} — covers 2xx/4xx/5xx and an explicit "error" label for network-level failures Grafana dashboards/planner.json gets a new row: - Upstream health (5m 2xx success ratio, red <90%, green >99%) - Cache hit ratio - Cache size - Upstream p95 latency Plus timeseries panels for cache event breakdown, upstream status over time, and p50/p95/p99 upstream latency. The success-rate formula uses clamp_min(..., 1) on the denominator so it doesn't NaN when traffic is zero. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/planner/app/lib/metrics.server.ts | 31 ++++ apps/planner/app/routes/api.overpass.ts | 15 ++ .../grafana/dashboards/planner.json | 162 ++++++++++++++++++ 3 files changed, 208 insertions(+) diff --git a/apps/planner/app/lib/metrics.server.ts b/apps/planner/app/lib/metrics.server.ts index 46d2060..b89f8c2 100644 --- a/apps/planner/app/lib/metrics.server.ts +++ b/apps/planner/app/lib/metrics.server.ts @@ -41,4 +41,35 @@ export const brouterRequestDuration = getOrCreate("brouter_request_duration_seco }), ); +export const overpassCacheEvents = getOrCreate("overpass_cache_events_total", () => + new client.Counter({ + name: "overpass_cache_events_total", + help: "Overpass proxy cache events", + labelNames: ["result"] as const, // hit | miss | coalesced + }), +); + +export const overpassCacheSize = getOrCreate("overpass_cache_size", () => + new client.Gauge({ + name: "overpass_cache_size", + help: "Current number of entries in the Overpass proxy cache", + }), +); + +export const overpassUpstreamDuration = getOrCreate("overpass_upstream_duration_seconds", () => + new client.Histogram({ + name: "overpass_upstream_duration_seconds", + help: "Duration of upstream Overpass API requests in seconds", + buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], + }), +); + +export const overpassUpstreamRequests = getOrCreate("overpass_upstream_requests_total", () => + new client.Counter({ + name: "overpass_upstream_requests_total", + help: "Upstream Overpass API requests by status", + labelNames: ["status"] as const, + }), +); + export const registry = client.register; diff --git a/apps/planner/app/routes/api.overpass.ts b/apps/planner/app/routes/api.overpass.ts index e1b90ea..e97b7c3 100644 --- a/apps/planner/app/routes/api.overpass.ts +++ b/apps/planner/app/routes/api.overpass.ts @@ -1,5 +1,11 @@ import type { Route } from "./+types/api.overpass"; import { checkRateLimit } from "~/lib/rate-limit"; +import { + overpassCacheEvents, + overpassCacheSize, + overpassUpstreamDuration, + overpassUpstreamRequests, +} from "~/lib/metrics.server"; const UPSTREAM_URL = process.env.OVERPASS_URL ?? "https://overpass.private.coffee/api/interpreter"; const USER_AGENT = "trails.cool Planner (https://trails.cool; legal@trails.cool)"; @@ -36,6 +42,7 @@ function putInCache(key: string, body: string, contentType: string) { cache.delete(oldest); } cache.set(key, { body, contentType, expiresAt: Date.now() + CACHE_TTL_MS }); + overpassCacheSize.set(cache.size); } export async function action({ request }: Route.ActionArgs) { @@ -56,6 +63,7 @@ export async function action({ request }: Route.ActionArgs) { // Cache hit: skip rate limit and upstream entirely const cached = getFromCache(cacheKey); if (cached) { + overpassCacheEvents.inc({ result: "hit" }); return new Response(cached.body, { status: 200, headers: { "Content-Type": cached.contentType, "X-Cache": "hit" }, @@ -75,7 +83,9 @@ export async function action({ request }: Route.ActionArgs) { // same session don't each hit upstream for the identical bbox. let pending = inFlight.get(cacheKey); if (!pending) { + overpassCacheEvents.inc({ result: "miss" }); pending = (async () => { + const start = Date.now(); try { const upstream = await fetch(UPSTREAM_URL, { method: "POST", @@ -85,6 +95,8 @@ export async function action({ request }: Route.ActionArgs) { }, body, }); + overpassUpstreamDuration.observe((Date.now() - start) / 1000); + overpassUpstreamRequests.inc({ status: String(upstream.status) }); const responseBody = await upstream.text(); const contentType = upstream.headers.get("content-type") ?? "application/json"; const cacheable = upstream.status === 200 && !responseBody.includes("rate_limited"); @@ -94,12 +106,15 @@ export async function action({ request }: Route.ActionArgs) { } })(); inFlight.set(cacheKey, pending); + } else { + overpassCacheEvents.inc({ result: "coalesced" }); } let result; try { result = await pending; } catch { + overpassUpstreamRequests.inc({ status: "error" }); return new Response("Upstream Overpass unavailable", { status: 502 }); } diff --git a/infrastructure/grafana/dashboards/planner.json b/infrastructure/grafana/dashboards/planner.json index 72a9e02..084fa8e 100644 --- a/infrastructure/grafana/dashboards/planner.json +++ b/infrastructure/grafana/dashboards/planner.json @@ -97,6 +97,168 @@ "legendFormat": "{{route}}" } ] + }, + { + "title": "Overpass Upstream Health (5m success rate)", + "type": "stat", + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 14 + }, + "targets": [ + { + "expr": "sum(rate(overpass_upstream_requests_total{status=~\"2..\"}[5m])) / clamp_min(sum(rate(overpass_upstream_requests_total[5m])), 1)", + "legendFormat": "success" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "min": 0, + "max": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "orange", "value": 0.9 }, + { "color": "green", "value": 0.99 } + ] + } + } + } + }, + { + "title": "Overpass Cache Hit Ratio (5m)", + "type": "stat", + "gridPos": { + "h": 5, + "w": 6, + "x": 6, + "y": 14 + }, + "targets": [ + { + "expr": "sum(rate(overpass_cache_events_total{result=\"hit\"}[5m])) / clamp_min(sum(rate(overpass_cache_events_total[5m])), 1)", + "legendFormat": "hit ratio" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "min": 0, + "max": 1 + } + } + }, + { + "title": "Overpass Cache Size", + "type": "stat", + "gridPos": { + "h": 5, + "w": 6, + "x": 12, + "y": 14 + }, + "targets": [ + { + "expr": "overpass_cache_size", + "legendFormat": "entries" + } + ] + }, + { + "title": "Overpass Upstream p95", + "type": "stat", + "gridPos": { + "h": 5, + "w": 6, + "x": 18, + "y": 14 + }, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(overpass_upstream_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p95" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s" + } + } + }, + { + "title": "Overpass Cache Events", + "type": "timeseries", + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 19 + }, + "targets": [ + { + "expr": "sum by (result) (rate(overpass_cache_events_total[5m]))", + "legendFormat": "{{result}}" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops" + } + } + }, + { + "title": "Overpass Upstream Status", + "type": "timeseries", + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 19 + }, + "targets": [ + { + "expr": "sum by (status) (rate(overpass_upstream_requests_total[5m]))", + "legendFormat": "{{status}}" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops" + } + } + }, + { + "title": "Overpass Upstream Latency", + "type": "timeseries", + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 26 + }, + "targets": [ + { + "expr": "histogram_quantile(0.50, sum(rate(overpass_upstream_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p50" + }, + { + "expr": "histogram_quantile(0.95, sum(rate(overpass_upstream_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p95" + }, + { + "expr": "histogram_quantile(0.99, sum(rate(overpass_upstream_duration_seconds_bucket[5m])) by (le))", + "legendFormat": "p99" + } + ], + "fieldConfig": { + "defaults": { + "unit": "s" + } + } } ] }