1.30 k8s pods查询
This commit is contained in:
parent
b7fd18876c
commit
53ff53738d
@ -122,19 +122,23 @@ public class K8sDeploymentApiController extends BaseController<K8sDeployment, K8
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "查询Pod日志",
|
summary = "查询Pod日志(引用点模式)",
|
||||||
description = "查询指定Pod的日志内容。默认返回最后500行、最近1小时的日志,最大10MB。" +
|
description = "基于Kubernetes Dashboard的引用点系统查询日志,支持无重复轮询和前后翻页。\n" +
|
||||||
"注意:由于K8s API限制,无法实现传统分页,建议使用下载功能获取完整日志。"
|
"初始加载:referenceTimestamp=newest, offsetFrom=-100, offsetTo=1\n" +
|
||||||
|
"轮询刷新:使用上次返回的selection作为参数\n" +
|
||||||
|
"向前翻页:offsetFrom=-200, offsetTo=-100\n" +
|
||||||
|
"向后翻页:offsetFrom=100, offsetTo=200"
|
||||||
)
|
)
|
||||||
@GetMapping("/{deploymentId}/pods/{podName}/logs")
|
@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 = "Deployment ID", required = true) @PathVariable Long deploymentId,
|
||||||
@Parameter(description = "Pod名称", required = true) @PathVariable String podName,
|
@Parameter(description = "Pod名称", required = true) @PathVariable String podName,
|
||||||
@Parameter(description = "容器名称(可选,默认第一个容器)") @RequestParam(required = false) String container,
|
@Parameter(description = "容器名称(可选,默认第一个容器)") @RequestParam(required = false) String container,
|
||||||
@Parameter(description = "返回最后N行日志(可选,默认500行,最大10000行)") @RequestParam(required = false) Integer tail,
|
@Parameter(description = "引用点时间戳(newest/oldest/具体时间戳)", required = false) @RequestParam(required = false, defaultValue = "newest") String referenceTimestamp,
|
||||||
@Parameter(description = "返回最近N秒的日志(可选,默认3600秒即1小时,最大86400秒即24小时)") @RequestParam(required = false) Integer sinceSeconds
|
@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滚动重启")
|
@Operation(summary = "重启Deployment", description = "通过更新annotation触发Deployment滚动重启")
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -55,14 +55,17 @@ public interface IK8sPodService {
|
|||||||
List<K8sPodResponse> listPodsByNamespace(Long namespaceId);
|
List<K8sPodResponse> listPodsByNamespace(Long namespaceId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询Pod日志
|
* 查询Pod日志(引用点模式)
|
||||||
*
|
*
|
||||||
* @param deploymentId Deployment ID
|
* @param deploymentId Deployment ID
|
||||||
* @param podName Pod名称
|
* @param podName Pod名称
|
||||||
* @param container 容器名称(可选)
|
* @param container 容器名称(可选)
|
||||||
* @param tail 返回最后N行日志(可选)
|
* @param referenceTimestamp 引用点时间戳
|
||||||
* @param sinceSeconds 返回最近N秒的日志(可选)
|
* @param offsetFrom 起始偏移量
|
||||||
* @return Pod日志内容
|
* @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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,17 +131,12 @@ public class K8sPodServiceImpl implements IK8sPodService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getPodLogs(Long deploymentId, String podName, String container, Integer tail, Integer sinceSeconds) {
|
public com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse getPodLogsWithReference(
|
||||||
log.info("查询Pod日志,deploymentId: {}, podName: {}, container: {}, tail: {}, sinceSeconds: {}",
|
Long deploymentId, String podName, String container,
|
||||||
deploymentId, podName, container, tail, sinceSeconds);
|
String referenceTimestamp, Integer offsetFrom, Integer offsetTo) {
|
||||||
|
|
||||||
// 参数校验
|
log.info("查询Pod日志(引用点模式),deploymentId: {}, podName: {}, container: {}, referenceTimestamp: {}, offsetFrom: {}, offsetTo: {}",
|
||||||
if (tail != null && (tail < 1 || tail > 10000)) {
|
deploymentId, podName, container, referenceTimestamp, offsetFrom, offsetTo);
|
||||||
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小时)"});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 查询K8sDeployment
|
// 1. 查询K8sDeployment
|
||||||
K8sDeployment deployment = k8sDeploymentRepository.findById(deploymentId)
|
K8sDeployment deployment = k8sDeploymentRepository.findById(deploymentId)
|
||||||
@ -155,15 +150,39 @@ public class K8sPodServiceImpl implements IK8sPodService {
|
|||||||
ExternalSystem externalSystem = externalSystemRepository.findById(deployment.getExternalSystemId())
|
ExternalSystem externalSystem = externalSystemRepository.findById(deployment.getExternalSystemId())
|
||||||
.orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND));
|
.orElseThrow(() -> new BusinessException(ResponseCode.EXTERNAL_SYSTEM_NOT_FOUND));
|
||||||
|
|
||||||
// 4. 调用K8s API查询Pod日志
|
// 4. 调用K8s API获取原始日志(获取足够多的日志以支持切片)
|
||||||
return k8sServiceIntegration.getPodLogs(
|
// 使用较大的tail值确保能够覆盖请求的范围
|
||||||
|
Integer effectiveTail = Math.max(1000, Math.abs(offsetFrom) + Math.abs(offsetTo) + 100);
|
||||||
|
String rawLogs = k8sServiceIntegration.getPodLogs(
|
||||||
externalSystem,
|
externalSystem,
|
||||||
namespace.getNamespaceName(),
|
namespace.getNamespaceName(),
|
||||||
podName,
|
podName,
|
||||||
container,
|
container,
|
||||||
tail,
|
effectiveTail,
|
||||||
sinceSeconds,
|
null, // 不使用sinceSeconds,获取更多历史日志
|
||||||
false // follow参数设为false,不持续输出
|
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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user