1.30 k8s pods查询

This commit is contained in:
dengqichen 2025-12-13 23:31:48 +08:00
parent b7fd18876c
commit 53ff53738d
7 changed files with 343 additions and 31 deletions

View File

@ -122,19 +122,23 @@ public class K8sDeploymentApiController extends BaseController<K8sDeployment, K8
}
@Operation(
summary = "查询Pod日志",
description = "查询指定Pod的日志内容。默认返回最后500行、最近1小时的日志最大10MB。" +
"注意由于K8s API限制无法实现传统分页建议使用下载功能获取完整日志。"
summary = "查询Pod日志引用点模式",
description = "基于Kubernetes Dashboard的引用点系统查询日志支持无重复轮询和前后翻页。\n" +
"初始加载referenceTimestamp=newest, offsetFrom=-100, offsetTo=1\n" +
"轮询刷新使用上次返回的selection作为参数\n" +
"向前翻页offsetFrom=-200, offsetTo=-100\n" +
"向后翻页offsetFrom=100, offsetTo=200"
)
@GetMapping("/{deploymentId}/pods/{podName}/logs")
public Response<String> getPodLogs(
public Response<com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse> getPodLogs(
@Parameter(description = "Deployment ID", required = true) @PathVariable Long deploymentId,
@Parameter(description = "Pod名称", required = true) @PathVariable String podName,
@Parameter(description = "容器名称(可选,默认第一个容器)") @RequestParam(required = false) String container,
@Parameter(description = "返回最后N行日志可选默认500行最大10000行") @RequestParam(required = false) Integer tail,
@Parameter(description = "返回最近N秒的日志可选默认3600秒即1小时最大86400秒即24小时") @RequestParam(required = false) Integer sinceSeconds
@Parameter(description = "引用点时间戳newest/oldest/具体时间戳)", required = false) @RequestParam(required = false, defaultValue = "newest") String referenceTimestamp,
@Parameter(description = "起始偏移量(相对于引用点)", required = false) @RequestParam(required = false, defaultValue = "-100") Integer offsetFrom,
@Parameter(description = "结束偏移量(相对于引用点)", required = false) @RequestParam(required = false, defaultValue = "1") Integer offsetTo
) {
return Response.success(k8sPodService.getPodLogs(deploymentId, podName, container, tail, sinceSeconds));
return Response.success(k8sPodService.getPodLogsWithReference(deploymentId, podName, container, referenceTimestamp, offsetFrom, offsetTo));
}
@Operation(summary = "重启Deployment", description = "通过更新annotation触发Deployment滚动重启")

View File

@ -0,0 +1,25 @@
package com.qqchen.deploy.backend.deploy.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* K8S Pod日志行
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class K8sLogLine {
/**
* 日志时间戳RFC3339格式
* 例如2025-12-13T23:00:01.123456789Z
*/
private String timestamp;
/**
* 日志内容
*/
private String content;
}

View File

@ -0,0 +1,38 @@
package com.qqchen.deploy.backend.deploy.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* K8S Pod日志选择器
* 基于Kubernetes Dashboard的引用点系统
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class K8sLogSelection {
/**
* 引用点时间戳
* 特殊值
* - "newest": 最新的日志行
* - "oldest": 最早的日志行
* - RFC3339时间戳具体的时间点
*/
private String referenceTimestamp;
/**
* 相对于引用点的起始偏移量包含
* 负数表示引用点之前的行
* 例如-100 表示引用点之前100行
*/
private Integer offsetFrom;
/**
* 相对于引用点的结束偏移量不包含
* 正数表示引用点之后的行
* 例如1 表示引用点之后1行不包含
*/
private Integer offsetTo;
}

View File

@ -0,0 +1,41 @@
package com.qqchen.deploy.backend.deploy.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* K8S Pod日志响应引用点模式
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class K8sPodLogsResponse {
/**
* Pod名称
*/
private String podName;
/**
* 容器名称
*/
private String containerName;
/**
* 日志选择器用于下次请求
*/
private K8sLogSelection selection;
/**
* 日志行列表
*/
private List<K8sLogLine> logs;
/**
* 是否被截断
*/
private Boolean truncated;
}

View File

@ -55,14 +55,17 @@ public interface IK8sPodService {
List<K8sPodResponse> listPodsByNamespace(Long namespaceId);
/**
* 查询Pod日志
* 查询Pod日志引用点模式
*
* @param deploymentId Deployment ID
* @param podName Pod名称
* @param container 容器名称可选
* @param tail 返回最后N行日志可选
* @param sinceSeconds 返回最近N秒的日志可选
* @return Pod日志内容
* @param deploymentId Deployment ID
* @param podName Pod名称
* @param container 容器名称可选
* @param referenceTimestamp 引用点时间戳
* @param offsetFrom 起始偏移量
* @param offsetTo 结束偏移量
* @return Pod日志响应
*/
String getPodLogs(Long deploymentId, String podName, String container, Integer tail, Integer sinceSeconds);
com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse getPodLogsWithReference(
Long deploymentId, String podName, String container,
String referenceTimestamp, Integer offsetFrom, Integer offsetTo);
}

View File

@ -131,17 +131,12 @@ public class K8sPodServiceImpl implements IK8sPodService {
}
@Override
public String getPodLogs(Long deploymentId, String podName, String container, Integer tail, Integer sinceSeconds) {
log.info("查询Pod日志deploymentId: {}, podName: {}, container: {}, tail: {}, sinceSeconds: {}",
deploymentId, podName, container, tail, sinceSeconds);
// 参数校验
if (tail != null && (tail < 1 || tail > 10000)) {
throw new BusinessException(ResponseCode.INVALID_PARAM, new Object[]{"tail参数范围1-10000行"});
}
if (sinceSeconds != null && (sinceSeconds < 1 || sinceSeconds > 86400)) {
throw new BusinessException(ResponseCode.INVALID_PARAM, new Object[]{"sinceSeconds参数范围1-86400秒24小时"});
}
public com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse getPodLogsWithReference(
Long deploymentId, String podName, String container,
String referenceTimestamp, Integer offsetFrom, Integer offsetTo) {
log.info("查询Pod日志引用点模式deploymentId: {}, podName: {}, container: {}, referenceTimestamp: {}, offsetFrom: {}, offsetTo: {}",
deploymentId, podName, container, referenceTimestamp, offsetFrom, offsetTo);
// 1. 查询K8sDeployment
K8sDeployment deployment = k8sDeploymentRepository.findById(deploymentId)
@ -155,15 +150,39 @@ public class K8sPodServiceImpl implements IK8sPodService {
ExternalSystem externalSystem = externalSystemRepository.findById(deployment.getExternalSystemId())
.orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND));
// 4. 调用K8s API查询Pod日志
return k8sServiceIntegration.getPodLogs(
// 4. 调用K8s API获取原始日志获取足够多的日志以支持切片
// 使用较大的tail值确保能够覆盖请求的范围
Integer effectiveTail = Math.max(1000, Math.abs(offsetFrom) + Math.abs(offsetTo) + 100);
String rawLogs = k8sServiceIntegration.getPodLogs(
externalSystem,
namespace.getNamespaceName(),
podName,
container,
tail,
sinceSeconds,
false // follow参数设为false不持续输出
effectiveTail,
null, // 不使用sinceSeconds获取更多历史日志
false
);
// 5. 解析日志
List<com.qqchen.deploy.backend.deploy.dto.K8sLogLine> logLines =
com.qqchen.deploy.backend.deploy.utils.K8sLogParser.parseLogLines(rawLogs);
// 6. 创建选择器
com.qqchen.deploy.backend.deploy.dto.K8sLogSelection selection =
new com.qqchen.deploy.backend.deploy.dto.K8sLogSelection(
referenceTimestamp, offsetFrom, offsetTo);
// 7. 切片日志
com.qqchen.deploy.backend.deploy.utils.K8sLogParser.LogSliceResult result =
com.qqchen.deploy.backend.deploy.utils.K8sLogParser.selectLogs(logLines, selection);
// 8. 构建响应
return new com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse(
podName,
container != null ? container : "default",
result.getSelection(),
result.getLogs(),
result.isTruncated()
);
}
}

View File

@ -0,0 +1,182 @@
package com.qqchen.deploy.backend.deploy.utils;
import com.qqchen.deploy.backend.deploy.dto.K8sLogLine;
import com.qqchen.deploy.backend.deploy.dto.K8sLogSelection;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* K8S日志解析工具类
* 基于Kubernetes Dashboard的引用点系统实现
*/
@Slf4j
public class K8sLogParser {
private static final String NEWEST = "newest";
private static final String OLDEST = "oldest";
/**
* 解析K8S原始日志为日志行列表
* K8S日志格式2025-12-13T23:00:01.123456789Z log content here
*
* @param rawLogs 原始日志字符串
* @return 日志行列表
*/
public static List<K8sLogLine> parseLogLines(String rawLogs) {
if (rawLogs == null || rawLogs.trim().isEmpty()) {
return Collections.emptyList();
}
List<K8sLogLine> lines = new ArrayList<>();
String[] logLines = rawLogs.split("\n");
for (String line : logLines) {
if (line.trim().isEmpty()) {
continue;
}
// 查找第一个空格分隔时间戳和内容
int spaceIndex = line.indexOf(' ');
if (spaceIndex > 0 && spaceIndex < line.length() - 1) {
String timestamp = line.substring(0, spaceIndex);
String content = line.substring(spaceIndex + 1);
lines.add(new K8sLogLine(timestamp, content));
} else {
// 如果没有时间戳使用空字符串作为时间戳
lines.add(new K8sLogLine("", line));
}
}
return lines;
}
/**
* 查找引用点在日志列表中的索引
*
* @param lines 日志行列表
* @param referenceTimestamp 引用点时间戳
* @return 引用点索引如果未找到返回-1
*/
public static int findReferenceIndex(List<K8sLogLine> lines, String referenceTimestamp) {
if (lines == null || lines.isEmpty()) {
return -1;
}
// 特殊值处理
if (NEWEST.equals(referenceTimestamp)) {
return lines.size() - 1;
}
if (OLDEST.equals(referenceTimestamp)) {
return 0;
}
// 使用二分查找定位时间戳
int index = Collections.binarySearch(lines, new K8sLogLine(referenceTimestamp, ""),
(a, b) -> a.getTimestamp().compareTo(b.getTimestamp()));
if (index >= 0) {
// 找到精确匹配
return index;
} else {
// 未找到精确匹配返回插入点
int insertionPoint = -(index + 1);
// 返回最接近的索引
if (insertionPoint >= lines.size()) {
return lines.size() - 1;
}
if (insertionPoint <= 0) {
return 0;
}
return insertionPoint;
}
}
/**
* 根据选择器切片日志
*
* @param lines 日志行列表
* @param selection 日志选择器
* @return 切片后的日志行列表和新的选择器
*/
public static LogSliceResult selectLogs(List<K8sLogLine> lines, K8sLogSelection selection) {
if (lines == null || lines.isEmpty()) {
return new LogSliceResult(Collections.emptyList(), selection, false);
}
// 查找引用点索引
int referenceIndex = findReferenceIndex(lines, selection.getReferenceTimestamp());
if (referenceIndex == -1) {
log.warn("引用点未找到: {}", selection.getReferenceTimestamp());
return new LogSliceResult(Collections.emptyList(), selection, false);
}
// 计算切片范围
int fromIndex = referenceIndex + selection.getOffsetFrom();
int toIndex = referenceIndex + selection.getOffsetTo();
// 边界处理
boolean truncated = false;
if (toIndex > lines.size()) {
fromIndex -= (toIndex - lines.size());
toIndex = lines.size();
truncated = true;
}
if (fromIndex < 0) {
toIndex += -fromIndex;
fromIndex = 0;
truncated = true;
}
if (toIndex > lines.size()) {
toIndex = lines.size();
}
// 确保范围有效
if (fromIndex >= toIndex || fromIndex < 0 || toIndex > lines.size()) {
log.warn("无效的切片范围: fromIndex={}, toIndex={}, size={}", fromIndex, toIndex, lines.size());
return new LogSliceResult(Collections.emptyList(), selection, false);
}
// 切片
List<K8sLogLine> result = lines.subList(fromIndex, toIndex);
// 创建新的引用点使用中间行
int middleIndex = lines.size() / 2;
K8sLogSelection newSelection = new K8sLogSelection(
lines.get(middleIndex).getTimestamp(),
fromIndex - middleIndex,
toIndex - middleIndex
);
return new LogSliceResult(new ArrayList<>(result), newSelection, truncated);
}
/**
* 日志切片结果
*/
public static class LogSliceResult {
private final List<K8sLogLine> logs;
private final K8sLogSelection selection;
private final boolean truncated;
public LogSliceResult(List<K8sLogLine> logs, K8sLogSelection selection, boolean truncated) {
this.logs = logs;
this.selection = selection;
this.truncated = truncated;
}
public List<K8sLogLine> getLogs() {
return logs;
}
public K8sLogSelection getSelection() {
return selection;
}
public boolean isTruncated() {
return truncated;
}
}
}