refactor: replace metrics with console.log, remove HMAC

Statuspage doesn't support HMAC signatures - removed all HMAC code.
Replaced KV-based metrics with simple console.log for CF Workers logs.
Queue consumer logs batch summary (sent/failed/retried/removed).
This commit is contained in:
2026-04-09 00:52:41 +07:00
parent d78e761731
commit 392fecf350
8 changed files with 3 additions and 127 deletions

View File

@@ -34,7 +34,6 @@ Cloudflare Workers with three entry points exported from `src/index.js`:
| GET | `/` | inline | Health check | | GET | `/` | inline | Health check |
| POST | `/webhook/telegram` | `bot-commands.js` | grammY `webhookCallback("cloudflare-mod")` | | POST | `/webhook/telegram` | `bot-commands.js` | grammY `webhookCallback("cloudflare-mod")` |
| POST | `/webhook/status/:secret` | `statuspage-webhook.js` | Receives Statuspage webhooks (URL secret) | | POST | `/webhook/status/:secret` | `statuspage-webhook.js` | Receives Statuspage webhooks (URL secret) |
| GET | `/metrics/:secret` | inline | Bot statistics (text or `?format=json`) |
| GET | `/migrate/:secret` | inline | One-time KV migration (remove after use) | | GET | `/migrate/:secret` | inline | One-time KV migration (remove after use) |
### Data Flow ### Data Flow
@@ -52,7 +51,6 @@ Per-subscriber keys (no read-modify-write races):
Special keys: Special keys:
- `last-status` — JSON snapshot of component statuses for cron comparison - `last-status` — JSON snapshot of component statuses for cron comparison
- `metrics` — Counters for webhooks, messages, cron checks, commands
`kv-store.js` handles key building/parsing with `kv.list({ prefix: "sub:" })` pagination. `threadId` can be `0` (General topic), so null checks use `!= null`. `kv-store.js` handles key building/parsing with `kv.list({ prefix: "sub:" })` pagination. `threadId` can be `0` (General topic), so null checks use `!= null`.

View File

