Files
cloudflare-prometheus-exporter/src/worker.tsx
2025-12-09 23:13:55 -05:00

129 lines
3.7 KiB
TypeScript

import { env } from "cloudflare:workers";
import { Hono } from "hono";
import { LandingPage } from "./components/LandingPage";
import { AccountMetricCoordinator } from "./durable-objects/AccountMetricCoordinator";
import { MetricCoordinator } from "./durable-objects/MetricCoordinator";
import { MetricExporter } from "./durable-objects/MetricExporter";
import { type AppConfig, parseConfig } from "./lib/config";
import { checkHealth, healthResponse } from "./lib/health";
import { configFromEnv, createLogger } from "./lib/logger";
import {
ConfigKeySchema,
getConfig,
getConfigKey,
getEnvDefaults,
resetAllConfig,
resetConfigKey,
setConfigKey,
} from "./lib/runtime-config";
export { MetricCoordinator, AccountMetricCoordinator, MetricExporter };
type Variables = { config: AppConfig };
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
// Parse config middleware
app.use("*", async (c, next) => {
c.set("config", parseConfig(c.env));
await next();
});
// Disable guards
app.use("*", async (c, next) => {
const path = c.req.path;
if (c.var.config.disableUi && path === "/") {
return c.text("Not Found", 404);
}
if (c.var.config.disableConfigApi && path.startsWith("/config")) {
return c.text("Not Found", 404);
}
await next();
});
// Dynamic metrics path middleware (runs before routing)
app.get(env.METRICS_PATH, async (c) => {
const logger = createLogger("worker", configFromEnv(c.env)).withContext({
request_id: crypto.randomUUID(),
});
logger.info("Metrics request received");
try {
const coordinator = await MetricCoordinator.get(c.env);
const output = await coordinator.export();
logger.info("Metrics exported successfully");
return c.text(output, 200, {
"Content-Type": "text/plain; charset=utf-8",
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error("Failed to collect metrics", { error: message });
return c.text(`Error collecting metrics: ${message}`, 500);
}
});
// Routes
app.get("/", (c) => c.html(<LandingPage config={c.var.config} />));
app.get("/health", async (c) => {
const health = await checkHealth(c.env);
return healthResponse(health);
});
// Config API routes
app.get("/config", async (c) => {
const config = await getConfig(c.env);
return c.json(config);
});
app.get("/config/defaults", (c) => {
const defaults = getEnvDefaults(c.env);
return c.json(defaults);
});
app.get("/config/:key", async (c) => {
const keyResult = ConfigKeySchema.safeParse(c.req.param("key"));
if (!keyResult.success) {
return c.json({ error: "Invalid config key" }, 400);
}
const value = await getConfigKey(c.env, keyResult.data);
return c.json({ key: keyResult.data, value });
});
app.put("/config/:key", async (c) => {
const keyResult = ConfigKeySchema.safeParse(c.req.param("key"));
if (!keyResult.success) {
return c.json({ error: "Invalid config key" }, 400);
}
const body = await c.req.json<{ value: unknown }>().catch(() => null);
if (!body || !("value" in body)) {
return c.json({ error: "Request body must contain 'value'" }, 400);
}
const result = await setConfigKey(c.env, keyResult.data, body.value);
if (!result.success) {
return c.json(
{ error: "Invalid value", details: result.error.issues },
400,
);
}
return c.json(result.config);
});
app.delete("/config/:key", async (c) => {
const keyResult = ConfigKeySchema.safeParse(c.req.param("key"));
if (!keyResult.success) {
return c.json({ error: "Invalid config key" }, 400);
}
const config = await resetConfigKey(c.env, keyResult.data);
return c.json(config);
});
app.delete("/config", async (c) => {
const config = await resetAllConfig(c.env);
return c.json(config);
});
app.notFound((c) => c.text("Not Found", 404));
export default app;