fix stream logs

This commit is contained in:
Dockpeek
2025-10-18 19:20:28 +02:00
parent b1c2df00b4
commit d45d22ccf7
2 changed files with 114 additions and 46 deletions
+11 -8
View File
@@ -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',
+103 -38
View File
@@ -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 class="logs-pre"></pre>';
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');