1.33 日志通用查询

This commit is contained in:
dengqichen 2025-12-16 10:54:40 +08:00
parent e70f6c5d41
commit 5056b133ca
20 changed files with 1016 additions and 6 deletions

View File

@ -1,21 +1,22 @@
package com.qqchen.deploy.backend.deploy.api;
import com.qqchen.deploy.backend.deploy.dto.TeamApplicationDTO;
import com.qqchen.deploy.backend.deploy.dto.log.BaseLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.LogQueryResponse;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.query.TeamApplicationQuery;
import com.qqchen.deploy.backend.deploy.service.ITeamApplicationService;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.controller.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@ -76,5 +77,58 @@ public class TeamApplicationApiController extends BaseController<TeamApplication
protected void exportData(HttpServletResponse response, List<TeamApplicationDTO> data) {
// TODO: 实现导出功能
}
/**
* 查询应用日志
* 统一的日志查询接口支持K8S/Docker/Server三种运行时类型
* 根据请求中的runtimeType字段自动选择对应的日志查询策略
*
* <p>注意此接口不影响现有的K8s Deployment日志查询接口
* (/api/v1/k8s-deployment/{deploymentId}/pods/{podName}/logs)
*
* <p>请求示例
* <pre>
* // K8S日志查询
* POST /api/v1/team-applications/123/logs
* {
* "runtimeType": "K8S",
* "podName": "backend-xxx",
* "container": "app",
* "referenceTimestamp": "newest",
* "direction": "next",
* "logCount": 500
* }
*
* // Docker日志查询
* POST /api/v1/team-applications/123/logs
* {
* "runtimeType": "DOCKER",
* "containerName": "my-container",
* "lines": 100,
* "since": "1h"
* }
*
* // Server日志查询
* POST /api/v1/team-applications/123/logs
* {
* "runtimeType": "SERVER",
* "lines": 100
* }
* </pre>
*/
@Operation(
summary = "查询应用日志",
description = "统一的日志查询接口支持K8S/Docker/Server三种运行时类型。\n\n" +
"使用POST请求请求体中包含runtimeType字段用于区分不同类型的参数。\n\n" +
"返回格式统一,前端无需关心运行时类型差异。"
)
@PostMapping("/{teamAppId}/logs")
public Response<LogQueryResponse> queryLogs(
@Parameter(description = "团队应用ID", required = true) @PathVariable Long teamAppId,
@Parameter(description = "日志查询请求根据runtimeType自动反序列化为对应的子类", required = true)
@Validated @RequestBody BaseLogQueryRequest request
) {
return Response.success(teamApplicationService.queryLogs(teamAppId, request));
}
}

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.deploy.dto;
import com.qqchen.deploy.backend.deploy.enums.BuildTypeEnum;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import com.qqchen.deploy.backend.framework.dto.BaseDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
@ -91,5 +92,52 @@ public class TeamApplicationDTO extends BaseDTO {
@Schema(description = "环境名称")
private String environmentName;
// ==================== 运行时配置 ====================
@Schema(description = "运行时类型", example = "K8S")
private RuntimeTypeEnum runtimeType;
// ==================== K8s运行时配置 ====================
@Schema(description = "K8s系统ID")
private Long k8sSystemId;
@Schema(description = "K8s系统名称")
private String k8sSystemName;
@Schema(description = "K8s命名空间ID")
private Long k8sNamespaceId;
@Schema(description = "K8s命名空间名称")
private String k8sNamespaceName;
@Schema(description = "K8s Deployment ID")
private Long k8sDeploymentId;
@Schema(description = "K8s Deployment名称")
private String k8sDeploymentName;
// ==================== Docker运行时配置 ====================
@Schema(description = "Docker服务器ID")
private Long dockerServerId;
@Schema(description = "Docker服务器名称")
private String dockerServerName;
@Schema(description = "Docker容器名称")
private String dockerContainerName;
// ==================== 服务器运行时配置 ====================
@Schema(description = "服务器ID")
private Long serverId;
@Schema(description = "服务器名称")
private String serverName;
@Schema(description = "日志查询命令(支持占位符)", example = "tail -n {lines} /var/log/app.log")
private String logQueryCommand;
}

View File

@ -0,0 +1,30 @@
package com.qqchen.deploy.backend.deploy.dto.log;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
/**
* 日志查询请求基类
* 不同运行时类型有不同的查询参数使用多态设计
*
* @author qqchen
* @since 2025-12-16
*/
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "runtimeType"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = K8sLogQueryRequest.class, name = "K8S"),
@JsonSubTypes.Type(value = DockerLogQueryRequest.class, name = "DOCKER"),
@JsonSubTypes.Type(value = ServerLogQueryRequest.class, name = "SERVER")
})
public abstract class BaseLogQueryRequest {
/**
* 获取运行时类型
*/
public abstract RuntimeTypeEnum getRuntimeType();
}

