|
@@ -5,15 +5,17 @@ import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
-import java.io.BufferedReader;
|
|
|
|
|
-import java.io.InputStream;
|
|
|
|
|
-import java.io.InputStreamReader;
|
|
|
|
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.net.StandardProtocolFamily;
|
|
|
|
|
+import java.net.UnixDomainSocketAddress;
|
|
|
|
|
+import java.nio.ByteBuffer;
|
|
|
|
|
+import java.nio.channels.SocketChannel;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Path;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
-import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Service
|
|
@Service
|
|
@@ -23,7 +25,7 @@ public class DockerLogService {
|
|
|
private final DockerProperties dockerProperties;
|
|
private final DockerProperties dockerProperties;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 通过 Docker Engine API 查询容器日志并按关键词过滤。
|
|
|
|
|
|
|
+ * 通过 Docker Engine API (Unix Socket) 查询容器日志并按关键词过滤。
|
|
|
* 需要宿主机将 /var/run/docker.sock 挂载到容器内。
|
|
* 需要宿主机将 /var/run/docker.sock 挂载到容器内。
|
|
|
*/
|
|
*/
|
|
|
public List<String> queryLogs(String containerName, String keyword, Integer tailLines) {
|
|
public List<String> queryLogs(String containerName, String keyword, Integer tailLines) {
|
|
@@ -35,74 +37,108 @@ public class DockerLogService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
String socketPath = dockerProperties.getSocketPath();
|
|
String socketPath = dockerProperties.getSocketPath();
|
|
|
- if (!Files.exists(Path.of(socketPath))) {
|
|
|
|
|
|
|
+ Path sockFile = Path.of(socketPath);
|
|
|
|
|
+ if (!Files.exists(sockFile)) {
|
|
|
throw new IllegalStateException(
|
|
throw new IllegalStateException(
|
|
|
"Docker socket 不存在: " + socketPath + ",请确认已挂载 -v /var/run/docker.sock:/var/run/docker.sock");
|
|
"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",
|
|
|
|
|
|
|
+ String requestPath = String.format(
|
|
|
|
|
+ "/v1.44/containers/%s/logs?stdout=true&stderr=true&tail=%d×tamps=true",
|
|
|
containerName, tailLines);
|
|
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);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ byte[] rawResponse = callDockerApi(sockFile, requestPath);
|
|
|
|
|
+ String body = extractHttpBody(rawResponse);
|
|
|
|
|
+
|
|
|
|
|
+ List<String> matchedLines = new ArrayList<>();
|
|
|
|
|
+ for (String line : body.split("\n")) {
|
|
|
|
|
+ String cleaned = stripNonPrintable(line).trim();
|
|
|
|
|
+ if (cleaned.isEmpty()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (keyword == null || keyword.isBlank() || cleaned.contains(keyword)) {
|
|
|
|
|
+ matchedLines.add(cleaned);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("查询Docker日志完成, container={}, keyword={}, matched={}", containerName, keyword, matchedLines.size());
|
|
|
|
|
+ return matchedLines;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- boolean finished = process.waitFor(30, TimeUnit.SECONDS);
|
|
|
|
|
- if (!finished) {
|
|
|
|
|
- process.destroyForcibly();
|
|
|
|
|
- log.warn("查询Docker日志超时, container={}", containerName);
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过 Java Unix Domain Socket 直接调用 Docker Engine API,
|
|
|
|
|
+ * 不依赖 curl 或 docker CLI。
|
|
|
|
|
+ */
|
|
|
|
|
+ private byte[] callDockerApi(Path socketPath, String requestPath) {
|
|
|
|
|
+ UnixDomainSocketAddress address = UnixDomainSocketAddress.of(socketPath);
|
|
|
|
|
+
|
|
|
|
|
+ try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
|
|
|
|
|
+ channel.connect(address);
|
|
|
|
|
+
|
|
|
|
|
+ String httpRequest = "GET " + requestPath + " HTTP/1.0\r\nHost: localhost\r\n\r\n";
|
|
|
|
|
+ channel.write(ByteBuffer.wrap(httpRequest.getBytes(StandardCharsets.UTF_8)));
|
|
|
|
|
+
|
|
|
|
|
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
|
|
|
+ ByteBuffer buf = ByteBuffer.allocate(8192);
|
|
|
|
|
+ while (channel.read(buf) > 0) {
|
|
|
|
|
+ buf.flip();
|
|
|
|
|
+ byte[] chunk = new byte[buf.remaining()];
|
|
|
|
|
+ buf.get(chunk);
|
|
|
|
|
+ baos.write(chunk);
|
|
|
|
|
+ buf.clear();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- int exitCode = finished ? process.exitValue() : -1;
|
|
|
|
|
- if (exitCode != 0 && matchedLines.isEmpty()) {
|
|
|
|
|
- log.error("curl 退出码={}, container={}", exitCode, containerName);
|
|
|
|
|
- throw new RuntimeException("查询Docker日志失败,请检查容器名称是否正确: " + containerName);
|
|
|
|
|
|
|
+ byte[] response = baos.toByteArray();
|
|
|
|
|
+ if (response.length == 0) {
|
|
|
|
|
+ throw new RuntimeException("Docker API 返回空响应,请检查 docker.sock 权限");
|
|
|
}
|
|
}
|
|
|
|
|
+ return response;
|
|
|
|
|
|
|
|
- log.info("查询Docker日志完成, container={}, keyword={}, matched={}", containerName, keyword, matchedLines.size());
|
|
|
|
|
- return matchedLines;
|
|
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ throw new RuntimeException("连接 Docker socket 失败: " + e.getMessage(), e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- } 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);
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从原始 HTTP 响应中提取 body(跳过 HTTP 头部)。
|
|
|
|
|
+ * 同时检查 HTTP 状态码,非 200 时抛出异常并附带错误信息。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String extractHttpBody(byte[] rawResponse) {
|
|
|
|
|
+ String full = new String(rawResponse, StandardCharsets.UTF_8);
|
|
|
|
|
+
|
|
|
|
|
+ int headerEnd = full.indexOf("\r\n\r\n");
|
|
|
|
|
+ if (headerEnd < 0) {
|
|
|
|
|
+ throw new RuntimeException("Docker API 响应格式异常: " + full.substring(0, Math.min(200, full.length())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String headers = full.substring(0, headerEnd);
|
|
|
|
|
+ String body = full.substring(headerEnd + 4);
|
|
|
|
|
+
|
|
|
|
|
+ String statusLine = headers.lines().findFirst().orElse("");
|
|
|
|
|
+ if (!statusLine.contains("200")) {
|
|
|
|
|
+ String errorMsg = body.trim().isEmpty() ? statusLine : body.trim();
|
|
|
|
|
+ log.error("Docker API 返回错误, status={}, body={}", statusLine, errorMsg);
|
|
|
|
|
+ throw new RuntimeException("Docker API 错误: " + errorMsg);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ return body;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Docker 非 TTY 模式日志的每一帧带有 8 字节二进制头,
|
|
* Docker 非 TTY 模式日志的每一帧带有 8 字节二进制头,
|
|
|
- * 这里尝试剥离行首的不可打印字符。
|
|
|
|
|
|
|
+ * 剥离所有不可打印字符(保留 tab)。
|
|
|
*/
|
|
*/
|
|
|
- private String stripDockerStreamHeader(String line) {
|
|
|
|
|
|
|
+ private String stripNonPrintable(String line) {
|
|
|
if (line == null || line.isEmpty()) {
|
|
if (line == null || line.isEmpty()) {
|
|
|
return line;
|
|
return line;
|
|
|
}
|
|
}
|
|
|
- int start = 0;
|
|
|
|
|
- while (start < line.length() && (line.charAt(start) < 0x20 && line.charAt(start) != '\t')) {
|
|
|
|
|
- start++;
|
|
|
|
|
|
|
+ StringBuilder sb = new StringBuilder(line.length());
|
|
|
|
|
+ for (int i = 0; i < line.length(); i++) {
|
|
|
|
|
+ char c = line.charAt(i);
|
|
|
|
|
+ if (c >= 0x20 || c == '\t') {
|
|
|
|
|
+ sb.append(c);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- return start > 0 ? line.substring(start) : line;
|
|
|
|
|
|
|
+ return sb.toString();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|