(days: number = 1)
| 300 | * Reads the last N days of log files and shows each request individually. |
| 301 | */ |
| 302 | export async function formatRecentLogs(days: number = 1): Promise<string> { |
| 303 | const logFiles = await getLogFiles(); |
| 304 | const filesToRead = logFiles.slice(0, days); |
| 305 | |
| 306 | const allEntries: UsageEntry[] = []; |
| 307 | for (const file of filesToRead) { |
| 308 | const entries = await parseLogFile(join(LOG_DIR, file)); |
| 309 | allEntries.push(...entries); |
| 310 | } |
| 311 | |
| 312 | // Sort chronologically (oldest first) |
| 313 | allEntries.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); |
| 314 | |
| 315 | const lines: string[] = []; |
| 316 | lines.push("╔════════════════════════════════════════════════════════════════════════╗"); |
| 317 | lines.push( |
| 318 | `║ ClawRouter Request Log — last ${days === 1 ? "24h" : `${days} days`}`.padEnd(72) + "║", |
| 319 | ); |
| 320 | lines.push("╠══════════════════╦══════════════════════════╦═════════╦══════╦════════╣"); |
| 321 | lines.push("║ Time ║ Model ║ Cost ║ ms ║ Status ║"); |
| 322 | lines.push("╠══════════════════╬══════════════════════════╬═════════╬══════╬════════╣"); |
| 323 | |
| 324 | if (allEntries.length === 0) { |
| 325 | lines.push("║ No requests found".padEnd(72) + "║"); |
| 326 | } |
| 327 | |
| 328 | let totalCost = 0; |
| 329 | for (const e of allEntries) { |
| 330 | const time = e.timestamp.slice(11, 19); // HH:MM:SS |
| 331 | const date = e.timestamp.slice(5, 10); // MM-DD |
| 332 | const displayTime = `${date} ${time}`; |
| 333 | const model = e.model.length > 24 ? e.model.slice(0, 21) + "..." : e.model; |
| 334 | const cost = `$${e.cost.toFixed(4)}`; |
| 335 | const ms = e.latencyMs > 9999 ? `${(e.latencyMs / 1000).toFixed(1)}s` : `${e.latencyMs}ms`; |
| 336 | const status = |
| 337 | (e as UsageEntry & { status?: string }).status === "error" ? " ERROR " : " OK "; |
| 338 | totalCost += e.cost; |
| 339 | lines.push( |
| 340 | `║ ${displayTime.padEnd(16)}║ ${model.padEnd(24)}║ ${cost.padStart(7)}║ ${ms.padStart(4)}║${status}║`, |
| 341 | ); |
| 342 | } |
| 343 | |
| 344 | lines.push("╠══════════════════╩══════════════════════════╩═════════╩══════╩════════╣"); |
| 345 | lines.push( |
| 346 | `║ ${allEntries.length} request${allEntries.length !== 1 ? "s" : ""} Total spent: $${totalCost.toFixed(4)}`.padEnd( |
| 347 | 72, |
| 348 | ) + "║", |
| 349 | ); |
| 350 | lines.push( |
| 351 | "║ Logs: ~/.openclaw/blockrun/logs/ (JSONL — one entry per request)".padEnd(72) + "║", |
| 352 | ); |
| 353 | lines.push("╚════════════════════════════════════════════════════════════════════════╝"); |
| 354 | |
| 355 | return lines.join("\n"); |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Delete all usage log files, resetting stats to zero. |
no test coverage detected