View File

@ -0,0 +1,49 @@
package com.qqchen.deploy.backend.deploy.dto.log;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Docker日志查询请求
*
* @author qqchen
* @since 2025-12-16
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DockerLogQueryRequest extends BaseLogQueryRequest {
/**
* 运行时类型用于Jackson反序列化
*/
private final RuntimeTypeEnum runtimeType = RuntimeTypeEnum.DOCKER;
/**
* 容器名称可选默认使用TeamApplication.dockerContainerName
*/
private String containerName;
/**
* 日志行数可选默认100
*/
@Positive(message = "日志行数必须大于0")
private Integer lines = 100;
/**
* 时间范围可选
* 例如1h1小时30m30分钟2d2天
*/
private String since;
/**
* 是否显示时间戳可选默认false
*/
private Boolean timestamps = false;
@Override
public RuntimeTypeEnum getRuntimeType() {
return RuntimeTypeEnum.DOCKER;
}
}

View File

@ -0,0 +1,62 @@
package com.qqchen.deploy.backend.deploy.dto.log;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* K8S日志查询请求
* 复用现有K8S日志查询的参数设计
*
* @author qqchen
* @since 2025-12-16
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class K8sLogQueryRequest extends BaseLogQueryRequest {
/**
* 运行时类型用于Jackson反序列化
*/
private final RuntimeTypeEnum runtimeType = RuntimeTypeEnum.K8S;
/**
* Pod名称必填
*/
@NotBlank(message = "Pod名称不能为空")
private String podName;
/**
* 容器名称可选默认第一个容器
*/
private String container;
/**
* 引用点时间戳可选默认newest
* 特殊值
* - "newest": 最新的日志行
* - "oldest": 最早的日志行
* - RFC3339时间戳具体的时间点
*/
private String referenceTimestamp = "newest";
/**
* 方向可选默认next
* - prev: 向上加载历史日志
* - next: 向下加载新日志
*/
private String direction = "next";
/**
* 每次加载的行数可选默认100
*/
@Positive(message = "日志行数必须大于0")
private Integer logCount = 100;
@Override
public RuntimeTypeEnum getRuntimeType() {
return RuntimeTypeEnum.K8S;
}
}

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.deploy.dto.log;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 统一日志行
* 适用于K8S/Docker/Server等所有运行时类型
*
* @author qqchen
* @since 2025-12-16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LogLine {
/**
* 日志时间戳RFC3339格式或其他格式
* 例如2025-12-16T10:30:00.123456789Z
*/
private String timestamp;
/**
* 日志内容
*/
private String content;
}

View File

@ -0,0 +1,51 @@
package com.qqchen.deploy.backend.deploy.dto.log;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 统一日志查询响应
* 适用于K8S/Docker/Server等所有运行时类型
*
* @author qqchen
* @since 2025-12-16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LogQueryResponse {
/**
* 运行时类型
*/
private RuntimeTypeEnum runtimeType;
/**
* 日志行列表
*/
private List<LogLine> logs;
/**
* 是否还有更多日志
*/
private Boolean hasMore;
/**
* 扩展元数据
* 用于存放特定运行时类型的额外信息
* 例如K8S的引用点信息
* - referenceForPrevious: K8sLogSelection对象
* - referenceForNext: K8sLogSelection对象
* - podName: Pod名称
* - containerName: 容器名称
* - truncated: 是否被截断
*/
private Map<String, Object> metadata;
}

View File

@ -0,0 +1,34 @@
package com.qqchen.deploy.backend.deploy.dto.log;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Server日志查询请求
*
* @author qqchen
* @since 2025-12-16
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ServerLogQueryRequest extends BaseLogQueryRequest {
/**
* 运行时类型用于Jackson反序列化
*/
private final RuntimeTypeEnum runtimeType = RuntimeTypeEnum.SERVER;
/**
* 日志行数可选默认100
* 将作为占位符{lines}替换到logQueryCommand中
*/
@Positive(message = "日志行数必须大于0")
private Integer lines = 100;
@Override
public RuntimeTypeEnum getRuntimeType() {
return RuntimeTypeEnum.SERVER;
}
}

View File

