diff options
| author | Adam Soltys <[email protected]> | 2026-10-08 12:03:38 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-08 12:03:38 -0700 |
| commit | 621cb7b6ae6655db60971f248d2e799dbb8c4b3d (patch) | |
| tree | aea3cd870ac6db68e5203598c8ea71b7da780868 | |
| parent | b3d6c983719560b6d3e2eb17b413f1147f2522a1 (diff) | |
| download | coinos-server-621cb7b6ae6655db60971f248d2e799dbb8c4b3d.tar.gz coinos-server-621cb7b6ae6655db60971f248d2e799dbb8c4b3d.zip | |
analytics-geo: read rotated request log chunks
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | scripts/analytics-geo.ts | 32 |
1 files changed, 26 insertions, 6 deletions
diff --git a/scripts/analytics-geo.ts b/scripts/analytics-geo.ts index 7a09fe8d..ef088b6b 100644 --- a/scripts/analytics-geo.ts +++ b/scripts/analytics-geo.ts @@ -4,10 +4,11 @@ // (not raw request volume, which is dominated by datacenter/bot traffic). // // Usage: bun scripts/analytics-geo.ts [--days N] [--top N] -// Streams ~/coinos-server/req (pino JSONL, multi-GB) — never loads it whole. +// Streams the request log (pino JSONL, multi-GB) — never loads it whole. import { createInterface } from "readline"; -import { createReadStream } from "fs"; +import { createReadStream, readdirSync } from "fs"; +import { createGunzip } from "zlib"; const argDays = Number(process.argv[process.argv.indexOf("--days") + 1]) || 30; const TOP = Number(process.argv[process.argv.indexOf("--top") + 1]) || 25; @@ -18,10 +19,29 @@ const cutoff = Date.now() - argDays * 24 * 60 * 60 * 1000; // for sequential usernames (bugtraq, bugtraqq, ...) in the last 30d. const BOT_IPS = new Set<string>(["195.123.220.232"]); -const rl = createInterface({ - input: createReadStream("/home/adam/coinos-server/req"), - crlfDelay: Infinity, -}); + +// The live log rotates into 50M dated chunks under reqlogs/ (named for when +// they were rotated out, gzipped except the newest), so stream every chunk +// rotated within the window, oldest first, then the live file. +const REQ = "/home/adam/coinos-server/req"; +const REQLOGS = "/home/adam/coinos-server/reqlogs"; +const chunkTime = (f: string) => { + const m = f.match(/^req-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})?/); + return m ? new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +(m[6] || 0)).getTime() : 0; +}; +async function* reqLines(since: number) { + const files = readdirSync(REQLOGS) + .filter((f) => chunkTime(f) >= since) + .sort((a, b) => chunkTime(a) - chunkTime(b) || a.localeCompare(b)) + .map((f) => `${REQLOGS}/${f}`); + for (const path of [...files, REQ]) { + const raw = createReadStream(path); + const input = path.endsWith(".gz") ? raw.pipe(createGunzip()) : raw; + yield* createInterface({ input, crlfDelay: Infinity }); + } +} + +const rl = reqLines(cutoff); // We only trust /login and /register events for geography: they are real human // auth actions tied to a username in the body, from the user's actual device. |
