mirror of
https://github.com/dockpeek/dockpeek.git
synced 2026-05-13 20:08:39 -05:00
fix stream logs
This commit is contained in:
+11
-8
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user