@ -108,5 +108,71 @@ public class TeamApplication extends Entity<Long> {
*/
@Column(name = "workflow_definition_id")
private Long workflowDefinitionId;
// ==================== 运行时配置 ====================
/**
* 运行时类型K8S-Kubernetes集群DOCKER-Docker容器SERVER-传统服务器
*/
@Enumerated(EnumType.STRING)
@Column(name = "runtime_type", length = 50)
private com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum runtimeType;
// ==================== K8s运行时配置 ====================
/**
* K8s系统ID关联sys_external_systemtype=K8S
* 仅当 runtimeType=K8S 时使用
*/
@Column(name = "k8s_system_id")
private Long k8sSystemId;
/**
* K8s命名空间ID关联deploy_k8s_namespace
* 仅当 runtimeType=K8S 时使用
*/
@Column(name = "k8s_namespace_id")
private Long k8sNamespaceId;
/**
* K8s Deployment ID关联deploy_k8s_deployment
* 仅当 runtimeType=K8S 时使用
*/
@Column(name = "k8s_deployment_id")
private Long k8sDeploymentId;
// ==================== Docker运行时配置 ====================
/**
* Docker服务器ID关联sys_server
* 仅当 runtimeType=DOCKER 时使用
*/
@Column(name = "docker_server_id")
private Long dockerServerId;
/**
* Docker容器名称
* 仅当 runtimeType=DOCKER 时使用
*/
@Column(name = "docker_container_name", length = 255)
private String dockerContainerName;
// ==================== 服务器运行时配置 ====================
/**
* 服务器ID关联sys_server
* 仅当 runtimeType=SERVER 时使用
*/
@Column(name = "server_id")
private Long serverId;
/**
* 日志查询命令支持占位符
* 例如tail -n {lines} /var/log/app.log
* 例如supervisorctl tail -n {lines} {appName}
* 仅当 runtimeType=SERVER 时使用
*/
@Column(name = "log_query_command", length = 500)
private String logQueryCommand;
}

View File

@ -0,0 +1,75 @@
package com.qqchen.deploy.backend.deploy.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
/**
* 运行时类型枚举
* 定义应用的运行环境类型
*
* @author qqchen
* @since 2025-12-15
*/
@Getter
public enum RuntimeTypeEnum {
/**
* Kubernetes集群
*/
K8S("K8S", "Kubernetes集群"),
/**
* Docker容器
*/
DOCKER("DOCKER", "Docker容器"),
/**
* 传统服务器JAR脚本等
*/
SERVER("SERVER", "传统服务器");
/**
* 枚举代码
*/
private final String code;
/**
* 枚举描述
*/
private final String description;
RuntimeTypeEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 根据code获取枚举
*/
@JsonCreator
public static RuntimeTypeEnum fromCode(String code) {
if (code == null) {
return null;
}
for (RuntimeTypeEnum type : RuntimeTypeEnum.values()) {
if (type.code.equals(code)) {
return type;
}
}
throw new IllegalArgumentException("未知的运行时类型: " + code);
}
/**
* 序列化为JSON时使用code
*/
@JsonValue
public String getCode() {
return code;
}
@Override
public String toString() {
return code;
}
}

View File

