Enhance container log retrieval and display: Implement formatted log retrieval with timestamps and improved error handling in ContainerManager. Update frontend to support log formatting, auto-scrolling, and additional log controls. Modify container command execution to temporarily start stopped containers, ensuring better user feedback on command execution status.

This commit is contained in:
Master3395
2025-09-20 21:14:12 +02:00
parent 5364e3e7d1
commit 70e76967ec
4 changed files with 265 additions and 24 deletions

View File

@@ -270,15 +270,72 @@ class ContainerManager(multi.Thread):
dockerAPI = docker.APIClient()
container = client.containers.get(name)
logs = container.logs().decode("utf-8")
# Get logs with proper formatting
try:
# Get logs with timestamps and proper formatting
logs = container.logs(
stdout=True,
stderr=True,
timestamps=True,
tail=1000 # Limit to last 1000 lines for performance
).decode("utf-8", errors='replace')
# Clean up the logs for better display
if logs:
# Split into lines and clean up
log_lines = logs.split('\n')
cleaned_lines = []
for line in log_lines:
# Remove Docker's log prefix if present
if line.startswith('[') and ']' in line:
# Extract timestamp and message
try:
timestamp_end = line.find(']')
if timestamp_end > 0:
timestamp = line[1:timestamp_end]
message = line[timestamp_end + 1:].strip()
# Format the line nicely
if message:
cleaned_lines.append(f"[{timestamp}] {message}")
else:
cleaned_lines.append(line)
except:
cleaned_lines.append(line)
else:
cleaned_lines.append(line)
logs = '\n'.join(cleaned_lines)
else:
logs = "No logs available for this container."
except Exception as log_err:
# Fallback to basic logs if timestamped logs fail
try:
logs = container.logs().decode("utf-8", errors='replace')
if not logs:
logs = "No logs available for this container."
except:
logs = f"Error retrieving logs: {str(log_err)}"
data_ret = {'containerLogStatus': 1, 'containerLog': logs, 'error_message': "None"}
json_data = json.dumps(data_ret)
data_ret = {
'containerLogStatus': 1,
'containerLog': logs,
'error_message': "None",
'container_status': container.status,
'log_count': len(logs.split('\n')) if logs else 0
}
json_data = json.dumps(data_ret, ensure_ascii=False)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'containerLogStatus': 0, 'containerLog': 'Error', 'error_message': str(msg)}
data_ret = {
'containerLogStatus': 0,
'containerLog': 'Error retrieving logs',
'error_message': str(msg)
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@@ -1555,10 +1612,26 @@ class ContainerManager(multi.Thread):
data_ret = {'commandStatus': 0, 'error_message': f'Error accessing container: {str(err)}'}
return HttpResponse(json.dumps(data_ret))
# Check if container is running
# Handle container status - try to start if not running
container_was_stopped = False
if container.status != 'running':
data_ret = {'commandStatus': 0, 'error_message': 'Container must be running to execute commands'}
return HttpResponse(json.dumps(data_ret))
try:
# Try to start the container temporarily
container.start()
container_was_stopped = True
# Wait a moment for container to fully start
import time
time.sleep(2)
# Verify container is now running
container.reload()
if container.status != 'running':
data_ret = {'commandStatus': 0, 'error_message': 'Failed to start container for command execution'}
return HttpResponse(json.dumps(data_ret))
except Exception as start_err:
data_ret = {'commandStatus': 0, 'error_message': f'Container is not running and cannot be started: {str(start_err)}'}
return HttpResponse(json.dumps(data_ret))
# Log the command execution attempt
self._log_command_execution(userID, name, command)
@@ -1599,6 +1672,14 @@ class ContainerManager(multi.Thread):
# Log successful execution
self._log_command_result(userID, name, command, exit_code, len(output))
# Stop container if it was started temporarily
if container_was_stopped:
try:
container.stop()
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {name} after command execution')
except Exception as stop_err:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not stop container {name} after command execution: {str(stop_err)}')
# Format the response
data_ret = {
'commandStatus': 1,
@@ -1606,17 +1687,34 @@ class ContainerManager(multi.Thread):
'output': output,
'exit_code': exit_code,
'command': command,
'timestamp': time.time()
'timestamp': time.time(),
'container_was_started': container_was_stopped
}
return HttpResponse(json.dumps(data_ret, ensure_ascii=False))
except docker.errors.APIError as err:
# Stop container if it was started temporarily
if container_was_stopped:
try:
container.stop()
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {name} after API error')
except Exception as stop_err:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not stop container {name} after API error: {str(stop_err)}')
error_msg = f'Docker API error: {str(err)}'
self._log_command_error(userID, name, command, error_msg)
data_ret = {'commandStatus': 0, 'error_message': error_msg}
return HttpResponse(json.dumps(data_ret))
except Exception as err:
# Stop container if it was started temporarily
if container_was_stopped:
try:
container.stop()
logging.CyberCPLogFileWriter.writeToFile(f'Stopped container {name} after execution error')
except Exception as stop_err:
logging.CyberCPLogFileWriter.writeToFile(f'Warning: Could not stop container {name} after execution error: {str(stop_err)}')
error_msg = f'Execution error: {str(err)}'
self._log_command_error(userID, name, command, error_msg)
data_ret = {'commandStatus': 0, 'error_message': error_msg}

View File

@@ -1095,6 +1095,9 @@ app.controller('listContainers', function ($scope, $http) {
$scope.showLog = function (name, refresh = false) {
$scope.logs = "";
$scope.logInfo = null;
$scope.formattedLogs = "";
if (refresh === false) {
$('#logs').modal('show');
$scope.activeLog = name;
@@ -1122,18 +1125,37 @@ app.controller('listContainers', function ($scope, $http) {
if (response.data.containerLogStatus === 1) {
$scope.logs = response.data.containerLog;
$scope.logInfo = {
container_status: response.data.container_status,
log_count: response.data.log_count
};
// Format logs for better display
$scope.formatLogs();
// Auto-scroll to bottom
setTimeout(function() {
$scope.scrollToBottom();
}, 100);
}
else {
$scope.logs = response.data.error_message;
$scope.logInfo = null;
$scope.formattedLogs = "";
new PNotify({
title: 'Unable to complete request',
text: response.data.error_message,
type: 'error'
});
}
}
function cantLoadInitialData(response) {
$scope.logs = "Error loading logs";
$scope.logInfo = null;
$scope.formattedLogs = "";
new PNotify({
title: 'Unable to complete request',
type: 'error'
@@ -1141,6 +1163,76 @@ app.controller('listContainers', function ($scope, $http) {
}
};
// Format logs with syntax highlighting and better readability
$scope.formatLogs = function() {
if (!$scope.logs || $scope.logs === 'Loading...') {
$scope.formattedLogs = $scope.logs;
return;
}
var lines = $scope.logs.split('\n');
var formattedLines = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var formattedLine = line;
// Escape HTML characters
formattedLine = formattedLine.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// Add syntax highlighting for common log patterns
if (line.match(/^\[.*?\]/)) {
// Timestamp lines
formattedLine = '<span style="color: #569cd6;">' + formattedLine + '</span>';
} else if (line.match(/ERROR|FATAL|CRITICAL/i)) {
// Error lines
formattedLine = '<span style="color: #f44747; font-weight: bold;">' + formattedLine + '</span>';
} else if (line.match(/WARN|WARNING/i)) {
// Warning lines
formattedLine = '<span style="color: #ffcc02; font-weight: bold;">' + formattedLine + '</span>';
} else if (line.match(/INFO/i)) {
// Info lines
formattedLine = '<span style="color: #4ec9b0;">' + formattedLine + '</span>';
} else if (line.match(/DEBUG/i)) {
// Debug lines
formattedLine = '<span style="color: #9cdcfe;">' + formattedLine + '</span>';
} else if (line.match(/SUCCESS|OK|COMPLETED/i)) {
// Success lines
formattedLine = '<span style="color: #4caf50; font-weight: bold;">' + formattedLine + '</span>';
}
formattedLines.push(formattedLine);
}
$scope.formattedLogs = formattedLines.join('\n');
};
// Scroll functions
$scope.scrollToTop = function() {
var container = document.getElementById('logContainer');
if (container) {
container.scrollTop = 0;
}
};
$scope.scrollToBottom = function() {
var container = document.getElementById('logContainer');
if (container) {
container.scrollTop = container.scrollHeight;
}
};
// Clear logs function
$scope.clearLogs = function() {
$scope.logs = "";
$scope.formattedLogs = "";
$scope.logInfo = null;
};
url = "/docker/getContainerList";
var data = {page: 1};
@@ -2080,13 +2172,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.commandOutput = {
command: response.data.command,
output: response.data.output,
exit_code: response.data.exit_code
exit_code: response.data.exit_code,
container_was_started: response.data.container_was_started
};
// Add to command history
$scope.commandHistory.unshift({
command: response.data.command,
timestamp: new Date()
timestamp: new Date(),
container_was_started: response.data.container_was_started
});
// Keep only last 10 commands
@@ -2094,10 +2188,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.commandHistory = $scope.commandHistory.slice(0, 10);
}
// Show success notification
// Show success notification with container status info
var notificationText = 'Command completed with exit code: ' + response.data.exit_code;
if (response.data.container_was_started) {
notificationText += ' (Container was temporarily started and stopped)';
}
new PNotify({
title: 'Command Executed',
text: 'Command completed with exit code: ' + response.data.exit_code,
text: notificationText,
type: response.data.exit_code === 0 ? 'success' : 'warning'
});
}

View File

@@ -766,25 +766,61 @@
<!-- Container Logs Modal -->
<div id="logs" class="modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-file-alt" style="margin-right: 0.5rem;"></i>
{% trans "Container Logs" %}
<small class="text-muted" ng-if="logInfo">
- Status: <span class="badge badge-info">{$ logInfo.container_status $}</span>
<span ng-if="logInfo.log_count"> | Lines: <span class="badge badge-secondary">{$ logInfo.log_count $}</span></span>
</small>
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: transparent; border: none;">&times;</button>
</div>
<div class="modal-body">
<textarea name="logs" class="form-control" style="font-family: monospace; height: 400px; resize: vertical;"
readonly>{$ logs $}</textarea>
<div class="modal-body" style="padding: 0;">
<!-- Log Controls -->
<div class="bg-light p-2 border-bottom">
<div class="row align-items-center">
<div class="col-md-6">
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary" ng-click="showLog('', true)">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" ng-click="scrollToTop()">
<i class="fas fa-arrow-up"></i> Top
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" ng-click="scrollToBottom()">
<i class="fas fa-arrow-down"></i> Bottom
</button>
<button type="button" class="btn btn-sm btn-outline-info" ng-click="clearLogs()">
<i class="fas fa-trash"></i> Clear
</button>
</div>
</div>
<div class="col-md-6 text-right">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Use Ctrl+F to search within logs
</small>
</div>
</div>
</div>
<!-- Log Display -->
<div id="logContainer" style="height: 500px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 15px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word;">
<div ng-if="logs === 'Loading...'" class="text-center text-muted">
<i class="fas fa-spinner fa-spin"></i> Loading logs...
</div>
<div ng-if="logs && logs !== 'Loading...'" ng-bind-html="formattedLogs"></div>
<div ng-if="!logs || logs === ''" class="text-center text-muted">
<i class="fas fa-file-alt"></i> No logs available
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="showLog('', true)">
<i class="fas fa-sync-alt"></i>
{% trans "Refresh" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fas fa-times"></i>
{% trans "Close" %}

View File

@@ -1104,7 +1104,7 @@
</div>
</div>
<small class="form-text text-muted">
{% trans "Commands will be executed inside the running container. Use proper shell syntax." %}
{% trans "Commands will be executed inside the container. If the container is not running, it will be temporarily started for command execution." %}
</small>
</div>
@@ -1120,7 +1120,12 @@
ng-click="selectCommand(cmd.command)"
style="cursor: pointer; padding: 0.25rem 0.5rem; margin: 0.125rem 0; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #007bff;">
<code style="font-size: 0.875rem;">{{ cmd.command }}</code>
<small class="text-muted" style="float: right;">{{ cmd.timestamp | date:'short' }}</small>
<small class="text-muted" style="float: right;">
{{ cmd.timestamp | date:'short' }}
<span ng-if="cmd.container_was_started" style="color: #f6ad55; margin-left: 0.5rem;">
<i class="fas fa-play-circle"></i>
</span>
</small>
</div>
</div>
</div>
@@ -1135,6 +1140,9 @@
<div ng-show="commandOutput.exit_code !== undefined" style="margin-bottom: 0.5rem;">
<span style="color: #68d391;">$</span> <span style="color: #fbb6ce;">{{ commandOutput.command }}</span>
<span style="color: #a0aec0; margin-left: 1rem;">(exit code: {{ commandOutput.exit_code }})</span>
<span ng-if="commandOutput.container_was_started" style="color: #f6ad55; margin-left: 1rem;">
<i class="fas fa-info-circle"></i> Container was temporarily started
</span>
</div>
<div ng-bind="commandOutput.output"></div>
</div>