mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-01-06 15:42:06 +01:00
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:
@@ -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}
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
// 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'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;">×</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" %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user