@ -2,11 +2,16 @@ package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.converter.TeamApplicationConverter;
import com.qqchen.deploy.backend.deploy.dto.TeamApplicationDTO;
import com.qqchen.deploy.backend.deploy.dto.log.BaseLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.LogQueryResponse;
import com.qqchen.deploy.backend.deploy.entity.Application;
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
import com.qqchen.deploy.backend.deploy.entity.Environment;
import com.qqchen.deploy.backend.deploy.entity.Team;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.entity.K8sNamespace;
import com.qqchen.deploy.backend.deploy.entity.K8sDeployment;
import com.qqchen.deploy.backend.deploy.entity.Server;
import com.qqchen.deploy.backend.deploy.enums.BuildTypeEnum;
import com.qqchen.deploy.backend.deploy.query.TeamApplicationQuery;
import com.qqchen.deploy.backend.deploy.entity.ExternalSystem;
@ -18,7 +23,12 @@ import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository;
import com.qqchen.deploy.backend.deploy.repository.IEnvironmentRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamApplicationRepository;
import com.qqchen.deploy.backend.deploy.repository.ITeamRepository;
import com.qqchen.deploy.backend.deploy.repository.IK8sNamespaceRepository;
import com.qqchen.deploy.backend.deploy.repository.IK8sDeploymentRepository;
import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
import com.qqchen.deploy.backend.deploy.service.ITeamApplicationService;
import com.qqchen.deploy.backend.deploy.strategy.ILogQueryStrategy;
import com.qqchen.deploy.backend.deploy.strategy.LogQueryStrategyFactory;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
@ -65,12 +75,48 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
@Resource
private TeamApplicationConverter teamApplicationConverter;
@Resource
private LogQueryStrategyFactory logQueryStrategyFactory;
@Resource
private IK8sNamespaceRepository k8sNamespaceRepository;
@Resource
private IK8sDeploymentRepository k8sDeploymentRepository;
@Resource
private IServerRepository serverRepository;
@Override
@Transactional
public TeamApplicationDTO create(TeamApplicationDTO dto) {
return super.create(dto);
}
@Override
public LogQueryResponse queryLogs(Long teamAppId, BaseLogQueryRequest request) {
log.info("查询应用日志teamAppId: {}, runtimeType: {}", teamAppId, request.getRuntimeType());
// 1. 查询团队应用
TeamApplication teamApp = teamApplicationRepository.findById(teamAppId)
.orElseThrow(() -> new BusinessException(ResponseCode.TEAM_APPLICATION_NOT_FOUND));
// 2. 校验运行时类型是否匹配
if (teamApp.getRuntimeType() != request.getRuntimeType()) {
throw new BusinessException(ResponseCode.TEAM_APP_RUNTIME_TYPE_MISMATCH,
new Object[]{teamApp.getRuntimeType(), request.getRuntimeType()});
}
// 3. 获取对应的日志查询策略
ILogQueryStrategy strategy = logQueryStrategyFactory.getStrategy(request.getRuntimeType());
// 4. 执行日志查询
LogQueryResponse response = strategy.queryLogs(teamApp, request);
log.info("日志查询完成,返回 {} 条日志", response.getLogs() != null ? response.getLogs().size() : 0);
return response;
}
/**
* 重写父类的唯一性约束校验方法
* <p>校验规则
@ -172,6 +218,28 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 收集K8S相关ID
Set<Long> k8sNamespaceIds = teamApps.stream()
.map(TeamApplicationDTO::getK8sNamespaceId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> k8sDeploymentIds = teamApps.stream()
.map(TeamApplicationDTO::getK8sDeploymentId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 收集Docker和Server相关ID
Set<Long> dockerServerIds = teamApps.stream()
.map(TeamApplicationDTO::getDockerServerId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Set<Long> serverIds = teamApps.stream()
.map(TeamApplicationDTO::getServerId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 2. 批量查询团队信息
Map<Long, Team> teamMap = new HashMap<>();
if (!teamIds.isEmpty()) {
@ -248,7 +316,34 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
);
}
// 11. 填充扩展字段
// 11. 批量查询K8S Namespace信息
Map<Long, K8sNamespace> k8sNamespaceMap = new HashMap<>();
if (!k8sNamespaceIds.isEmpty()) {
k8sNamespaceRepository.findAllById(k8sNamespaceIds).forEach(namespace ->
k8sNamespaceMap.put(namespace.getId(), namespace)
);
}
// 12. 批量查询K8S Deployment信息
Map<Long, K8sDeployment> k8sDeploymentMap = new HashMap<>();
if (!k8sDeploymentIds.isEmpty()) {
k8sDeploymentRepository.findAllById(k8sDeploymentIds).forEach(deployment ->
k8sDeploymentMap.put(deployment.getId(), deployment)
);
}
// 13. 批量查询Server信息Docker和Server共用
Map<Long, Server> serverMap = new HashMap<>();
Set<Long> allServerIds = new HashSet<>();
allServerIds.addAll(dockerServerIds);
allServerIds.addAll(serverIds);
if (!allServerIds.isEmpty()) {
serverRepository.findAllById(allServerIds).forEach(server ->
serverMap.put(server.getId(), server)
);
}
// 14. 填充扩展字段
teamApps.forEach(teamApp -> {
// 填充团队名称
if (teamApp.getTeamId() != null) {
@ -324,6 +419,38 @@ public class TeamApplicationServiceImpl extends BaseServiceImpl<TeamApplication,
teamApp.setTargetGitProjectName(project.getName());
}
}
// 填充K8S Namespace名称
if (teamApp.getK8sNamespaceId() != null) {
K8sNamespace namespace = k8sNamespaceMap.get(teamApp.getK8sNamespaceId());
if (namespace != null) {
teamApp.setK8sNamespaceName(namespace.getNamespaceName());
}
}
// 填充K8S Deployment名称
if (teamApp.getK8sDeploymentId() != null) {
K8sDeployment deployment = k8sDeploymentMap.get(teamApp.getK8sDeploymentId());
if (deployment != null) {
teamApp.setK8sDeploymentName(deployment.getDeploymentName());
}
}
// 填充Docker Server名称
if (teamApp.getDockerServerId() != null) {
Server server = serverMap.get(teamApp.getDockerServerId());
if (server != null) {
teamApp.setDockerServerName(server.getServerName());
}
}
// 填充Server名称
if (teamApp.getServerId() != null) {
Server server = serverMap.get(teamApp.getServerId());
if (server != null) {
teamApp.setServerName(server.getServerName());
}
}
});
}

View File

@ -0,0 +1,32 @@
package com.qqchen.deploy.backend.deploy.strategy;
import com.qqchen.deploy.backend.deploy.dto.log.BaseLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.LogQueryResponse;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
/**
* 日志查询策略接口
* 使用策略模式支持不同运行时类型的日志查询
*
* @author qqchen
* @since 2025-12-16
*/
public interface ILogQueryStrategy {
/**
* 支持的运行时类型
*
* @return 运行时类型枚举
*/
RuntimeTypeEnum supportedType();
/**
* 查询日志
*
* @param teamApp 团队应用实体
* @param request 日志查询请求
* @return 统一的日志查询响应
*/
LogQueryResponse queryLogs(TeamApplication teamApp, BaseLogQueryRequest request);
}

