Merge pull request #240 from trails-cool/feat/overpass-metrics
Add Prometheus metrics + Grafana panels for Overpass proxy
This commit is contained in:
commit
97ad15db98
3 changed files with 208 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue