// LogsTab.jsx — Sistem Log İzleme // SSE ile canlı streaming · Seviye filtresi · Arama & highlight // Terminal-style viewer (koyu panel) + uygulama temasıyla uyumlu kontrol çubuğu (function () { const { useState, useEffect, useRef, useCallback, useMemo } = React; /* ── Sabitler ─────────────────────────────────────────────────── */ const LOG_FILES = [ { key: 'app', label: 'Uygulama', hint: 'app.log' }, { key: 'sip', label: 'SIP / ESL', hint: 'sip_events.log' }, { key: 'audit', label: 'Denetim', hint: 'audit.log' }, { key: 'http', label: 'HTTP', hint: 'http_access.log' }, { key: 'freeswitch', label: 'FreeSWITCH', hint: 'freeswitch.log / journalctl' }, ]; const LEVELS = ['ALL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']; const LEVEL_STYLE = { ERROR: { fg: '#f87171', bg: 'rgba(248,113,113,.07)', bar: '#f87171' }, WARNING: { fg: '#fbbf24', bg: 'rgba(251,191,36,.05)', bar: '#fbbf24' }, INFO: { fg: '#cbd5e1', bg: 'transparent', bar: 'transparent' }, DEBUG: { fg: '#475569', bg: 'transparent', bar: 'transparent' }, OTHER: { fg: '#64748b', bg: 'transparent', bar: 'transparent' }, }; const LEVEL_FILTER_COLOR = { ALL: '#5b7a92', ERROR: '#ef4444', WARNING: '#f59e0b', INFO: '#3b82f6', DEBUG: '#64748b', }; const MAX_BUF = 2000; // UI'de tutulacak max satır let _uid = 0; /* ── Yardımcılar ──────────────────────────────────────────────── */ function detectLevel(text) { // Önce JSON yapısal log kontrolü try { const o = JSON.parse(text); if (o.level) { const l = o.level.toUpperCase(); if (l === 'ERROR' || l === 'CRITICAL') return 'ERROR'; if (l === 'WARNING' || l === 'WARN') return 'WARNING'; if (l === 'DEBUG') return 'DEBUG'; if (l === 'INFO') return 'INFO'; } } catch {} if (/\b(ERROR|CRITICAL|CRIT)\b|\[ERR\]/i.test(text)) return 'ERROR'; if (/\b(WARNING|WARN)\b/i.test(text)) return 'WARNING'; if (/\bDEBUG\b/i.test(text)) return 'DEBUG'; if (/\bINFO\b/i.test(text)) return 'INFO'; return 'OTHER'; } function formatLogLine(raw) { try { const o = JSON.parse(raw); if (!o['@timestamp'] && !o.timestamp && !o.level) return raw; // Backend @timestamp UTC olarak yazılır (Elasticsearch/Filebeat standardı). // UI'da kullanıcının yerel saatine çevirip göster. const rawTs = o['@timestamp'] || o.timestamp || ''; let ts = ''; if (rawTs) { const d = parseUTC(rawTs); ts = d ? d.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : rawTs.replace(/^.*T/, '').replace(/\.\d{3}Z$/, ''); // fallback (eski davranış) } const lvl = (o.level || '').toUpperCase().padEnd(7); const src = o.logger || o.module || ''; const msg = o.message || ''; let line = ''; if (ts) line += ts + ' '; line += lvl; if (src) line += ' [' + src + ']'; line += ' ' + msg; // Exception bilgisi if (o.exception && o.exception.type) { line += '\n └─ ' + o.exception.type + ': ' + o.exception.message; } return line; } catch { return raw; } } function makeLine(text, hist = false) { const level = detectLevel(text); const formatted = formatLogLine(text); return { id: ++_uid, text: formatted, level, hist }; } function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /* ── Ana Bileşen ──────────────────────────────────────────────── */ function LogsTab() { const [logFile, setLogFile] = useState('app'); const [lines, setLines] = useState([]); const [filterLevel, setFilterLevel] = useState('ALL'); const [search, setSearch] = useState(''); const [autoScroll, setAutoScroll] = useState(true); const [paused, setPaused] = useState(false); const [status, setStatus] = useState('idle'); // idle | connecting | live | error | closed const bottomRef = useRef(null); const esRef = useRef(null); const pausedRef = useRef(false); const pendingRef = useRef([]); // pause sırasında biriken satırlar const autoScrollRef = useRef(true); pausedRef.current = paused; autoScrollRef.current = autoScroll; /* ── SSE Bağlantısı ─────────────────────────────────────────── */ const connect = useCallback(() => { if (esRef.current) { esRef.current.close(); esRef.current = null; } pendingRef.current = []; setLines([]); setStatus('connecting'); const url = `/api/logs/stream?log_file=${logFile}&lines=150`; const es = new EventSource(url); esRef.current = es; es.onmessage = (evt) => { let msg; try { msg = JSON.parse(evt.data); } catch { return; } if (msg.t === 'ping') return; if (msg.t === 'sep') { setStatus('live'); return; } if (msg.t === 'error') { setLines(prev => [...prev, makeLine(`[HATA] ${msg.text}`, true)]); return; } if (msg.t === 'line' && msg.text) { const line = makeLine(msg.text, !!msg.hist); if (pausedRef.current) { pendingRef.current.push(line); } else { setLines(prev => { const next = [...prev, line]; return next.length > MAX_BUF ? next.slice(-MAX_BUF) : next; }); } } }; es.onerror = () => { setStatus('error'); es.close(); esRef.current = null; }; }, [logFile]); // Dosya değişince yeniden bağlan useEffect(() => { connect(); return () => { if (esRef.current) esRef.current.close(); }; }, [connect]); // Tab görünmez olunca bağlantıyı kapat (kaynak tasarrufu) useEffect(() => { const onVis = () => { if (document.hidden) { if (esRef.current) { esRef.current.close(); esRef.current = null; setStatus('closed'); } } else { if (!esRef.current) connect(); } }; document.addEventListener('visibilitychange', onVis); return () => document.removeEventListener('visibilitychange', onVis); }, [connect]); /* ── Auto-scroll ─────────────────────────────────────────────── */ useEffect(() => { if (autoScrollRef.current && bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: 'instant' }); } }, [lines]); /* ── Pause / Resume ──────────────────────────────────────────── */ const togglePause = () => { const goLive = paused; // şu an duraklamış → devam setPaused(!paused); if (goLive && pendingRef.current.length > 0) { const pending = pendingRef.current.splice(0); setLines(prev => { const next = [...prev, ...pending]; return next.length > MAX_BUF ? next.slice(-MAX_BUF) : next; }); } }; /* ── Filtreli Satırlar (memo) ─────────────────────────────────── */ const visible = useMemo(() => { let out = lines; if (filterLevel !== 'ALL') out = out.filter(l => l.level === filterLevel); if (search.trim()) { const q = search.trim().toLowerCase(); out = out.filter(l => l.text.toLowerCase().includes(q)); } return out; }, [lines, filterLevel, search]); /* ── Arama Highlight ─────────────────────────────────────────── */ function highlight(text) { if (!search.trim()) return text; const re = new RegExp(`(${escapeRe(search.trim())})`, 'gi'); const parts = text.split(re); return parts.map((p, i) => re.test(p) ? React.createElement('mark', { key: i, style: { background: '#fef08a', color: '#1a1a1a', borderRadius: 2, padding: '0 1px' }, }, p) : p ); } /* ── Durum göstergesi ────────────────────────────────────────── */ const statusInfo = { idle: { color: '#475569', label: 'Bekleniyor' }, connecting: { color: '#f59e0b', label: 'Bağlanıyor...' }, live: { color: '#10b981', label: 'Canlı' }, error: { color: '#ef4444', label: 'Bağlantı kesildi' }, closed: { color: '#64748b', label: 'Duraklatıldı' }, }[status] || { color: '#475569', label: status }; const pendingCount = pendingRef.current.length; /* ── Render ──────────────────────────────────────────────────── */ return (
{/* ═══ Kontrol Çubuğu ═══ */}
{/* Durum göstergesi */}
{statusInfo.label}
{/* Log Dosyası Seçici */}
{LOG_FILES.map(f => ( ))}
{/* Seviye Filtresi */}
{LEVELS.map(lvl => { const active = filterLevel === lvl; const c = LEVEL_FILTER_COLOR[lvl]; return ( ); })}
{/* Arama */}
setSearch(e.target.value)} placeholder="Ara... (regex destekli)" style={{ width: '100%', paddingLeft: 26, paddingRight: search ? 26 : 8, paddingTop: 5, paddingBottom: 5, background: '#f0f4f8', border: '1px solid #dce6ef', borderRadius: 6, fontSize: 12, color: '#1a3047', outline: 'none', boxSizing: 'border-box', }} /> {search && ( )}
{/* Sağ kontroller */}
{visible.length.toLocaleString('tr-TR')} / {lines.length.toLocaleString('tr-TR')} satır
{/* ═══ Terminal Görünümü ═══ */}
{/* Başlık çubuğu */}
{LOG_FILES.find(f => f.key === logFile)?.hint || 'app.log'} max {MAX_BUF.toLocaleString('tr-TR')} satır buffer
{/* Log satırları */} {visible.length === 0 ? (
{status === 'connecting' ? '⟳ Bağlanıyor...' : filterLevel !== 'ALL' || search ? 'Filtreyle eşleşen satır yok' : 'Henüz log satırı yok'}
) : (
{visible.map(line => { const ls = LEVEL_STYLE[line.level] || LEVEL_STYLE.OTHER; return (
{/* Sol durum çizgisi */} {/* Satır içeriği */} {highlight(line.text)}
); })}
)}
); } window.LogsTab = LogsTab; })();