View File

@ -0,0 +1,59 @@
package com.qqchen.deploy.backend.deploy.strategy;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 日志查询策略工厂
* 根据运行时类型选择对应的策略实现
*
* @author qqchen
* @since 2025-12-16
*/
@Slf4j
@Component
public class LogQueryStrategyFactory {
@Resource
private List<ILogQueryStrategy> strategies;
private Map<RuntimeTypeEnum, ILogQueryStrategy> strategyMap;
/**
* 初始化策略映射
*/
@PostConstruct
public void init() {
strategyMap = new HashMap<>();
for (ILogQueryStrategy strategy : strategies) {
strategyMap.put(strategy.supportedType(), strategy);
log.info("注册日志查询策略: {} -> {}",
strategy.supportedType(), strategy.getClass().getSimpleName());
}
}
/**
* 获取日志查询策略
*
* @param runtimeType 运行时类型
* @return 对应的策略实现
* @throws BusinessException 如果找不到对应的策略
*/
public ILogQueryStrategy getStrategy(RuntimeTypeEnum runtimeType) {
ILogQueryStrategy strategy = strategyMap.get(runtimeType);
if (strategy == null) {
throw new BusinessException(ResponseCode.LOG_QUERY_STRATEGY_NOT_FOUND,
new Object[]{runtimeType});
}
return strategy;
}
}

View File

@ -0,0 +1,57 @@
package com.qqchen.deploy.backend.deploy.strategy.impl;
import com.qqchen.deploy.backend.deploy.dto.log.BaseLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.DockerLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.LogQueryResponse;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import com.qqchen.deploy.backend.deploy.strategy.ILogQueryStrategy;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Docker日志查询策略骨架实现
*
* @author qqchen
* @since 2025-12-16
*/
@Slf4j
@Component
public class DockerLogQueryStrategy implements ILogQueryStrategy {
@Override
public RuntimeTypeEnum supportedType() {
return RuntimeTypeEnum.DOCKER;
}
@Override
public LogQueryResponse queryLogs(TeamApplication teamApp, BaseLogQueryRequest request) {
// 类型转换
DockerLogQueryRequest dockerRequest = (DockerLogQueryRequest) request;
// 校验Docker配置
validateDockerConfig(teamApp);
// TODO: 实现Docker日志查询逻辑
// 1. 获取Docker服务器连接信息
// 2. 执行docker logs命令
// 3. 解析日志输出
// 4. 转换为统一格式
throw new BusinessException(ResponseCode.FEATURE_NOT_IMPLEMENTED);
}
/**
* 校验Docker配置是否完整
*/
private void validateDockerConfig(TeamApplication teamApp) {
if (teamApp.getDockerServerId() == null) {
throw new BusinessException(ResponseCode.TEAM_APP_DOCKER_CONFIG_INCOMPLETE);
}
if (teamApp.getDockerContainerName() == null || teamApp.getDockerContainerName().isBlank()) {
throw new BusinessException(ResponseCode.TEAM_APP_DOCKER_CONFIG_INCOMPLETE);
}
}
}

View File

