diff --git a/dockpeek/main.py b/dockpeek/main.py index bb4a829..ed7f861 100644 --- a/dockpeek/main.py +++ b/dockpeek/main.py @@ -503,13 +503,14 @@ def get_logs(): return jsonify(result), 500 -@main_bp.route("/stream-container-logs", methods=["GET"]) +@main_bp.route("/stream-container-logs", methods=["POST"]) @conditional_login_required def stream_logs(): - server_name = request.args.get('server_name') - container_name = request.args.get('container_name') - tail = int(request.args.get('tail', 100)) - is_swarm = request.args.get('is_swarm', 'false').lower() == 'true' + request_data = request.get_json() or {} + server_name = request_data.get('server_name') + container_name = request_data.get('container_name') + tail = request_data.get('tail', 100) + is_swarm = request_data.get('is_swarm', False) if not server_name or not container_name: return jsonify({"error": "Missing server_name or container_name"}), 400 @@ -531,13 +532,15 @@ def stream_logs(): stream_func = stream_container_logs for log_line in stream_func(stream_client, container_name, tail): - yield f"data: {log_line}\n\n" + chunk = json.dumps({"line": log_line}) + "\n" + yield chunk except GeneratorExit: logger.info(f"Stream closed for {container_name}") raise except Exception as e: logger.error(f"Stream error: {e}") - yield f"data: Error: {str(e)}\n\n" + error_chunk = json.dumps({"error": str(e)}) + "\n" + yield error_chunk finally: try: stream_client.close() @@ -546,7 +549,7 @@ def stream_logs(): response = Response( generate(), - mimetype='text/event-stream', + mimetype='application/x-ndjson', headers={ 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no', diff --git a/dockpeek/static/js/modules/logs-viewer.js b/dockpeek/static/js/modules/logs-viewer.js index a2f9f94..6f97244 100644 --- a/dockpeek/static/js/modules/logs-viewer.js +++ b/dockpeek/static/js/modules/logs-viewer.js @@ -4,7 +4,7 @@ export class LogsViewer { constructor() { this.modal = null; this.logsContent = null; - this.eventSource = null; + this.streamReader = null; this.isStreaming = false; this.currentServer = null; this.currentContainer = null; @@ -15,6 +15,7 @@ export class LogsViewer { this.containerList = []; this.currentContainerIndex = -1; this.fetchController = null; + this.streamController = null; this.initModal(); } @@ -350,8 +351,13 @@ export class LogsViewer { this.fetchController = null; } + if (this.streamController) { + this.streamController.abort(); + this.streamController = null; + } + document.body.style.overflow = ''; - + this.modal.classList.add('hidden'); this.logsContent.innerHTML = ''; const searchInput = document.getElementById('logs-search-input'); @@ -555,57 +561,116 @@ export class LogsViewer { const tail = Math.min(parseInt(tailSelect.value) || 100, 100); this.isStreaming = true; + this.streamController = new AbortController(); this.updateStreamButton(); - const url = `${apiUrl('/stream-container-logs')}?server_name=${encodeURIComponent(this.currentServer)}&container_name=${encodeURIComponent(this.currentContainer)}&tail=${tail}&is_swarm=${this.isSwarm || false}`; + try { + const response = await fetch(apiUrl('/stream-container-logs'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + server_name: this.currentServer, + container_name: this.currentContainer, + tail: tail, + is_swarm: this.isSwarm || false + }), + signal: this.streamController.signal + }); - this.eventSource = new EventSource(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - this.eventSource.onmessage = (event) => { - const line = event.data; - this.appendLogLine(line); - }; - - this.eventSource.onerror = (error) => { - console.error('Stream error:', error); - this.stopStreaming(); - this.updateStatus('Stream disconnected'); - }; - - this.eventSource.onopen = () => { this.updateStatus('Streaming live...'); - }; + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (this.isStreaming) { + const { done, value } = await reader.read(); + + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); + + for (const line of lines) { + if (line.trim()) { + try { + const data = JSON.parse(line); + if (data.error) { + console.error('Stream error:', data.error); + this.stopStreaming(); + this.updateStatus('Stream error'); + break; + } + if (data.line) { + this.appendLogLine(data.line); + } + } catch (e) { + console.error('Failed to parse line:', line, e); + } + } + } + } + + reader.releaseLock(); + + } catch (error) { + if (error.name === 'AbortError') { + console.log('Stream aborted'); + } else { + console.error('Stream error:', error); + this.updateStatus('Stream disconnected'); + } + } finally { + this.isStreaming = false; + this.updateStreamButton(); + } } stopStreaming() { - if (this.eventSource) { - this.eventSource.close(); - this.eventSource = null; - } this.isStreaming = false; + + if (this.streamController) { + this.streamController.abort(); + this.streamController = null; + } + this.updateStreamButton(); this.updateStatus('Stream stopped'); } appendLogLine(line) { - const pre = this.logsContent.querySelector('.logs-pre'); - if (pre) { - const formattedLine = this.formatLogLine(line); - pre.insertAdjacentHTML('beforeend', formattedLine); - - // Limit number of lines in memory - const lines = pre.querySelectorAll('.log-line'); - if (lines.length > 5000) { - lines[0].remove(); - } - - if (this.autoScroll) { - this.scrollToBottom(); - } - - this.updateLineCount(lines.length); + let pre = this.logsContent.querySelector('.logs-pre'); + + // Jeśli nie ma pre, utwórz go + if (!pre) { + this.logsContent.innerHTML = '
'; + pre = this.logsContent.querySelector('.logs-pre'); } - } + + if (pre) { + // Usuń trailing newline z linii + const cleanLine = line.replace(/\n$/, ''); + const formattedLine = this.formatLogLine(cleanLine); + pre.insertAdjacentHTML('beforeend', formattedLine); + + // Ogranicz liczbę linii w pamięci + const lines = pre.querySelectorAll('.log-line'); + if (lines.length > 5000) { + lines[0].remove(); + } + + if (this.autoScroll) { + this.scrollToBottom(); + } + + this.updateLineCount(lines.length); + } +} updateStreamButton() { const btn = document.getElementById('logs-stream-btn');