添加容器日志

This commit is contained in:
孙小云 2026-02-03 16:18:08 +08:00
parent 80304fcfa8
commit 7b90688164
1 changed files with 346 additions and 3 deletions

View File

@ -749,6 +749,31 @@ class DeploymentServer:
self.app = Flask(__name__) self.app = Flask(__name__)
self._setup_routes() self._setup_routes()
def get_all_containers(self):
"""获取所有Docker容器列表"""
try:
cmd = "docker ps --format '{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return []
containers = []
for line in result.stdout.strip().split('\n'):
if line:
parts = line.split('|')
if len(parts) >= 4:
containers.append({
'id': parts[0],
'name': parts[1],
'status': parts[2],
'image': parts[3]
})
return containers
except Exception as e:
Logger.error(f"获取容器列表失败: {e}")
return []
def _setup_routes(self): def _setup_routes(self):
"""设置路由""" """设置路由"""
@ -834,7 +859,7 @@ class DeploymentServer:
<h3>📖 使用说明</h3> <h3>📖 使用说明</h3>
<p><strong>触发部署</strong>点击下方表格中的"部署链接"或直接访问 <code>http://IP:9999/项目名称</code></p> <p><strong>触发部署</strong>点击下方表格中的"部署链接"或直接访问 <code>http://IP:9999/项目名称</code></p>
<p><strong>示例</strong><code>http://IP:9999/tuoheng-device</code> 触发 tuoheng-device 项目部署</p> <p><strong>示例</strong><code>http://IP:9999/tuoheng-device</code> 触发 tuoheng-device 项目部署</p>
<p><strong>查看日志</strong><a href="/logs" style="color: #1976d2; font-weight: bold;">点击这里查看部署日志</a></p> <p><strong>查看日志</strong><a href="/logs" style="color: #1976d2; font-weight: bold;">点击这里查看部署日志</a> | <a href="/containers" style="color: #1976d2; font-weight: bold;">查看容器日志</a></p>
</div> </div>
<h2>📦 可部署项目列表</h2> <h2>📦 可部署项目列表</h2>
<table> <table>
@ -1063,11 +1088,329 @@ class DeploymentServer:
except Exception as e: except Exception as e:
return jsonify({'logs': [], 'error': str(e)}) return jsonify({'logs': [], 'error': str(e)})
@self.app.route('/containers')
def view_containers():
"""容器列表页面"""
containers = self.get_all_containers()
html = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Docker 容器管理</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1400px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
text-align: center;
}
.info {
background-color: #e3f2fd;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.back-link {
color: #1976d2;
text-decoration: none;
font-weight: bold;
}
.back-link:hover {
text-decoration: underline;
}
table {
width: 100%;
border-collapse: collapse;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #1976d2;
color: white;
}
tr:hover {
background-color: #f5f5f5;
}
.container-link {
color: #1976d2;
text-decoration: none;
font-weight: bold;
}
.container-link:hover {
text-decoration: underline;
}
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
background-color: #4caf50;
color: white;
}
</style>
</head>
<body>
<h1>🐳 Docker 容器管理</h1>
<div class="info">
<a href="/" class="back-link"> 返回首页</a>
<p><strong>说明</strong>点击容器名称查看该容器的实时日志</p>
</div>
<h2>📋 运行中的容器列表</h2>
<table>
<thead>
<tr>
<th>容器ID</th>
<th>容器名称</th>
<th>状态</th>
<th>镜像</th>
<th>操作</th>
</tr>
</thead>
<tbody>
'''
for container in containers:
html += f'''
<tr>
<td>{container['id'][:12]}</td>
<td>{container['name']}</td>
<td><span class="status-badge">{container['status']}</span></td>
<td>{container['image']}</td>
<td><a href="/container-logs/{container['name']}" class="container-link">查看日志</a></td>
</tr>
'''
if not containers:
html += '''
<tr>
<td colspan="5" style="text-align: center; padding: 20px; color: #999;">
暂无运行中的容器
</td>
</tr>
'''
html += '''
</tbody>
</table>
</body>
</html>
'''
return html
@self.app.route('/container-logs/<container_name>')
def view_container_logs(container_name):
"""容器日志查看页面"""
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>容器日志 - {container_name}</title>
<style>
body {{
font-family: 'Courier New', monospace;
margin: 0;
padding: 20px;
background-color: #1e1e1e;
color: #d4d4d4;
}}
.header {{
background-color: #2d2d30;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}}
h1 {{
margin: 0;
color: #4ec9b0;
font-size: 24px;
}}
.controls {{
display: flex;
gap: 10px;
}}
button {{
background-color: #0e639c;
color: white;
border: none;
padding: 8px 16px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}}
button:hover {{
background-color: #1177bb;
}}
.log-container {{
background-color: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 5px;
padding: 15px;
height: calc(100vh - 180px);
overflow-y: auto;
font-size: 13px;
line-height: 1.6;
}}
.log-line {{
margin: 2px 0;
white-space: pre-wrap;
word-wrap: break-word;
}}
.log-error {{
color: #f48771;
}}
.log-warning {{
color: #dcdcaa;
}}
.log-info {{
color: #4ec9b0;
}}
.loading {{
text-align: center;
padding: 20px;
color: #858585;
}}
.back-link {{
color: #4ec9b0;
text-decoration: none;
font-size: 14px;
}}
.back-link:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
<div class="header">
<div>
<h1>📋 容器日志: {container_name}</h1>
<a href="/containers" class="back-link"> 返回容器列表</a>
</div>
<div class="controls">
<button onclick="refreshLogs()">🔄 刷新</button>
<button onclick="clearDisplay()">🗑 清空显示</button>
<button onclick="toggleAutoRefresh()"> <span id="autoRefreshText">开启自动刷新</span></button>
</div>
</div>
<div class="log-container" id="logContainer">
<div class="loading">正在加载日志...</div>
</div>
<script>
let autoRefreshInterval = null;
let isAutoRefresh = false;
const containerName = '{container_name}';
function formatLogLine(line) {{
if (line.includes('ERROR') || line.includes('error') || line.includes('Exception') || line.includes('Failed')) {{
return '<div class="log-line log-error">' + escapeHtml(line) + '</div>';
}} else if (line.includes('WARN') || line.includes('warn') || line.includes('WARNING')) {{
return '<div class="log-line log-warning">' + escapeHtml(line) + '</div>';
}} else if (line.includes('INFO') || line.includes('info')) {{
return '<div class="log-line log-info">' + escapeHtml(line) + '</div>';
}} else {{
return '<div class="log-line">' + escapeHtml(line) + '</div>';
}}
}}
function escapeHtml(text) {{
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}}
function refreshLogs() {{
fetch('/api/container-logs/' + containerName)
.then(response => response.json())
.then(data => {{
const container = document.getElementById('logContainer');
if (data.logs && data.logs.length > 0) {{
container.innerHTML = data.logs.map(formatLogLine).join('');
container.scrollTop = container.scrollHeight;
}} else if (data.error) {{
container.innerHTML = '<div class="log-line log-error">获取日志失败: ' + data.error + '</div>';
}} else {{
container.innerHTML = '<div class="loading">暂无日志</div>';
}}
}})
.catch(error => {{
console.error('获取日志失败:', error);
document.getElementById('logContainer').innerHTML =
'<div class="log-line log-error">获取日志失败: ' + error.message + '</div>';
}});
}}
function clearDisplay() {{
document.getElementById('logContainer').innerHTML = '<div class="loading">显示已清空,点击刷新重新加载</div>';
}}
function toggleAutoRefresh() {{
isAutoRefresh = !isAutoRefresh;
const text = document.getElementById('autoRefreshText');
if (isAutoRefresh) {{
text.textContent = '关闭自动刷新';
autoRefreshInterval = setInterval(refreshLogs, 3000);
refreshLogs();
}} else {{
text.textContent = '开启自动刷新';
if (autoRefreshInterval) {{
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}}
}}
}}
// 页面加载时获取日志
refreshLogs();
</script>
</body>
</html>
'''
return html
@self.app.route('/api/container-logs/<container_name>')
def get_container_logs(container_name):
"""获取容器日志API"""
try:
cmd = f"docker logs --tail 500 {container_name}"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return jsonify({'logs': [], 'error': f'获取容器日志失败: {result.stderr}'})
# 合并 stdout 和 stderr
logs = []
if result.stdout:
logs.extend(result.stdout.strip().split('\n'))
if result.stderr:
logs.extend(result.stderr.strip().split('\n'))
return jsonify({'logs': logs})
except Exception as e:
return jsonify({'logs': [], 'error': str(e)})
@self.app.route('/<project_name>') @self.app.route('/<project_name>')
def deploy_project(project_name): def deploy_project(project_name):
"""触发项目部署""" """触发项目部署"""
# 避免与 /logs 路由冲突 # 避免与其他路由冲突
if project_name == 'logs' or project_name == 'api': if project_name in ['logs', 'api', 'containers', 'container-logs']:
return jsonify({'code': 404, 'message': '路径不存在'}) return jsonify({'code': 404, 'message': '路径不存在'})
Logger.info(f"收到HTTP部署请求: {project_name}") Logger.info(f"收到HTTP部署请求: {project_name}")