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(
|
||||
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滚动重启")
|
||||
|
||||
@ -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);
|
||||
|
||||
/**
|
||||
* 查询Pod日志
|
||||
* 查询Pod日志(引用点模式)
|
||||
*
|
||||
* @param deploymentId Deployment ID
|
||||
* @param podName Pod名称
|
||||
* @param container 容器名称(可选)
|
||||
* @param tail 返回最后N行日志(可选)
|
||||
* @param sinceSeconds 返回最近N秒的日志(可选)
|
||||
* @return Pod日志内容
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
public com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse getPodLogsWithReference(
|
||||
Long deploymentId, String podName, String container,
|
||||
String referenceTimestamp, Integer offsetFrom, Integer offsetTo) {
|
||||
|
||||
// 参数校验
|
||||
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小时)"});
|
||||
}
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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