token-monitor/logger.js
Hannibal Smith 34898b1196
Phase 2: analysis layer (analyze.js), cache guard, log hygiene
- analyze.js: burn rate, weekly reconstruction, cycle stagger, rotation
  rank, underspend alerts, log prune with weekly archive
- logger.js: getCachedRun(maxAgeMinutes) — skip probing if recent data exists
- monitor.js: cache guard at wake — 20-min dedup, zero extra API calls
- test.js: fix type assertion for gemini-api/xai-api providers (+5 passing);
  add 14 new tests for cache guard and analyze.js (162 total, all green)
- docs/analyze.md: usage reference

Co-authored-by: Hannibal Smith <hannibal@trentuna.com>
2026-04-05 04:49:05 +00:00

47 lines
1.7 KiB
JavaScript

/**
* logger.js — persistent JSONL log to ~/.logs/token-monitor/YYYY-MM-DD.jsonl
*/
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
export function logRun(data) {
const dir = join(homedir(), '.logs', 'token-monitor');
mkdirSync(dir, { recursive: true });
const file = join(dir, `${new Date().toISOString().slice(0, 10)}.jsonl`);
appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...data }) + '\n');
}
export function getLogPath() {
const today = new Date().toISOString().slice(0, 10);
return join(homedir(), '.logs', 'token-monitor', `${today}.jsonl`);
}
/**
* Returns the last logged run if it was within maxAgeMinutes, otherwise null.
* Skips test/empty entries (entries where providers has no typed providers).
*/
export function getCachedRun(maxAgeMinutes = 20) {
const dir = join(homedir(), '.logs', 'token-monitor');
const today = new Date().toISOString().slice(0, 10);
const file = join(dir, `${today}.jsonl`);
if (!existsSync(file)) return null;
const lines = readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean);
for (let i = lines.length - 1; i >= 0; i--) {
try {
const entry = JSON.parse(lines[i]);
const providers = entry.providers || {};
// Skip test/empty entries — real entries have at least one provider with a type
const hasRealData = Object.values(providers).some(p => p && p.type);
if (!hasRealData) continue;
const ageMinutes = (Date.now() - new Date(entry.ts).getTime()) / 60000;
if (ageMinutes <= maxAgeMinutes) return entry;
return null; // last real entry is too old
} catch { continue; }
}
return null;
}