UNPKG

gemini-cli-templates

Version:

Advanced analytics dashboard and monitoring tool for Gemini CLI with real-time metrics, token tracking, and telemetry visualization

457 lines (398 loc) 18.3 kB
const express = require('express'); const path = require('path'); const http = require('http'); const fs = require('fs'); const os = require('os'); const app = express(); // Serve static files app.use(express.static(path.join(__dirname, 'public'))); // Manual proxy for Jaeger API app.get('/api/services', (req, res) => { const jaegerUrl = `http://localhost:16686/api/services`; console.log('Fetching services:', jaegerUrl); http.get(jaegerUrl, (jaegerRes) => { let data = ''; jaegerRes.on('data', (chunk) => { data += chunk; }); jaegerRes.on('end', () => { res.setHeader('Content-Type', 'application/json'); res.send(data); }); }).on('error', (err) => { console.error('Jaeger API error:', err.message); res.status(500).json({ error: 'Failed to fetch from Jaeger: ' + err.message }); }); }); app.get('/api/traces', (req, res) => { const queryString = req.url.includes('?') ? req.url.split('?')[1] : ''; const jaegerUrl = `http://localhost:16686/api/traces${queryString ? '?' + queryString : ''}`; console.log('Fetching traces:', jaegerUrl); http.get(jaegerUrl, (jaegerRes) => { let data = ''; jaegerRes.on('data', (chunk) => { data += chunk; }); jaegerRes.on('end', () => { res.setHeader('Content-Type', 'application/json'); res.send(data); }); }).on('error', (err) => { console.error('Jaeger API error:', err.message); res.status(500).json({ error: 'Failed to fetch from Jaeger: ' + err.message }); }); }); // Get Gemini CLI metrics from collector logs app.get('/api/metrics', (req, res) => { try { const homeDir = os.homedir(); const geminiTmpPath = path.join(homeDir, '.gemini', 'tmp'); // Find the latest collector log file let logFile = null; if (fs.existsSync(geminiTmpPath)) { const tmpDirs = fs.readdirSync(geminiTmpPath) .map(dir => ({ name: dir, path: path.join(geminiTmpPath, dir) })) .filter(dir => fs.statSync(dir.path).isDirectory()) .sort((a, b) => fs.statSync(b.path).mtime.getTime() - fs.statSync(a.path).mtime.getTime()); for (const dir of tmpDirs) { const collectorLogPath = path.join(dir.path, 'otel', 'collector.log'); if (fs.existsSync(collectorLogPath)) { logFile = collectorLogPath; break; // Found the latest one } } } if (!logFile) { return res.json({ metrics: [], message: 'No collector log found' }); } // Read recent metrics from log const logContent = fs.readFileSync(logFile, 'utf8'); const lines = logContent.split('\n'); // Read all lines const metrics = []; const tokenEvents = []; let currentMetric = {}; let currentLogRecord = {}; let inDataPoint = false; let inLogRecord = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; try { // Start of a new metric if (line.includes('Metric #')) { currentMetric = { attributes: {} }; inDataPoint = false; inLogRecord = false; } // Start of a new log record - first complete any previous tool call record if (line.includes('LogRecord #')) { // Complete previous tool call log record if it exists if (inLogRecord && currentLogRecord.isToolCall && currentLogRecord.sessionId && currentLogRecord.timestamp && currentLogRecord.functionName) { tokenEvents.push({ name: 'gemini_cli.tool.usage', description: 'Tool call executed', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: 1, attributes: { operation: currentLogRecord.functionName, success: currentLogRecord.success ? 'true' : 'false', duration_ms: currentLogRecord.durationMs || 0 }, type: 'counter' }); } currentLogRecord = { attributes: {} }; inLogRecord = true; inDataPoint = false; } // Extract metric name if (line.includes('-> Name:')) { const nameMatch = line.match(/-> Name: (.+)$/); if (nameMatch) currentMetric.name = nameMatch[1].trim(); } // Extract description if (line.includes('-> Description:')) { const descMatch = line.match(/-> Description: (.+)$/); if (descMatch) currentMetric.description = descMatch[1].trim(); } // Start of a data point if (line.includes('NumberDataPoints #')) { inDataPoint = true; // Reset attributes for new data point if (currentMetric.name) { currentMetric.attributes = {}; } } // Extract all attributes if (inDataPoint && line.includes('-> ')) { // Session ID if (line.includes('session.id: Str(')) { const sessionMatch = line.match(/session\.id: Str\(([^)]+)\)/); if (sessionMatch) currentMetric.attributes.sessionId = sessionMatch[1]; } // Model if (line.includes('model: Str(')) { const modelMatch = line.match(/model: Str\(([^)]+)\)/); if (modelMatch) currentMetric.attributes.model = modelMatch[1]; } // Type (input/output for tokens) if (line.includes('type: Str(')) { const typeMatch = line.match(/type: Str\(([^)]+)\)/); if (typeMatch) currentMetric.attributes.type = typeMatch[1]; } // Token type from description if (currentMetric.name && currentMetric.name.includes('gemini.token.count')) { if (currentMetric.description && currentMetric.description.toLowerCase().includes('input')) { currentMetric.attributes.type = 'input'; } else if (currentMetric.description && currentMetric.description.toLowerCase().includes('output')) { currentMetric.attributes.type = 'output'; } } // Operation (for file operations) if (line.includes('operation: Str(')) { const opMatch = line.match(/operation: Str\(([^)]+)\)/); if (opMatch) currentMetric.attributes.operation = opMatch[1]; } // Lines (for file operations) if (line.includes('lines: Int(')) { const linesMatch = line.match(/lines: Int\(([^)]+)\)/); if (linesMatch) currentMetric.attributes.lines = parseInt(linesMatch[1]); } // Mimetype if (line.includes('mimetype: Str(')) { const mimetypeMatch = line.match(/mimetype: Str\(([^)]+)\)/); if (mimetypeMatch) currentMetric.attributes.mimetype = mimetypeMatch[1]; } // Extension if (line.includes('extension: Str(')) { const extMatch = line.match(/extension: Str\(([^)]+)\)/); if (extMatch) currentMetric.attributes.extension = extMatch[1]; } } // Extract timestamp for metrics if (line.includes('Timestamp:') && !line.includes('StartTimestamp') && !inLogRecord) { const timestampMatch = line.match(/Timestamp: (.+) UTC$/); if (timestampMatch) currentMetric.timestamp = timestampMatch[1]; } // Extract timestamp for log records if (line.includes('Timestamp:') && !line.includes('StartTimestamp') && !line.includes('ObservedTimestamp') && inLogRecord) { const timestampMatch = line.match(/Timestamp: (.+) UTC$/); if (timestampMatch) currentLogRecord.timestamp = timestampMatch[1]; } // Parse data from log record attributes if (inLogRecord && line.includes('-> ')) { // Session ID for log records if (line.includes('session.id: Str(')) { const sessionMatch = line.match(/session\.id: Str\(([^)]+)\)/); if (sessionMatch) currentLogRecord.sessionId = sessionMatch[1]; } // Event name to identify API response logs if (line.includes('event.name: Str(') && line.includes('gemini_cli.api_response')) { currentLogRecord.isApiResponse = true; } // Event name to identify tool call logs if (line.includes('event.name: Str(') && line.includes('gemini_cli.tool_call')) { currentLogRecord.isToolCall = true; } // Model information if (line.includes('model: Str(')) { const modelMatch = line.match(/model: Str\(([^)]+)\)/); if (modelMatch) currentLogRecord.model = modelMatch[1]; } // Function name for tool calls if (line.includes('function_name: Str(')) { const funcMatch = line.match(/function_name: Str\(([^)]+)\)/); if (funcMatch) currentLogRecord.functionName = funcMatch[1]; } // Duration (context-aware) if (line.includes('duration_ms: Int(')) { const durationMatch = line.match(/duration_ms: Int\((\d+)\)/); if (durationMatch) { const duration = parseInt(durationMatch[1]); if (currentLogRecord.isApiResponse) { currentLogRecord.apiDurationMs = duration; } else if (currentLogRecord.isToolCall) { currentLogRecord.durationMs = duration; } } } // Success status for tool calls if (line.includes('success: Bool(')) { const successMatch = line.match(/success: Bool\(([^)]+)\)/); if (successMatch) currentLogRecord.success = successMatch[1] === 'true'; } // Token counts if (line.includes('input_token_count: Int(')) { const tokenMatch = line.match(/input_token_count: Int\((\d+)\)/); if (tokenMatch) currentLogRecord.inputTokens = parseInt(tokenMatch[1]); } if (line.includes('output_token_count: Int(')) { const tokenMatch = line.match(/output_token_count: Int\((\d+)\)/); if (tokenMatch) currentLogRecord.outputTokens = parseInt(tokenMatch[1]); } if (line.includes('cached_content_token_count: Int(')) { const tokenMatch = line.match(/cached_content_token_count: Int\((\d+)\)/); if (tokenMatch) currentLogRecord.cachedTokens = parseInt(tokenMatch[1]); } if (line.includes('thoughts_token_count: Int(')) { const tokenMatch = line.match(/thoughts_token_count: Int\((\d+)\)/); if (tokenMatch) currentLogRecord.thoughtsTokens = parseInt(tokenMatch[1]); } if (line.includes('total_token_count: Int(')) { const tokenMatch = line.match(/total_token_count: Int\((\d+)\)/); if (tokenMatch) currentLogRecord.totalTokens = parseInt(tokenMatch[1]); } } // Complete log record and extract token metrics if (inLogRecord && currentLogRecord.isApiResponse && currentLogRecord.sessionId && currentLogRecord.timestamp) { if (currentLogRecord.inputTokens !== undefined) { tokenEvents.push({ name: 'gemini_cli.token.usage', description: 'Input tokens used in API call', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: currentLogRecord.inputTokens, attributes: { type: 'input', model: currentLogRecord.model }, type: 'counter' }); } if (currentLogRecord.outputTokens !== undefined) { tokenEvents.push({ name: 'gemini_cli.token.usage', description: 'Output tokens generated in API call', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: currentLogRecord.outputTokens, attributes: { type: 'output', model: currentLogRecord.model }, type: 'counter' }); } if (currentLogRecord.cachedTokens !== undefined) { tokenEvents.push({ name: 'gemini_cli.token.usage', description: 'Cached tokens used in API call', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: currentLogRecord.cachedTokens, attributes: { type: 'cached', model: currentLogRecord.model }, type: 'counter' }); } if (currentLogRecord.thoughtsTokens !== undefined) { tokenEvents.push({ name: 'gemini_cli.token.usage', description: 'Thoughts tokens used in API call', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: currentLogRecord.thoughtsTokens, attributes: { type: 'thoughts', model: currentLogRecord.model }, type: 'counter' }); } // Create API response time metric if we have duration data if (currentLogRecord.apiDurationMs !== undefined) { tokenEvents.push({ name: 'gemini_cli.api.response_time', description: 'API response time in milliseconds', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: currentLogRecord.apiDurationMs, attributes: { model: currentLogRecord.model, duration_ms: currentLogRecord.apiDurationMs }, type: 'gauge' }); } // Reset for next log record currentLogRecord = { attributes: {} }; inLogRecord = false; } // Complete log record and extract tool call metrics if (inLogRecord && currentLogRecord.isToolCall && currentLogRecord.sessionId && currentLogRecord.timestamp && currentLogRecord.functionName) { tokenEvents.push({ name: 'gemini_cli.tool.usage', description: 'Tool call executed', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: 1, attributes: { operation: currentLogRecord.functionName, success: currentLogRecord.success ? 'true' : 'false', duration_ms: currentLogRecord.durationMs || 0 }, type: 'counter' }); // Reset for next log record currentLogRecord = { attributes: {} }; inLogRecord = false; } // Extract value and complete the metric if (line.includes('Value:')) { const valueMatch = line.match(/Value: (\d+)$/); if (valueMatch) { currentMetric.value = parseInt(valueMatch[1]); // Complete metric found, add to array if (currentMetric.name && currentMetric.attributes.sessionId && currentMetric.timestamp) { metrics.push({ name: currentMetric.name, description: currentMetric.description || '', sessionId: currentMetric.attributes.sessionId, timestamp: currentMetric.timestamp, value: currentMetric.value, attributes: { ...currentMetric.attributes }, type: 'counter' }); } inDataPoint = false; // Reset for next data point } } } catch (e) { // Skip invalid lines } } // Complete any remaining tool call log record if (inLogRecord && currentLogRecord.isToolCall && currentLogRecord.sessionId && currentLogRecord.timestamp && currentLogRecord.functionName) { tokenEvents.push({ name: 'gemini_cli.tool.usage', description: 'Tool call executed', sessionId: currentLogRecord.sessionId, timestamp: currentLogRecord.timestamp, value: 1, attributes: { operation: currentLogRecord.functionName, success: currentLogRecord.success ? 'true' : 'false', duration_ms: currentLogRecord.durationMs || 0 }, type: 'counter' }); } // Combine regular metrics with token events const allMetrics = [...metrics, ...tokenEvents]; // Deduplicate and sort by timestamp const uniqueMetrics = allMetrics .filter((metric, index, arr) => arr.findIndex(m => m.timestamp === metric.timestamp && m.sessionId === metric.sessionId && m.name === metric.name && JSON.stringify(m.attributes) === JSON.stringify(metric.attributes)) === index ) .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) .slice(0, 100); // Last 100 metrics (increased to show more token data) // Log token and tool metrics for debugging const tokenMetrics = uniqueMetrics.filter(m => m.name && m.name.includes('token')); const toolMetrics = uniqueMetrics.filter(m => m.name && m.name.includes('tool')); console.log('Token metrics found:', tokenMetrics.length); console.log('Tool metrics found:', toolMetrics.length); console.log(`Found ${uniqueMetrics.length} metrics`); res.json({ metrics: uniqueMetrics, total: uniqueMetrics.length }); } catch (error) { console.error('Error reading metrics:', error.message); res.status(500).json({ error: 'Failed to read metrics: ' + error.message }); } }); // Test route app.get('/test', (req, res) => { res.json({ message: 'Server is working' }); }); app.listen(3337, () => { console.log('Gemini CLI Dashboard listening on port 3337'); console.log('Open http://localhost:3337 in your browser'); });