|
@@ -0,0 +1,108 @@
|
|
|
|
|
+package com.aitori.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.aitori.config.DockerProperties;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.io.InputStreamReader;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class DockerLogService {
|
|
|
|
|
+
|
|
|
|
|
+ private final DockerProperties dockerProperties;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过 Docker Engine API 查询容器日志并按关键词过滤。
|
|
|
|
|
+ * 需要宿主机将 /var/run/docker.sock 挂载到容器内。
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<String> queryLogs(String containerName, String keyword, Integer tailLines) {
|
|
|
|
|
+ if (containerName == null || containerName.isBlank()) {
|
|
|
|
|
+ containerName = dockerProperties.getDefaultContainer();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tailLines == null || tailLines <= 0) {
|
|
|
|
|
+ tailLines = dockerProperties.getLogTailLines();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String socketPath = dockerProperties.getSocketPath();
|
|
|
|
|
+ if (!Files.exists(Path.of(socketPath))) {
|
|
|
|
|
+ throw new IllegalStateException(
|
|
|
|
|
+ "Docker socket 不存在: " + socketPath + ",请确认已挂载 -v /var/run/docker.sock:/var/run/docker.sock");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String url = String.format(
|
|
|
|
|
+ "http://localhost/v1.44/containers/%s/logs?stdout=true&stderr=true&tail=%d×tamps=true",
|
|
|
|
|
+ containerName, tailLines);
|
|
|
|
|
+
|
|
|
|
|
+ ProcessBuilder pb = new ProcessBuilder(
|
|
|
|
|
+ "curl", "--silent", "--no-buffer",
|
|
|
|
|
+ "--unix-socket", socketPath,
|
|
|
|
|
+ url
|
|
|
|
|
+ );
|
|
|
|
|
+ pb.redirectErrorStream(true);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ Process process = pb.start();
|
|
|
|
|
+ List<String> matchedLines = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ try (InputStream is = process.getInputStream();
|
|
|
|
|
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
|
|
|
|
+
|
|
|
|
|
+ String line;
|
|
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
|
|
+ String cleaned = stripDockerStreamHeader(line);
|
|
|
|
|
+ if (keyword == null || keyword.isBlank() || cleaned.contains(keyword)) {
|
|
|
|
|
+ matchedLines.add(cleaned);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ boolean finished = process.waitFor(30, TimeUnit.SECONDS);
|
|
|
|
|
+ if (!finished) {
|
|
|
|
|
+ process.destroyForcibly();
|
|
|
|
|
+ log.warn("查询Docker日志超时, container={}", containerName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int exitCode = finished ? process.exitValue() : -1;
|
|
|
|
|
+ if (exitCode != 0 && matchedLines.isEmpty()) {
|
|
|
|
|
+ log.error("curl 退出码={}, container={}", exitCode, containerName);
|
|
|
|
|
+ throw new RuntimeException("查询Docker日志失败,请检查容器名称是否正确: " + containerName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("查询Docker日志完成, container={}, keyword={}, matched={}", containerName, keyword, matchedLines.size());
|
|
|
|
|
+ return matchedLines;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (InterruptedException e) {
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ throw new RuntimeException("查询Docker日志被中断", e);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("查询Docker日志异常, container={}, keyword={}", containerName, keyword, e);
|
|
|
|
|
+ throw new RuntimeException("查询Docker日志异常: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Docker 非 TTY 模式日志的每一帧带有 8 字节二进制头,
|
|
|
|
|
+ * 这里尝试剥离行首的不可打印字符。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String stripDockerStreamHeader(String line) {
|
|
|
|
|
+ if (line == null || line.isEmpty()) {
|
|
|
|
|
+ return line;
|
|
|
|
|
+ }
|
|
|
|
|
+ int start = 0;
|
|
|
|
|
+ while (start < line.length() && (line.charAt(start) < 0x20 && line.charAt(start) != '\t')) {
|
|
|
|
|
+ start++;
|
|
|
|
|
+ }
|
|
|
|
|
+ return start > 0 ? line.substring(start) : line;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|