Compare commits
1 commit
main
...
feat/overp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a981a8383 |
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;
|
export const registry = client.register;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import type { Route } from "./+types/api.overpass";
|
import type { Route } from "./+types/api.overpass";
|
||||||
import { checkRateLimit } from "~/lib/rate-limit";
|
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 UPSTREAM_URL = process.env.OVERPASS_URL ?? "https://overpass.private.coffee/api/interpreter";
|
||||||
const USER_AGENT = "trails.cool Planner (https://trails.cool; legal@trails.cool)";
|
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.delete(oldest);
|
||||||
}
|
}
|
||||||
cache.set(key, { body, contentType, expiresAt: Date.now() + CACHE_TTL_MS });
|
cache.set(key, { body, contentType, expiresAt: Date.now() + CACHE_TTL_MS });
|
||||||
|
overpassCacheSize.set(cache.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action({ request }: Route.ActionArgs) {
|
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
|
// Cache hit: skip rate limit and upstream entirely
|
||||||
const cached = getFromCache(cacheKey);
|
const cached = getFromCache(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
overpassCacheEvents.inc({ result: "hit" });
|
||||||
return new Response(cached.body, {
|
return new Response(cached.body, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": cached.contentType, "X-Cache": "hit" },
|
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.
|
// same session don't each hit upstream for the identical bbox.
|
||||||
let pending = inFlight.get(cacheKey);
|
let pending = inFlight.get(cacheKey);
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
|
overpassCacheEvents.inc({ result: "miss" });
|
||||||
pending = (async () => {
|
pending = (async () => {
|
||||||
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
const upstream = await fetch(UPSTREAM_URL, {
|
const upstream = await fetch(UPSTREAM_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -85,6 +95,8 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
},
|
},
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
|
overpassUpstreamDuration.observe((Date.now() - start) / 1000);
|
||||||
|
overpassUpstreamRequests.inc({ status: String(upstream.status) });
|
||||||
const responseBody = await upstream.text();
|
const responseBody = await upstream.text();
|
||||||
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
||||||
const cacheable = upstream.status === 200 && !responseBody.includes("rate_limited");
|
const cacheable = upstream.status === 200 && !responseBody.includes("rate_limited");
|
||||||
|
|
@ -94,12 +106,15 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
inFlight.set(cacheKey, pending);
|
inFlight.set(cacheKey, pending);
|
||||||
|
} else {
|
||||||
|
overpassCacheEvents.inc({ result: "coalesced" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await pending;
|
result = await pending;
|
||||||
} catch {
|
} catch {
|
||||||
|
overpassUpstreamRequests.inc({ status: "error" });
|
||||||
return new Response("Upstream Overpass unavailable", { status: 502 });
|
return new Response("Upstream Overpass unavailable", { status: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,168 @@
|
||||||
"legendFormat": "{{route}}"
|
"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