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.
- package/README.md +169 -8
- package/cli/analytics.js +509 -0
- package/cli/benchmark.js +342 -0
- package/cli/commands.js +300 -0
- package/config/smart-intent-router.js +543 -0
- package/docs/v1.1.0-FEATURES.md +752 -0
- package/logging/enhanced-logger.js +410 -0
- package/logging/health-monitor.js +472 -0
- package/logging/middleware.js +384 -0
- package/package.json +29 -6
- package/plugins/plugin-manager.js +607 -0
- package/templates/README.md +161 -0
- package/templates/balanced.json +111 -0
- package/templates/cost-optimized.json +96 -0
- package/templates/development.json +104 -0
- package/templates/performance-optimized.json +88 -0
- package/templates/quality-focused.json +105 -0
- package/web-dashboard/public/css/dashboard.css +575 -0
- package/web-dashboard/public/index.html +308 -0
- package/web-dashboard/public/js/dashboard.js +512 -0
- package/web-dashboard/server.js +352 -0
|
@@ -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
|
+
};
|