Add SDK usage tracking to JSONL logs

Features:
- New UsageLogger utility that writes usage metrics to daily JSONL files
- Captures token counts, costs, timing, and cache metrics from SDK result messages
- Usage logs stored in ~/.claude-mem/usage-logs/ (one file per day)
- Added analyze-usage.js script for analyzing usage patterns

Usage data captured:
- Token counts (input, output, cache creation, cache read)
- Total cost in USD per API call
- Duration metrics (total and API time)
- Number of turns per session
- Session and project attribution

Analysis script features:
- Aggregates totals by project and model
- Shows cache hit rates and savings
- Displays cost breakdowns and averages
- npm scripts: usage:analyze and usage:today

Files:
- src/utils/usage-logger.ts (new)
- src/services/worker-service.ts (modified - captures SDK result messages)
- scripts/analyze-usage.js (new)
- package.json (added usage:* npm scripts)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-11-02 21:53:42 -05:00
parent 9215c7e1f5
commit f20bb5bced
7 changed files with 327 additions and 29 deletions
+38
View File
@@ -13,6 +13,7 @@ import type { SDKSession } from '../sdk/prompts.js';
import { logger } from '../utils/logger.js';
import { ensureAllDataDirs } from '../shared/paths.js';
import { execSync } from 'child_process';
import { UsageLogger } from '../utils/usage-logger.js';
const MODEL = process.env.CLAUDE_MEM_MODEL || 'claude-sonnet-4-5';
const DISALLOWED_TOOLS = ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'];
@@ -83,10 +84,12 @@ class WorkerService {
private app: express.Application;
private port: number | null = null;
private sessions: Map<number, ActiveSession> = new Map();
private usageLogger: UsageLogger;
constructor() {
this.app = express();
this.app.use(express.json({ limit: '50mb' }));
this.usageLogger = new UsageLogger();
// Health check
this.app.get('/health', this.handleHealth.bind(this));
@@ -409,6 +412,41 @@ class WorkerService {
// Parse and store with prompt number
this.handleAgentMessage(session, textContent, session.lastPromptNumber);
}
// Capture usage data from result messages
if (message.type === 'result' && message.subtype === 'success') {
const usageData = {
timestamp: new Date().toISOString(),
sessionDbId: session.sessionDbId,
claudeSessionId: session.claudeSessionId,
project: session.project,
promptNumber: session.lastPromptNumber,
model: MODEL,
sessionId: message.session_id,
uuid: message.uuid,
durationMs: message.duration_ms,
durationApiMs: message.duration_api_ms,
numTurns: message.num_turns,
totalCostUsd: message.total_cost_usd,
usage: {
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
cacheCreationInputTokens: message.usage.cache_creation_input_tokens,
cacheReadInputTokens: message.usage.cache_read_input_tokens
}
};
this.usageLogger.logUsage(usageData);
logger.info('SDK', 'Usage data logged', {
sessionId: session.sessionDbId,
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
cacheCreation: message.usage.cache_creation_input_tokens,
cacheRead: message.usage.cache_read_input_tokens,
totalCostUsd: message.total_cost_usd
});
}
}
// Mark completed
+61
View File
@@ -0,0 +1,61 @@
import { appendFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
/**
* Usage data structure from Claude Agent SDK result messages
*/
export interface UsageData {
timestamp: string;
sessionDbId: number;
claudeSessionId: string;
project: string;
promptNumber: number;
model: string;
sessionId: string; // SDK session ID
uuid: string; // SDK message UUID
durationMs: number;
durationApiMs: number;
numTurns: number;
totalCostUsd: number;
usage: {
inputTokens: number;
outputTokens: number;
cacheCreationInputTokens: number;
cacheReadInputTokens: number;
};
}
/**
* Logger for capturing usage metrics to JSONL files
*/
export class UsageLogger {
private logDir: string;
private logFile: string;
constructor() {
this.logDir = join(homedir(), '.claude-mem', 'usage-logs');
// Create a daily log file
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
this.logFile = join(this.logDir, `usage-${date}.jsonl`);
}
/**
* Log usage data from SDK result message
*/
logUsage(data: UsageData): void {
try {
const line = JSON.stringify(data) + '\n';
appendFileSync(this.logFile, line, 'utf-8');
} catch (error) {
console.error('Failed to log usage data:', error);
}
}
/**
* Get the current log file path
*/
getLogFilePath(): string {
return this.logFile;
}
}