@ -0,0 +1,115 @@
package com.qqchen.deploy.backend.deploy.strategy.impl;
import com.qqchen.deploy.backend.deploy.dto.K8sLogLine;
import com.qqchen.deploy.backend.deploy.dto.K8sPodLogsResponse;
import com.qqchen.deploy.backend.deploy.dto.log.BaseLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.K8sLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.LogLine;
import com.qqchen.deploy.backend.deploy.dto.log.LogQueryResponse;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import com.qqchen.deploy.backend.deploy.service.IK8sPodService;
import com.qqchen.deploy.backend.deploy.strategy.ILogQueryStrategy;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* K8S日志查询策略
* 复用现有的IK8sPodService实现
*
* @author qqchen
* @since 2025-12-16
*/
@Slf4j
@Component
public class K8sLogQueryStrategy implements ILogQueryStrategy {
@Resource
private IK8sPodService k8sPodService;
@Override
public RuntimeTypeEnum supportedType() {
return RuntimeTypeEnum.K8S;
}
@Override
public LogQueryResponse queryLogs(TeamApplication teamApp, BaseLogQueryRequest request) {
// 类型转换
K8sLogQueryRequest k8sRequest = (K8sLogQueryRequest) request;
// 校验K8S配置
validateK8sConfig(teamApp);
// 直接使用K8S Deployment ID
Long deploymentId = teamApp.getK8sDeploymentId();
// 调用现有的K8S日志查询服务
K8sPodLogsResponse k8sResponse = k8sPodService.getPodLogsWithReference(
deploymentId,
k8sRequest.getPodName(),
k8sRequest.getContainer(),
k8sRequest.getReferenceTimestamp(),
k8sRequest.getDirection(),
k8sRequest.getLogCount()
);
// 转换为统一格式
return convertToLogQueryResponse(k8sResponse);
}
/**
* 校验K8S配置是否完整
*/
private void validateK8sConfig(TeamApplication teamApp) {
if (teamApp.getK8sSystemId() == null) {
throw new BusinessException(ResponseCode.TEAM_APP_K8S_CONFIG_INCOMPLETE);
}
if (teamApp.getK8sNamespaceId() == null) {
throw new BusinessException(ResponseCode.TEAM_APP_K8S_CONFIG_INCOMPLETE);
}
if (teamApp.getK8sDeploymentId() == null) {
throw new BusinessException(ResponseCode.TEAM_APP_K8S_CONFIG_INCOMPLETE);
}
}
/**
* 将K8sPodLogsResponse转换为统一的LogQueryResponse
*/
private LogQueryResponse convertToLogQueryResponse(K8sPodLogsResponse k8sResponse) {
// 转换日志行
List<LogLine> logs = k8sResponse.getLogs().stream()
.map(this::convertLogLine)
.collect(Collectors.toList());
// 构建元数据
Map<String, Object> metadata = new HashMap<>();
metadata.put("podName", k8sResponse.getPodName());
metadata.put("containerName", k8sResponse.getContainerName());
metadata.put("referenceForPrevious", k8sResponse.getReferenceForPrevious());
metadata.put("referenceForNext", k8sResponse.getReferenceForNext());
metadata.put("truncated", k8sResponse.getTruncated());
// 构建响应
return LogQueryResponse.builder()
.runtimeType(RuntimeTypeEnum.K8S)
.logs(logs)
.hasMore(!Boolean.TRUE.equals(k8sResponse.getTruncated()))
.metadata(metadata)
.build();
}
/**
* 转换单个日志行
*/
private LogLine convertLogLine(K8sLogLine k8sLogLine) {
return new LogLine(k8sLogLine.getTimestamp(), k8sLogLine.getContent());
}
}

View File

@ -0,0 +1,59 @@
package com.qqchen.deploy.backend.deploy.strategy.impl;
import com.qqchen.deploy.backend.deploy.dto.log.BaseLogQueryRequest;
import com.qqchen.deploy.backend.deploy.dto.log.LogQueryResponse;
import com.qqchen.deploy.backend.deploy.dto.log.ServerLogQueryRequest;
import com.qqchen.deploy.backend.deploy.entity.TeamApplication;
import com.qqchen.deploy.backend.deploy.enums.RuntimeTypeEnum;
import com.qqchen.deploy.backend.deploy.strategy.ILogQueryStrategy;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* Server日志查询策略骨架实现
* TODO: 待后续实现时注入ISSHCommandService
*
* @author qqchen
* @since 2025-12-16
*/
@Slf4j
@Component
public class ServerLogQueryStrategy implements ILogQueryStrategy {
@Override
public RuntimeTypeEnum supportedType() {
return RuntimeTypeEnum.SERVER;
}
@Override
public LogQueryResponse queryLogs(TeamApplication teamApp, BaseLogQueryRequest request) {
// 类型转换
ServerLogQueryRequest serverRequest = (ServerLogQueryRequest) request;
// 校验Server配置
validateServerConfig(teamApp);
// TODO: 实现Server日志查询逻辑
// 1. 获取服务器连接信息
// 2. 替换logQueryCommand中的占位符{lines}
// 3. 通过SSH执行命令
// 4. 解析命令输出
// 5. 转换为统一格式
throw new BusinessException(ResponseCode.FEATURE_NOT_IMPLEMENTED);
}
/**
* 校验Server配置是否完整
*/
private void validateServerConfig(TeamApplication teamApp) {
if (teamApp.getServerId() == null) {
throw new BusinessException(ResponseCode.TEAM_APP_SERVER_CONFIG_INCOMPLETE);
}
if (teamApp.getLogQueryCommand() == null || teamApp.getLogQueryCommand().isBlank()) {
throw new BusinessException(ResponseCode.TEAM_APP_SERVER_CONFIG_INCOMPLETE);
}
}
}