@@ -13,7 +13,6 @@ Hosted on [Cloudflare Workers](https://workers.cloudflare.com/) with KV for stor
- **Supergroup topic support** — send `/start` in a specific topic and notifications go to that topic - **Supergroup topic support** — send `/start` in a specific topic and notifications go to that topic
- **On-demand status check** — `/status` fetches live data from status.claude.com - **On-demand status check** — `/status` fetches live data from status.claude.com
- **Automatic status monitoring** — cron checks every 5 minutes as a safety net - **Automatic status monitoring** — cron checks every 5 minutes as a safety net
- **Metrics dashboard** — track webhooks, messages, cron checks via `/metrics` endpoint
- **Self-healing** — automatically removes subscribers who block the bot - **Self-healing** — automatically removes subscribers who block the bot
## Bot Commands ## Bot Commands

View File

@@ -8,8 +8,6 @@ import {
} from "./kv-store.js"; } from "./kv-store.js";
import { fetchComponentByName, escapeHtml } from "./status-fetcher.js"; import { fetchComponentByName, escapeHtml } from "./status-fetcher.js";
import { registerInfoCommands } from "./bot-info-commands.js"; import { registerInfoCommands } from "./bot-info-commands.js";
import { trackMetrics } from "./metrics.js";
/** /**
* Extract chatId and threadId from grammY context * Extract chatId and threadId from grammY context
*/ */
@@ -27,12 +25,6 @@ export async function handleTelegramWebhook(c) {
const bot = new Bot(c.env.BOT_TOKEN); const bot = new Bot(c.env.BOT_TOKEN);
const kv = c.env.claude_status; const kv = c.env.claude_status;
// Track command usage
bot.use(async (ctx, next) => {
await trackMetrics(kv, { commandsProcessed: 1 });
await next();
});
bot.command("start", async (ctx) => { bot.command("start", async (ctx) => {
const { chatId, threadId } = getChatTarget(ctx); const { chatId, threadId } = getChatTarget(ctx);
await addSubscriber(kv, chatId, threadId); await addSubscriber(kv, chatId, threadId);

View File

@@ -1,7 +1,5 @@
import { fetchSummary, humanizeStatus, escapeHtml } from "./status-fetcher.js"; import { fetchSummary, humanizeStatus, escapeHtml } from "./status-fetcher.js";
import { getSubscribersByType } from "./kv-store.js"; import { getSubscribersByType } from "./kv-store.js";
import { trackMetrics } from "./metrics.js";
const LAST_STATUS_KEY = "last-status"; const LAST_STATUS_KEY = "last-status";
/** /**
@@ -59,11 +57,6 @@ export async function handleScheduled(env) {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
})); }));
await trackMetrics(kv, {
cronChecks: 1,
lastCronAt: new Date().toISOString(),
});
if (changes.length === 0) return; if (changes.length === 0) return;
console.log(`Cron: ${changes.length} component change(s) detected`); console.log(`Cron: ${changes.length} component change(s) detected`);
@@ -80,6 +73,4 @@ export async function handleScheduled(env) {
} }
console.log(`Cron: enqueued ${messages.length} messages for ${name} change`); console.log(`Cron: enqueued ${messages.length} messages for ${name} change`);
} }
await trackMetrics(kv, { cronChangesDetected: changes.length });
} }

View File

@@ -4,8 +4,6 @@ import { handleStatuspageWebhook } from "./statuspage-webhook.js";
import { handleQueue } from "./queue-consumer.js"; import { handleQueue } from "./queue-consumer.js";
import { handleScheduled } from "./cron-status-check.js"; import { handleScheduled } from "./cron-status-check.js";
import { migrateFromSingleKey } from "./kv-store.js"; import { migrateFromSingleKey } from "./kv-store.js";
import { getMetrics, formatMetricsText } from "./metrics.js";
const app = new Hono(); const app = new Hono();
/** /**
@@ -23,18 +21,6 @@ app.get("/", (c) => c.text("Claude Status Bot is running"));
app.post("/webhook/telegram", (c) => handleTelegramWebhook(c)); app.post("/webhook/telegram", (c) => handleTelegramWebhook(c));
app.post("/webhook/status/:secret", (c) => handleStatuspageWebhook(c)); app.post("/webhook/status/:secret", (c) => handleStatuspageWebhook(c));
// Metrics endpoint — view bot statistics
app.get("/metrics/:secret", async (c) => {
const secret = c.req.param("secret");
if (!await validateSecret(secret, c.env.WEBHOOK_SECRET)) {
return c.text("Unauthorized", 401);
}
const metrics = await getMetrics(c.env.claude_status);
const format = c.req.query("format");
if (format === "json") return c.json(metrics);
return c.text(formatMetricsText(metrics));
});
// One-time migration route — remove after migration is confirmed // One-time migration route — remove after migration is confirmed
app.get("/migrate/:secret", async (c) => { app.get("/migrate/:secret", async (c) => {
const secret = c.req.param("secret"); const secret = c.req.param("secret");

View File

@@ -1,77 +0,0 @@
const METRICS_KEY = "metrics";
const DEFAULT_METRICS = {
webhooksReceived: 0,
messagesEnqueued: 0,
messagesSent: 0,
messagesFailedPermanent: 0,
messagesRetried: 0,
subscribersRemoved: 0,
cronChecks: 0,
cronChangesDetected: 0,
commandsProcessed: 0,
lastWebhookAt: null,
lastCronAt: null,
startedAt: new Date().toISOString(),
};
/**
* Get current metrics from KV
*/
export async function getMetrics(kv) {
const data = await kv.get(METRICS_KEY, "json");
return data || { ...DEFAULT_METRICS };
}
/**
* Increment one or more metric counters and optionally set timestamp fields
*/
export async function trackMetrics(kv, updates) {
const metrics = await getMetrics(kv);
for (const [key, value] of Object.entries(updates)) {
if (typeof value === "number") {
metrics[key] = (metrics[key] || 0) + value;
} else {
metrics[key] = value;
}
}
await kv.put(METRICS_KEY, JSON.stringify(metrics));
}
/**
* Format metrics as HTML for Telegram or plain text for API
*/
export function formatMetricsText(metrics) {
const uptime = metrics.startedAt
? timeSince(new Date(metrics.startedAt))
: "unknown";
return [
`Webhooks received: ${metrics.webhooksReceived}`,
`Messages enqueued: ${metrics.messagesEnqueued}`,
`Messages sent: ${metrics.messagesSent}`,
`Messages failed: ${metrics.messagesFailedPermanent}`,
`Messages retried: ${metrics.messagesRetried}`,
`Subscribers auto-removed: ${metrics.subscribersRemoved}`,
`Cron checks: ${metrics.cronChecks}`,
`Cron changes detected: ${metrics.cronChangesDetected}`,
`Commands processed: ${metrics.commandsProcessed}`,
`Last webhook: ${metrics.lastWebhookAt || "never"}`,
`Last cron: ${metrics.lastCronAt || "never"}`,
`Tracking since: ${uptime}`,
].join("\n");
}
/**
* Human-readable time duration since a given date
*/
function timeSince(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ${minutes % 60}m ago`;
const days = Math.floor(hours / 24);
return `${days}d ${hours % 24}h ago`;
}

View File

@@ -1,7 +1,5 @@
import { removeSubscriber } from "./kv-store.js"; import { removeSubscriber } from "./kv-store.js";
import { telegramUrl } from "./telegram-api.js"; import { telegramUrl } from "./telegram-api.js";
import { trackMetrics } from "./metrics.js";
/** /**
* Process a batch of queued messages, sending each to Telegram. * Process a batch of queued messages, sending each to Telegram.
* Handles rate limits (429 → retry), blocked bots (403/400 → remove subscriber). * Handles rate limits (429 → retry), blocked bots (403/400 → remove subscriber).
@@ -59,10 +57,7 @@ export async function handleQueue(batch, env) {
} }
} }
await trackMetrics(env.claude_status, { if (sent || failed || retried || removed) {
messagesSent: sent, console.log(`Queue batch: sent=${sent} failed=${failed} retried=${retried} removed=${removed}`);
messagesFailedPermanent: failed, }
messagesRetried: retried,
subscribersRemoved: removed,
});
} }

View File

@@ -1,7 +1,5 @@
import { getSubscribersByType } from "./kv-store.js"; import { getSubscribersByType } from "./kv-store.js";
import { humanizeStatus, escapeHtml } from "./status-fetcher.js"; import { humanizeStatus, escapeHtml } from "./status-fetcher.js";
import { trackMetrics } from "./metrics.js";
/** /**
* Timing-safe string comparison * Timing-safe string comparison
*/ */
@@ -93,11 +91,5 @@ export async function handleStatuspageWebhook(c) {
console.log(`Enqueued ${messages.length} messages for ${category}${componentName ? `:${componentName}` : ""}`); console.log(`Enqueued ${messages.length} messages for ${category}${componentName ? `:${componentName}` : ""}`);
await trackMetrics(c.env.claude_status, {
webhooksReceived: 1,
messagesEnqueued: messages.length,
lastWebhookAt: new Date().toISOString(),
});
return c.text("OK", 200); return c.text("OK", 200);
} }