Skip to main content
Glama
Logs.tsx7.54 kB
import { useState, useEffect, useRef } from 'react'; import { Download, Trash2, Pause, Play } from 'lucide-react'; interface LogEntry { timestamp: string; container: string; level: 'info' | 'warn' | 'error' | 'debug'; message: string; } export default function Logs() { const [logs, setLogs] = useState<LogEntry[]>([]); const [selectedContainer, setSelectedContainer] = useState('all'); const [autoScroll, setAutoScroll] = useState(true); const [isPaused, setIsPaused] = useState(false); const [filter, setFilter] = useState(''); const logsEndRef = useRef<HTMLDivElement>(null); const containers = ['all', 'ai-mcp-gateway', 'ai-mcp-postgres', 'ai-mcp-redis', 'ai-mcp-dashboard']; useEffect(() => { // Simulate log streaming if (!isPaused) { const interval = setInterval(() => { const newLog: LogEntry = { timestamp: new Date().toISOString(), container: containers[Math.floor(Math.random() * (containers.length - 1)) + 1], level: ['info', 'warn', 'error', 'debug'][Math.floor(Math.random() * 4)] as LogEntry['level'], message: generateRandomLogMessage(), }; setLogs((prev) => [...prev.slice(-99), newLog]); // Keep last 100 logs }, 2000); return () => clearInterval(interval); } }, [isPaused]); useEffect(() => { if (autoScroll) { logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [logs, autoScroll]); function generateRandomLogMessage(): string { const messages = [ 'Processing request...', 'Database connection established', 'Cache hit for key: user:123', 'API request received: POST /v1/chat/completions', 'Model response received in 1.23s', 'Cost tracking updated: $0.0023', 'Layer escalation triggered: L0 -> L1', 'Health check completed successfully', 'Redis connection pool active: 5/10', 'Request completed: 200 OK', ]; return messages[Math.floor(Math.random() * messages.length)]; } function clearLogs() { setLogs([]); } function downloadLogs() { const content = logs.map(log => `[${log.timestamp}] [${log.container}] [${log.level.toUpperCase()}] ${log.message}` ).join('\n'); const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `logs-${new Date().toISOString()}.txt`; a.click(); URL.revokeObjectURL(url); } const filteredLogs = logs.filter(log => { const matchesContainer = selectedContainer === 'all' || log.container === selectedContainer; const matchesFilter = !filter || log.message.toLowerCase().includes(filter.toLowerCase()); return matchesContainer && matchesFilter; }); const getLevelColor = (level: LogEntry['level']) => { switch (level) { case 'error': return 'text-red-400'; case 'warn': return 'text-yellow-400'; case 'info': return 'text-blue-400'; case 'debug': return 'text-slate-400'; default: return 'text-white'; } }; return ( <div className="space-y-6"> <div className="flex items-center justify-between"> <h1 className="text-3xl font-bold text-white">Docker Logs</h1> <div className="flex items-center gap-3"> <button onClick={() => setIsPaused(!isPaused)} className={`btn-secondary flex items-center gap-2 ${isPaused ? 'bg-yellow-500/20 text-yellow-400' : ''}`} > {isPaused ? ( <> <Play className="w-4 h-4" /> Resume </> ) : ( <> <Pause className="w-4 h-4" /> Pause </> )} </button> <button onClick={downloadLogs} className="btn-secondary flex items-center gap-2"> <Download className="w-4 h-4" /> Download </button> <button onClick={clearLogs} className="btn-danger flex items-center gap-2"> <Trash2 className="w-4 h-4" /> Clear </button> </div> </div> {/* Controls */} <div className="card p-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div> <label className="block text-sm font-semibold text-slate-300 mb-2"> Container </label> <select value={selectedContainer} onChange={(e) => setSelectedContainer(e.target.value)} className="input w-full" > {containers.map((container) => ( <option key={container} value={container}> {container === 'all' ? 'All Containers' : container} </option> ))} </select> </div> <div> <label className="block text-sm font-semibold text-slate-300 mb-2"> Filter </label> <input type="text" value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="Search logs..." className="input w-full" /> </div> <div> <label className="block text-sm font-semibold text-slate-300 mb-2"> Options </label> <label className="flex items-center gap-2 text-white cursor-pointer"> <input type="checkbox" checked={autoScroll} onChange={(e) => setAutoScroll(e.target.checked)} className="w-4 h-4 text-blue-500 rounded focus:ring-2 focus:ring-blue-500" /> Auto-scroll </label> </div> </div> </div> {/* Logs Display */} <div className="card p-4"> <div className="flex items-center justify-between mb-4"> <div className="text-sm text-slate-400"> Showing {filteredLogs.length} log{filteredLogs.length !== 1 ? 's' : ''} </div> {isPaused && ( <div className="badge badge-warning"> <Pause className="w-3 h-3 mr-1" /> Paused </div> )} </div> <div className="bg-slate-950 rounded-lg p-4 h-[600px] overflow-y-auto font-mono text-sm"> {filteredLogs.length === 0 ? ( <div className="flex items-center justify-center h-full text-slate-400"> No logs to display </div> ) : ( <div className="space-y-1"> {filteredLogs.map((log, index) => ( <div key={index} className="flex gap-3 hover:bg-slate-800/50 px-2 py-1 rounded"> <span className="text-slate-500 shrink-0"> {new Date(log.timestamp).toLocaleTimeString()} </span> <span className="text-purple-400 shrink-0 w-40 truncate"> {log.container} </span> <span className={`${getLevelColor(log.level)} shrink-0 w-16 uppercase`}> {log.level} </span> <span className="text-slate-300 break-all"> {log.message} </span> </div> ))} <div ref={logsEndRef} /> </div> )} </div> </div> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server