claude-code-router-config 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,410 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+ const { spawn } = require('child_process');
6
+
7
+ class EnhancedLogger {
8
+ constructor(options = {}) {
9
+ this.logDir = options.logDir || path.join(os.homedir(), '.claude-code-router', 'logs');
10
+ this.level = options.level || 'info';
11
+ this.enableConsole = options.enableConsole !== false;
12
+ this.enableFile = options.enableFile !== false;
13
+ this.enableMetrics = options.enableMetrics !== false;
14
+ this.enableAnalytics = options.enableAnalytics !== false;
15
+ this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024; // 10MB
16
+ this.maxFiles = options.maxFiles || 5;
17
+
18
+ // Initialize log directory
19
+ this.initLogDirectory();
20
+
21
+ // Current date for log rotation
22
+ this.currentDate = new Date().toISOString().split('T')[0];
23
+ this.logFile = path.join(this.logDir, `claude-router-${this.currentDate}.log`);
24
+ this.metricsFile = path.join(this.logDir, 'metrics.json');
25
+ this.errorFile = path.join(this.logDir, 'errors.log');
26
+ }
27
+
28
+ initLogDirectory() {
29
+ if (!fs.existsSync(this.logDir)) {
30
+ fs.mkdirSync(this.logDir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ // Log levels with numeric values for filtering
35
+ static levels = {
36
+ fatal: 0,
37
+ error: 1,
38
+ warn: 2,
39
+ info: 3,
40
+ debug: 4,
41
+ trace: 5
42
+ };
43
+
44
+ // Check if we should log at this level
45
+ shouldLog(level) {
46
+ return EnhancedLogger.levels[level] <= EnhancedLogger.levels[this.level];
47
+ }
48
+
49
+ // Format log entry
50
+ formatEntry(level, message, meta = {}) {
51
+ const timestamp = new Date().toISOString();
52
+ const pid = process.pid;
53
+ const entry = {
54
+ timestamp,
55
+ level: level.toUpperCase(),
56
+ pid,
57
+ message,
58
+ ...meta
59
+ };
60
+
61
+ // Format for console
62
+ const consoleMessage = `[${timestamp}] ${level.toUpperCase()} ${message}`;
63
+
64
+ return { entry, consoleMessage };
65
+ }
66
+
67
+ // Write to file with rotation
68
+ writeToFile(entry) {
69
+ if (!this.enableFile) return;
70
+
71
+ try {
72
+ // Check if we need to rotate log file
73
+ if (fs.existsSync(this.logFile)) {
74
+ const stats = fs.statSync(this.logFile);
75
+ if (stats.size > this.maxFileSize) {
76
+ this.rotateLog();
77
+ }
78
+ }
79
+
80
+ const logLine = JSON.stringify(entry) + '\n';
81
+ fs.appendFileSync(this.logFile, logLine);
82
+ } catch (error) {
83
+ console.error('Failed to write to log file:', error);
84
+ }
85
+ }
86
+
87
+ // Log rotation
88
+ rotateLog() {
89
+ const baseName = path.join(this.logDir, `claude-router-${this.currentDate}`);
90
+
91
+ // Rotate existing files
92
+ for (let i = this.maxFiles - 1; i >= 1; i--) {
93
+ const oldFile = `${baseName}.${i}.log`;
94
+ const newFile = `${baseName}.${i + 1}.log`;
95
+
96
+ if (fs.existsSync(oldFile)) {
97
+ if (i === this.maxFiles - 1) {
98
+ fs.unlinkSync(oldFile); // Delete oldest
99
+ } else {
100
+ fs.renameSync(oldFile, newFile);
101
+ }
102
+ }
103
+ }
104
+
105
+ // Move current log to .1
106
+ if (fs.existsSync(this.logFile)) {
107
+ fs.renameSync(this.logFile, `${baseName}.1.log`);
108
+ }
109
+ }
110
+
111
+ // Log to console with colors
112
+ writeToConsole(level, consoleMessage) {
113
+ if (!this.enableConsole) return;
114
+
115
+ let coloredMessage;
116
+ switch (level) {
117
+ case 'fatal':
118
+ coloredMessage = chalk.red.bold(consoleMessage);
119
+ break;
120
+ case 'error':
121
+ coloredMessage = chalk.red(consoleMessage);
122
+ break;
123
+ case 'warn':
124
+ coloredMessage = chalk.yellow(consoleMessage);
125
+ break;
126
+ case 'info':
127
+ coloredMessage = chalk.blue(consoleMessage);
128
+ break;
129
+ case 'debug':
130
+ coloredMessage = chalk.magenta(consoleMessage);
131
+ break;
132
+ case 'trace':
133
+ coloredMessage = chalk.gray(consoleMessage);
134
+ break;
135
+ default:
136
+ coloredMessage = consoleMessage;
137
+ }
138
+
139
+ console.log(coloredMessage);
140
+ }
141
+
142
+ // Log entry method
143
+ log(level, message, meta = {}) {
144
+ if (!this.shouldLog(level)) return;
145
+
146
+ const { entry, consoleMessage } = this.formatEntry(level, message, meta);
147
+
148
+ // Write to console
149
+ this.writeToConsole(level, consoleMessage);
150
+
151
+ // Write to file
152
+ this.writeToFile(entry);
153
+
154
+ // Handle errors specially
155
+ if (level === 'error' || level === 'fatal') {
156
+ this.logError(entry);
157
+ }
158
+
159
+ // Update metrics
160
+ if (this.enableMetrics) {
161
+ this.updateMetrics(level, meta);
162
+ }
163
+
164
+ // Send to analytics if enabled
165
+ if (this.enableAnalytics && meta.provider && meta.model) {
166
+ this.sendToAnalytics(entry);
167
+ }
168
+ }
169
+
170
+ // Log errors to separate file
171
+ logError(entry) {
172
+ try {
173
+ const errorLine = JSON.stringify(entry) + '\n';
174
+ fs.appendFileSync(this.errorFile, errorLine);
175
+ } catch (error) {
176
+ console.error('Failed to write to error log:', error);
177
+ }
178
+ }
179
+
180
+ // Update metrics
181
+ updateMetrics(level, meta) {
182
+ try {
183
+ let metrics = {};
184
+
185
+ if (fs.existsSync(this.metricsFile)) {
186
+ metrics = JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
187
+ }
188
+
189
+ const now = new Date().toISOString();
190
+
191
+ // Initialize metrics structure
192
+ if (!metrics.requests) metrics.requests = { total: 0, byLevel: {}, byProvider: {} };
193
+ if (!metrics.latency) metrics.latency = { avg: 0, min: Infinity, max: 0, samples: [] };
194
+ if (!metrics.errors) metrics.errors = { total: 0, byProvider: {} };
195
+ if (!metrics.costs) metrics.costs = { total: 0, byProvider: {} };
196
+ if (!metrics.uptime) metrics.uptime = { start: metrics.uptime?.start || now, lastActivity: now };
197
+
198
+ // Update request counts
199
+ metrics.requests.total++;
200
+ metrics.requests.byLevel[level] = (metrics.requests.byLevel[level] || 0) + 1;
201
+
202
+ if (meta.provider) {
203
+ metrics.requests.byProvider[meta.provider] = (metrics.requests.byProvider[meta.provider] || 0) + 1;
204
+ }
205
+
206
+ // Update latency
207
+ if (meta.latency) {
208
+ metrics.latency.samples.push(meta.latency);
209
+ metrics.latency.min = Math.min(metrics.latency.min, meta.latency);
210
+ metrics.latency.max = Math.max(metrics.latency.max, meta.latency);
211
+
212
+ // Keep only last 1000 samples for performance
213
+ if (metrics.latency.samples.length > 1000) {
214
+ metrics.latency.samples = metrics.latency.samples.slice(-1000);
215
+ }
216
+
217
+ metrics.latency.avg = Math.round(
218
+ metrics.latency.samples.reduce((a, b) => a + b, 0) / metrics.latency.samples.length
219
+ );
220
+ }
221
+
222
+ // Update errors
223
+ if (level === 'error' || level === 'fatal') {
224
+ metrics.errors.total++;
225
+ if (meta.provider) {
226
+ metrics.errors.byProvider[meta.provider] = (metrics.errors.byProvider[meta.provider] || 0) + 1;
227
+ }
228
+ }
229
+
230
+ // Update costs
231
+ if (meta.cost) {
232
+ metrics.costs.total += meta.cost;
233
+ if (meta.provider) {
234
+ metrics.costs.byProvider[meta.provider] = (metrics.costs.byProvider[meta.provider] || 0) + meta.cost;
235
+ }
236
+ }
237
+
238
+ // Update uptime
239
+ metrics.uptime.lastActivity = now;
240
+
241
+ fs.writeFileSync(this.metricsFile, JSON.stringify(metrics, null, 2));
242
+ } catch (error) {
243
+ console.error('Failed to update metrics:', error);
244
+ }
245
+ }
246
+
247
+ // Send to analytics module
248
+ sendToAnalytics(entry) {
249
+ if (!entry.meta || !entry.meta.provider || !entry.meta.model) return;
250
+
251
+ try {
252
+ const analyticsPath = path.join(__dirname, '..', 'cli', 'analytics.js');
253
+
254
+ // Call analytics module if available
255
+ const child = spawn('node', [analyticsPath, 'record',
256
+ entry.meta.provider,
257
+ entry.meta.model,
258
+ entry.meta.inputTokens || 0,
259
+ entry.meta.outputTokens || 0,
260
+ entry.meta.latency || 0,
261
+ entry.success !== false
262
+ ], {
263
+ stdio: 'ignore',
264
+ detached: true
265
+ });
266
+
267
+ child.unref();
268
+ } catch (error) {
269
+ // Silently fail analytics - don't break logging
270
+ }
271
+ }
272
+
273
+ // Convenience methods
274
+ fatal(message, meta = {}) {
275
+ this.log('fatal', message, { ...meta, stack: new Error().stack });
276
+ }
277
+
278
+ error(message, meta = {}) {
279
+ this.log('error', message, meta);
280
+ }
281
+
282
+ warn(message, meta = {}) {
283
+ this.log('warn', message, meta);
284
+ }
285
+
286
+ info(message, meta = {}) {
287
+ this.log('info', message, meta);
288
+ }
289
+
290
+ debug(message, meta = {}) {
291
+ this.log('debug', message, meta);
292
+ }
293
+
294
+ trace(message, meta = {}) {
295
+ this.log('trace', message, meta);
296
+ }
297
+
298
+ // Request logging with standard format
299
+ logRequest(provider, model, inputTokens, outputTokens, latency, success, cost = null) {
300
+ this.info('API Request', {
301
+ event: 'api_request',
302
+ provider,
303
+ model,
304
+ inputTokens,
305
+ outputTokens,
306
+ totalTokens: inputTokens + outputTokens,
307
+ latency,
308
+ success,
309
+ cost,
310
+ timestamp: new Date().toISOString()
311
+ });
312
+ }
313
+
314
+ // Route decision logging
315
+ logRoute(request, selectedProvider, selectedModel, reason, alternatives = []) {
316
+ this.debug('Route Decision', {
317
+ event: 'route_decision',
318
+ requestId: request.id || 'unknown',
319
+ requestType: request.type || 'unknown',
320
+ selectedProvider,
321
+ selectedModel,
322
+ reason,
323
+ alternatives,
324
+ timestamp: new Date().toISOString()
325
+ });
326
+ }
327
+
328
+ // Health check logging
329
+ logHealthCheck(provider, status, latency = null, error = null) {
330
+ const level = status === 'healthy' ? 'info' : 'warn';
331
+ this.log(level, `Health Check - ${provider}`, {
332
+ event: 'health_check',
333
+ provider,
334
+ status,
335
+ latency,
336
+ error,
337
+ timestamp: new Date().toISOString()
338
+ });
339
+ }
340
+
341
+ // Get metrics summary
342
+ getMetrics() {
343
+ try {
344
+ if (fs.existsSync(this.metricsFile)) {
345
+ return JSON.parse(fs.readFileSync(this.metricsFile, 'utf8'));
346
+ }
347
+ } catch (error) {
348
+ console.error('Failed to read metrics:', error);
349
+ }
350
+ return null;
351
+ }
352
+
353
+ // Get recent logs
354
+ getRecentLogs(count = 100, level = null) {
355
+ try {
356
+ if (!fs.existsSync(this.logFile)) return [];
357
+
358
+ const content = fs.readFileSync(this.logFile, 'utf8');
359
+ const lines = content.trim().split('\n').filter(line => line);
360
+ const logs = lines.map(line => {
361
+ try {
362
+ return JSON.parse(line);
363
+ } catch {
364
+ return null;
365
+ }
366
+ }).filter(Boolean);
367
+
368
+ // Filter by level if specified
369
+ if (level) {
370
+ return logs.filter(log => log.level === level.toUpperCase()).slice(-count);
371
+ }
372
+
373
+ return logs.slice(-count);
374
+ } catch (error) {
375
+ console.error('Failed to read recent logs:', error);
376
+ return [];
377
+ }
378
+ }
379
+
380
+ // Clean old logs
381
+ cleanup() {
382
+ try {
383
+ const files = fs.readdirSync(this.logDir);
384
+ const now = Date.now();
385
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
386
+
387
+ files.forEach(file => {
388
+ if (file.endsWith('.log') || file.endsWith('.json')) {
389
+ const filePath = path.join(this.logDir, file);
390
+ const stats = fs.statSync(filePath);
391
+
392
+ if (now - stats.mtime.getTime() > maxAge) {
393
+ fs.unlinkSync(filePath);
394
+ console.log(`Cleaned up old log file: ${file}`);
395
+ }
396
+ }
397
+ });
398
+ } catch (error) {
399
+ console.error('Failed to cleanup logs:', error);
400
+ }
401
+ }
402
+ }
403
+
404
+ // Export singleton instance
405
+ const logger = new EnhancedLogger();
406
+
407
+ module.exports = {
408
+ EnhancedLogger,
409
+ logger
410
+ };