View File

@ -226,6 +226,14 @@ public enum ResponseCode {
TEAM_APPLICATION_ALREADY_EXISTS(2928, "team.application.already.exists"),
TEAM_CONFIG_NOT_FOUND(2929, "team.config.not.found"),
TEAM_APPLICATION_DEPLOY_JOB_EXISTS(2930, "team.application.deploy.job.exists"),
TEAM_APPLICATION_RUNTIME_TYPE_NOT_CONFIGURED(2931, "team.application.runtime.type.not.configured"),
TEAM_APPLICATION_LOG_QUERY_STRATEGY_NOT_FOUND(2932, "team.application.log.query.strategy.not.found"),
TEAM_APP_RUNTIME_TYPE_MISMATCH(2933, "team.application.runtime.type.mismatch"),
TEAM_APP_K8S_CONFIG_INCOMPLETE(2934, "team.application.k8s.config.incomplete"),
TEAM_APP_DOCKER_CONFIG_INCOMPLETE(2935, "team.application.docker.config.incomplete"),
TEAM_APP_SERVER_CONFIG_INCOMPLETE(2936, "team.application.server.config.incomplete"),
LOG_QUERY_STRATEGY_NOT_FOUND(2937, "log.query.strategy.not.found"),
FEATURE_NOT_IMPLEMENTED(2938, "feature.not.implemented"),
// 服务器管理相关错误码 (2950-2969)
SERVER_NOT_FOUND(2950, "server.not.found"),
@ -289,7 +297,13 @@ public enum ResponseCode {
K8S_DEPLOYMENT_SYNC_FAILED(3233, "k8s.deployment.sync.failed"),
K8S_POD_NOT_FOUND(3234, "k8s.pod.not.found"),
K8S_RESOURCE_NOT_FOUND(3235, "k8s.resource.not.found"),
K8S_OPERATION_FAILED(3236, "k8s.operation.failed");
K8S_OPERATION_FAILED(3236, "k8s.operation.failed"),
// Docker集成错误码 (3240-3259)
DOCKER_LOG_QUERY_NOT_IMPLEMENTED(3240, "docker.log.query.not.implemented"),
// 服务器日志查询错误码 (3260-3279)
SERVER_LOG_QUERY_NOT_IMPLEMENTED(3260, "server.log.query.not.implemented");
private final int code;
private final String messageKey; // 国际化消息key

View File

@ -825,6 +825,22 @@ CREATE TABLE deploy_team_application
deploy_system_id BIGINT NULL COMMENT '部署系统ID关联sys_external_systemtype=JENKINS仅当build_type=JENKINS时使用',
deploy_job VARCHAR(100) NULL COMMENT '部署任务名称Jenkins Job名称仅当build_type=JENKINS时使用',
workflow_definition_id BIGINT NULL COMMENT '工作流定义ID关联workflow_definition',
-- 运行时配置
runtime_type VARCHAR(50) NULL COMMENT '运行时类型K8S-Kubernetes集群DOCKER-Docker容器SERVER-传统服务器)',
-- K8s运行时配置
k8s_system_id BIGINT NULL COMMENT 'K8s系统ID关联sys_external_systemtype=K8S仅当runtime_type=K8S时使用',
k8s_namespace_id BIGINT NULL COMMENT 'K8s命名空间ID关联deploy_k8s_namespace仅当runtime_type=K8S时使用',
k8s_deployment_id BIGINT NULL COMMENT 'K8s Deployment ID关联deploy_k8s_deployment仅当runtime_type=K8S时使用',
-- Docker运行时配置
docker_server_id BIGINT NULL COMMENT 'Docker服务器ID关联sys_server仅当runtime_type=DOCKER时使用',
docker_container_name VARCHAR(255) NULL COMMENT 'Docker容器名称仅当runtime_type=DOCKER时使用',
-- 服务器运行时配置
server_id BIGINT NULL COMMENT '服务器ID关联sys_server仅当runtime_type=SERVER时使用',
log_query_command VARCHAR(500) NULL COMMENT '日志查询命令支持占位符如tail -n {lines} /var/log/app.log仅当runtime_type=SERVER时使用',
UNIQUE INDEX uk_team_app_env (team_id, application_id, environment_id),
INDEX idx_team (team_id),
@ -836,6 +852,11 @@ CREATE TABLE deploy_team_application
INDEX idx_deploy_job (deploy_job),
INDEX idx_workflow_definition (workflow_definition_id),
INDEX idx_target_git_system (target_git_system_id),
INDEX idx_runtime_type (runtime_type),
INDEX idx_k8s_system (k8s_system_id),
INDEX idx_k8s_namespace (k8s_namespace_id),
INDEX idx_docker_server (docker_server_id),
INDEX idx_server (server_id),
CONSTRAINT fk_team_app_team FOREIGN KEY (team_id) REFERENCES deploy_team (id),
CONSTRAINT fk_team_app_application FOREIGN KEY (application_id) REFERENCES deploy_application (id),
CONSTRAINT fk_team_app_target_git_system FOREIGN KEY (target_git_system_id) REFERENCES sys_external_system (id)

View File

@ -231,6 +231,14 @@ jenkins.response.parse.error=Jenkins response parse error: {0}
# Team Application Related (2920-2939)
team.application.already.exists=Application already configured for this environment
team.application.deploy.job.exists=This Jenkins Job is already used by another application in this environment, please check the configuration
team.application.runtime.type.not.configured=Application runtime type not configured, please contact administrator to configure runtime type (K8S/DOCKER/SERVER)
team.application.log.query.strategy.not.found=Log query strategy not found for runtime type, please contact administrator to check system configuration
team.application.runtime.type.mismatch=Team application runtime type does not match request type
team.application.k8s.config.incomplete=K8S configuration incomplete, please check K8S system ID, namespace ID and deployment name
team.application.docker.config.incomplete=Docker configuration incomplete, please check Docker server ID and container name
team.application.server.config.incomplete=Server configuration incomplete, please check server ID and log query command
log.query.strategy.not.found=Log query strategy not found for runtime type
feature.not.implemented=Feature not implemented yet
# --------------------------------------------------------------------------------------
# K8S Integration Related - 3220-3239
@ -263,3 +271,9 @@ k8s.deployment.sync.failed=K8S deployment synchronization failed, please check c
# K8S Operation Errors
k8s.operation.failed=K8S operation failed, please try again later
# Docker Integration Errors
docker.log.query.not.implemented=Docker log query feature not implemented yet, please contact administrator
# Server Log Query Errors
server.log.query.not.implemented=Server log query feature not implemented yet, please contact administrator

View File

@ -231,6 +231,14 @@ jenkins.response.parse.error=Jenkins响应解析失败{0}
# 团队应用相关 (2920-2939)
team.application.already.exists=该应用已配置到此环境
team.application.deploy.job.exists=该环境下已有其他应用使用此 Jenkins Job请检查配置
team.application.runtime.type.not.configured=应用运行时类型未配置请联系管理员配置运行时类型K8S/DOCKER/SERVER
team.application.log.query.strategy.not.found=未找到运行时类型对应的日志查询策略,请联系管理员检查系统配置
team.application.runtime.type.mismatch=团队应用的运行时类型与请求类型不匹配
team.application.k8s.config.incomplete=K8S配置不完整请检查K8S系统ID、命名空间ID和Deployment名称
team.application.docker.config.incomplete=Docker配置不完整请检查Docker服务器ID和容器名称
team.application.server.config.incomplete=Server配置不完整请检查服务器ID和日志查询命令
log.query.strategy.not.found=未找到对应运行时类型的日志查询策略
feature.not.implemented=功能待实现
# --------------------------------------------------------------------------------------
# K8S集成相关 (K8S Integration) - 3220-3239
@ -263,3 +271,9 @@ k8s.deployment.sync.failed=K8S Deployment同步失败请检查集群配置和
# K8S操作错误
k8s.operation.failed=K8S操作失败请稍后重试
# Docker集成错误
docker.log.query.not.implemented=Docker日志查询功能待实现请联系管理员
# 服务器日志查询错误
server.log.query.not.implemented=服务器日志查询功能待实现,请联系管理员