fix: use docker ps labels to list services and fetch logs

Replace compose-file-path-dependent `docker compose -f <path>` calls
with label-based `docker ps` queries so the ops log viewer works on
both public cloud and private deployments regardless of compose file
location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-05-22 23:43:39 +08:00
父节点 5e788fe26b
当前提交 26261263a0

查看文件

@ -60,15 +60,16 @@ public class SystemUpdateService {
// 公开接口 // 公开接口
/** /**
* 返回当前正在运行的服务名列表docker compose ps --filter status=running * 返回当前正在运行的服务名列表
* 结果已过滤为 ALLOWED_LOG_SERVICES 白名单内的服务防止枚举非预期服务 * 使用 docker ps --format "{{.Label \"com.docker.compose.service\"}}" 枚举运行中容器的服务标签
* 不依赖 compose 文件路径公有云/私有云均可用
* 结果已过滤为 ALLOWED_LOG_SERVICES 白名单内的服务
*/ */
public List<String> getRunningServices() { public List<String> getRunningServices() {
String composeFile = deployRoot + "/docker-compose.yml";
try { try {
Process p = new ProcessBuilder( Process p = new ProcessBuilder(
"docker", "compose", "-f", composeFile, "docker", "ps",
"ps", "--services", "--filter", "status=running" "--format", "{{.Label \"com.docker.compose.service\"}}"
).redirectErrorStream(true).start(); ).redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
p.waitFor(); p.waitFor();
@ -76,6 +77,7 @@ public class SystemUpdateService {
return Arrays.stream(out.split("\n")) return Arrays.stream(out.split("\n"))
.map(String::trim) .map(String::trim)
.filter(s -> !s.isEmpty() && ALLOWED_LOG_SERVICES.contains(s)) .filter(s -> !s.isEmpty() && ALLOWED_LOG_SERVICES.contains(s))
.distinct()
.collect(Collectors.toList()); .collect(Collectors.toList());
} catch (Exception e) { } catch (Exception e) {
log.error("failed to list running services", e); log.error("failed to list running services", e);
@ -85,23 +87,36 @@ public class SystemUpdateService {
/** /**
* 获取指定服务最近 N 行日志上限 1000 * 获取指定服务最近 N 行日志上限 1000
* service 名称校验白名单防止注入 * 通过 docker ps 标签找到容器 ID 后使用 docker logs不依赖 compose 文件路径
*/ */
public String getServiceLogs(String service, int lines) { public String getServiceLogs(String service, int lines) {
if (!ALLOWED_LOG_SERVICES.contains(service)) { if (!ALLOWED_LOG_SERVICES.contains(service)) {
throw new IllegalArgumentException("不允许查看此服务的日志: " + service); throw new IllegalArgumentException("不允许查看此服务的日志: " + service);
} }
int safeLines = Math.min(Math.max(lines, 10), 1000); int safeLines = Math.min(Math.max(lines, 10), 1000);
String composeFile = deployRoot + "/docker-compose.yml";
try { try {
Process p = new ProcessBuilder( // 找到对应服务的容器 ID可能有多个取第一个
"docker", "compose", "-f", composeFile, Process psProc = new ProcessBuilder(
"logs", "--tail", String.valueOf(safeLines), "--no-color", service "docker", "ps",
"--filter", "label=com.docker.compose.service=" + service,
"--format", "{{.ID}}"
).redirectErrorStream(true).start(); ).redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8); String containerId = new String(psProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
int exitCode = p.waitFor(); psProc.waitFor();
if (containerId.isEmpty()) {
return "(服务 " + service + " 当前没有运行中的容器)";
}
// 取第一行如有多个容器
String firstId = containerId.split("\n")[0].trim();
Process logsProc = new ProcessBuilder(
"docker", "logs", "--tail", String.valueOf(safeLines), "--timestamps", firstId
).redirectErrorStream(true).start();
String out = new String(logsProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
int exitCode = logsProc.waitFor();
if (exitCode != 0 && out.isBlank()) { if (exitCode != 0 && out.isBlank()) {
throw new RuntimeException("docker compose logs 返回非零退出码: " + exitCode); throw new RuntimeException("docker logs 返回非零退出码: " + exitCode);
} }
return out; return out;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {