Files
project-panel/routes_docker.py
hz4th_coder 78a4a3f1aa feat: Docker 监控面板
后端 API (routes_docker.py):
- /api/docker/containers - 容器列表(状态+资源stats: CPU/内存/网络I/O/磁盘I/O/进程数)
- /api/docker/container/<id>/stats - 单容器实时资源
- /api/docker/images - 镜像列表(大小/时间/悬空标记)
- /api/docker/system - 系统信息(版本/磁盘占用)
- /api/docker/container/<id>/start|stop|restart - 容器控制
- /api/docker/container/<id>/logs - 容器日志
- /api/docker/prune - 资源清理

前端 Docker 监控 Tab:
- 统计卡片: 总容器/运行中/已停止/镜像数/磁盘占用/可回收
- 容器列表: 状态灯+CPU/内存/NET IO/磁盘IO/端口/运行时间
- 操作按钮: 启动/停止/重启/查看日志
- 镜像列表: 有效镜像+悬空镜像分区显示
- 清理: 容器/镜像/全部一键清理
2026-06-01 18:01:17 +08:00

244 lines
9.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Docker 监控 API 路由
"""
import subprocess
import json
from flask import jsonify, request
from utils import log_message
def _run_docker(cmd, timeout=10):
"""执行 docker 命令并返回解析结果"""
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
if result.returncode != 0:
return None, result.stderr.strip()
return result.stdout.strip(), None
except subprocess.TimeoutExpired:
return None, '命令超时'
except FileNotFoundError:
return None, 'Docker 未安装'
except Exception as e:
return None, str(e)
def register_docker_routes(app):
"""注册 Docker 监控相关路由"""
@app.route('/api/docker/containers')
def api_docker_containers():
"""获取所有容器列表(含资源使用)"""
# 获取容器列表
fmt = '{{.ID}}|{{.Names}}|{{.Image}}|{{.State}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}|{{.RunningFor}}'
out, err = _run_docker(['docker', 'ps', '-a', '--format', fmt, '--no-trunc'])
if out is None:
return jsonify({'error': err, 'containers': []})
containers = []
for line in out.split('\n'):
if not line.strip():
continue
parts = line.split('|')
if len(parts) < 8:
continue
cid = parts[0][:12] # 缩短ID
containers.append({
'id': cid,
'name': parts[1],
'image': parts[2],
'state': parts[3],
'status': parts[4],
'ports': parts[5],
'created': parts[6],
'runningFor': parts[7]
})
# 批量获取运行中容器的stats
running = [c for c in containers if c['state'] == 'running']
if running:
try:
stats_out, _ = _run_docker([
'docker', 'stats', '--no-stream', '--no-trunc',
'--format', '{{.Name}}|{{.CPUPerc}}|{{.MemPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}'
], timeout=15)
if stats_out:
stats_map = {}
for line in stats_out.split('\n'):
if not line.strip():
continue
sp = line.split('|')
if len(sp) >= 7:
stats_map[sp[0]] = {
'cpuPercent': sp[1],
'memPercent': sp[2],
'memUsage': sp[3],
'netIO': sp[4],
'blockIO': sp[5],
'pids': sp[6]
}
for c in containers:
if c['name'] in stats_map:
c['stats'] = stats_map[c['name']]
except:
pass
return jsonify({'containers': containers})
@app.route('/api/docker/container/<container_id>/stats')
def api_docker_container_stats(container_id):
"""获取单个容器实时资源统计"""
out, err = _run_docker([
'docker', 'stats', '--no-stream', '--no-trunc',
'--format', '{{.CPUPerc}}|{{.MemPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}',
container_id
], timeout=10)
if out is None:
return jsonify({'error': err}), 500
if not out:
return jsonify({'error': '容器不存在或未运行'}), 404
parts = out.split('|')
return jsonify({
'cpuPercent': parts[0] if len(parts) > 0 else '0%',
'memPercent': parts[1] if len(parts) > 1 else '0%',
'memUsage': parts[2] if len(parts) > 2 else '',
'netIO': parts[3] if len(parts) > 3 else '',
'blockIO': parts[4] if len(parts) > 4 else '',
'pids': parts[5] if len(parts) > 5 else '0'
})
@app.route('/api/docker/images')
def api_docker_images():
"""获取所有镜像列表"""
fmt = '{{.Repository}}|{{.Tag}}|{{.ID}}|{{.Size}}|{{.CreatedAt}}|{{.Containers}}'
out, err = _run_docker(['docker', 'images', '--format', fmt, '--no-trunc'])
if out is None:
return jsonify({'error': err, 'images': []})
images = []
for line in out.split('\n'):
if not line.strip():
continue
parts = line.split('|')
if len(parts) < 6:
continue
repo = parts[0] if parts[0] != '<none>' else ''
tag = parts[1] if parts[1] != '<none>' else ''
images.append({
'repository': repo,
'tag': tag,
'id': parts[2][:12],
'size': parts[3],
'created': parts[4],
'containers': parts[5],
'dangling': not repo and not tag
})
return jsonify({'images': images})
@app.route('/api/docker/system')
def api_docker_system():
"""获取 Docker 系统信息"""
# 版本信息
ver_out, _ = _run_docker(['docker', 'version', '--format', '{{.Server.Version}}|{{.Server.Os}}|{{.Server.Arch}}'], timeout=5)
# 磁盘使用
df_out, _ = _run_docker(['docker', 'system', 'df', '--format', '{{.Type}}|{{.TotalCount}}|{{.Active}}|{{.Size}}|{{.Reclaimable}}'], timeout=10)
system_info = {
'version': '',
'os': '',
'arch': '',
'diskUsage': []
}
if ver_out:
ver_parts = ver_out.split('|')
if len(ver_parts) >= 3:
system_info.update({
'version': ver_parts[0],
'os': ver_parts[1],
'arch': ver_parts[2]
})
if df_out:
for line in df_out.split('\n'):
if not line.strip():
continue
parts = line.split('|')
if len(parts) >= 5:
system_info['diskUsage'].append({
'type': parts[0],
'total': parts[1],
'active': parts[2],
'size': parts[3],
'reclaimable': parts[4]
})
return jsonify(system_info)
@app.route('/api/docker/container/<container_id>/logs')
def api_docker_container_logs(container_id):
"""获取容器日志最近200行"""
lines = int(request.args.get('lines', 200))
lines = max(10, min(1000, lines))
out, err = _run_docker(['docker', 'logs', '--tail', str(lines), container_id], timeout=10)
if out is None:
return jsonify({'error': err}), 500
return jsonify({'logs': out, 'lines': lines})
@app.route('/api/docker/container/<container_id>/start', methods=['POST'])
def api_docker_container_start(container_id):
"""启动容器"""
out, err = _run_docker(['docker', 'start', container_id])
if out is None and err:
return jsonify({'error': err}), 500
log_message(f"Docker 容器启动: {container_id}")
return jsonify({'message': f'容器 {container_id} 已启动'})
@app.route('/api/docker/container/<container_id>/stop', methods=['POST'])
def api_docker_container_stop(container_id):
"""停止容器"""
out, err = _run_docker(['docker', 'stop', container_id])
if out is None and err:
return jsonify({'error': err}), 500
log_message(f"Docker 容器停止: {container_id}")
return jsonify({'message': f'容器 {container_id} 已停止'})
@app.route('/api/docker/container/<container_id>/restart', methods=['POST'])
def api_docker_container_restart(container_id):
"""重启容器"""
out, err = _run_docker(['docker', 'restart', container_id])
if out is None and err:
return jsonify({'error': err}), 500
log_message(f"Docker 容器重启: {container_id}")
return jsonify({'message': f'容器 {container_id} 已重启'})
@app.route('/api/docker/prune', methods=['POST'])
def api_docker_prune():
"""清理Docker资源"""
target = request.json.get('target', 'all') if request.json else 'all'
results = {}
# 清理停止的容器
if target in ('all', 'containers'):
out, _ = _run_docker(['docker', 'container', 'prune', '-f'], timeout=30)
results['containers'] = out or '已清理'
# 清理悬空镜像
if target in ('all', 'images'):
out, _ = _run_docker(['docker', 'image', 'prune', '-f'], timeout=60)
results['images'] = out or '已清理'
# 清理构建缓存
if target in ('all', 'build'):
out, _ = _run_docker(['docker', 'builder', 'prune', '-f'], timeout=60)
results['build'] = out or '已清理'
log_message(f"Docker 资源清理完成: {target}")
return jsonify({'message': '清理完成', 'results': results})