Merge branch 'feature/REQ-2972' into 'master'

Feature/req 2972

See merge request universal/infrastructure/backend/axzo-log-plat!126
This commit is contained in:
金海洋 2024-09-25 10:12:05 +00:00
commit bcfa1ed5e5
23 changed files with 1272 additions and 5 deletions

2
.reviewboardrc Normal file
View File

@ -0,0 +1,2 @@
REPOSITORY = 'axzo-log-plat'
REVIEWBOARD_URL = "https://reviewboard.axzo.cn"

View File

@ -0,0 +1,65 @@
package cn.axzo.log.platform.client.feign;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.azxo.framework.common.model.CommonResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:31
*/
@FeignClient(name = "log-plat", url = "http://log-plat:8080")
public interface LogApi {
/**
* 通过筛查条件查询日志列表
* <p>
* 1. 若所有条件为空则返回最近的1000条日志
* 2. 支持分页可通过开关控制默认开启最多返回1000条
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 日志列表
*/
@PostMapping(value = "/api/log/findLogsWithExample", consumes = "application/json")
CommonResponse<List<LogResp>> findLogsWithExample(@RequestBody LogFindReq req);
/**
* 通过日志id查询日志详情
*
* @param id 日志id
* @return 日志
*/
@GetMapping(value = "/api/log/findLogById")
CommonResponse<LogResp> findLogById(@RequestParam("id") @NotBlank(message = "id is required") String id);
/**
* 添加日志
*
* @param req 日志信息 {@link LogAddReq}
* @return 日志id
*/
@PostMapping(value = "/api/log/add", consumes = "application/json")
CommonResponse<String> addLog(@RequestBody @Valid LogAddReq req);
/**
* 批量添加日志
*
* @param req 日志列表 {@link LogBatchAddReq}
* @return 日志id列表
*/
@PostMapping(value = "/api/log/add/batch", consumes = "application/json")
CommonResponse<List<String>> batchAddLogs(@RequestBody @Valid LogBatchAddReq req);
}

View File

@ -0,0 +1,37 @@
package cn.axzo.log.platform.client.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/13 10:20
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Condition {
/**
* 字段名
*/
@NotBlank(message = "key is required")
private String key;
/**
* 操作符, 等于eq, 大于gt, 小于le, 大于等于ge, 小于等于le, 不等于ne, 包含like, 不包含not like, 存在exists, 不存在not exists
*/
@NotBlank(message = "operator is required")
private String operator;
/**
* 字段值
*/
private Object value;
}

View File

@ -0,0 +1,46 @@
package cn.axzo.log.platform.client.model.req;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* 新增日志请求参数
*
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:51
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogAddReq {
/**
* 场景
*/
@NotBlank(message = "scene is required")
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
@NotBlank(message = "level is required")
private String level;
/**
* 日志标签建议可以将服务名称操作等信息作为tag
*/
private List<String> tags;
/**
* 日志内容需要是json格式
*/
@NotBlank(message = "message is required")
private String message;
}

View File

@ -0,0 +1,25 @@
package cn.axzo.log.platform.client.model.req;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/12 10:56
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogBatchAddReq {
@NotEmpty(message = "logs is required")
private List<@Valid LogAddReq> logs;
}

View File

@ -0,0 +1,23 @@
package cn.axzo.log.platform.client.model.req;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/24 14:37
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LogBatchDeleteReq {
/**
* 待删除的日志id列表
*/
private List<String> ids;
}

View File

@ -0,0 +1,93 @@
package cn.axzo.log.platform.client.model.req;
import cn.axzo.log.platform.client.model.Condition;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.Valid;
import java.util.List;
/**
* 查询日志列表请求参数
*
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:45
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogFindReq {
/**
* 场景
*/
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志标签
*/
private List<String> tags;
/**
* 日志内容筛查
* 具体使用可参考https://alidocs.dingtalk.com/i/nodes/YndMj49yWj7q1dB9hQLNneo0V3pmz5aA
* <p>
* message中可嵌套json当需要查询内部json中某个字段的相关情况时key为message.xxx例如message.comments.commentId
* "messageConditions": [
* {
* "key": "comments.commentId",
* "operator": "like",
* "value": "c"
* },{
* "key": "articleId",
* "operator": "not exists"
* }
* ]
* </p>
*/
private List<@Valid Condition> messageConditions;
/**
* 开始时间时间戳
*/
private Long start;
/**
* 结束时间时间戳
*/
private Long end;
/**
* 分页页码从1开始{@code pageSize}搭配使用
* <p>
* 1. 分页开关开启后生效默认开启分页
* 2. 若与{@code lastId}同时存在则优先使用{@code lastId}参数
* </p>
*/
private Integer pageNum;
/**
* 分页大小{@code pageNum}{@code lastId}搭配使用
* <p>最大限制为1000</p>
*/
private Integer pageSize;
/**
* 游标上次请求返回的最后一条日志的id{@code pageSize}搭配使用
* <p>
* 1. 分页开关开启后生效默认开启分页
* 2. 默认为空首次请求时不传此参数
* 3. 若与{@code pageNum}同时存在则优先使用本参数
* </p>
*/
private String lastId;
}

View File

@ -0,0 +1,50 @@
package cn.axzo.log.platform.client.model.resp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:38
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LogResp {
private String id;
/**
* 日志场景
*/
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志时间戳
*/
private Long timestamp;
/**
* 日志标签建议可以将服务名称操作等信息作为tag
*/
private List<String> tags;
/**
* 日志内容, json格式
*/
private String message;
public String getTimestampAsString() {
return String.valueOf(timestamp);
}
}

View File

@ -146,6 +146,29 @@
<groupId>cn.axzo.trade</groupId>
<artifactId>trade-data-security-sd-extension</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.platform</groupId>
<artifactId>axzo-log-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
</dependencies>
<build>

View File

@ -19,7 +19,8 @@ import org.springframework.core.env.Environment;
"cn.axzo.apollo",
"cn.axzo.pudge",
"cn.axzo.elise",
"cn.axzo.basics"
"cn.axzo.basics",
"cn.axzo.log.platform.client"
})
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = "cn.axzo.log.platform.server")

View File

@ -0,0 +1,32 @@
package cn.axzo.log.platform.server.config;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.framework.domain.web.result.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:31
*/
@Order(value = 0)
@RestControllerAdvice
@Slf4j
public class ExceptionAdviceHandler {
@ExceptionHandler(ServiceException.class)
public ApiResult<Void> basicsServiceExceptionHandler(ServiceException e) {
log.warn("业务异常", e);
return ApiResult.err(e.getMessage());
}
@ExceptionHandler(cn.axzo.framework.domain.ServiceException.class)
public ApiResult<Void> domainServiceExceptionHandler(cn.axzo.framework.domain.ServiceException e) {
log.warn("业务异常", e);
return ApiResult.err(e.getMessage());
}
}

View File

@ -0,0 +1,49 @@
package cn.axzo.log.platform.server.controller.api;
import cn.axzo.log.platform.client.feign.LogApi;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchAddReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.service.LogService;
import cn.azxo.framework.common.model.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
/**
* 日志接口实现
*
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 10:55
*/
@RestController
@RequiredArgsConstructor
public class LogController implements LogApi {
private final LogService logService;
@Override
public CommonResponse<List<LogResp>> findLogsWithExample(LogFindReq req) {
return CommonResponse.success(logService.findLogsWithExample(req));
}
@Override
public CommonResponse<LogResp> findLogById(String id) {
return CommonResponse.success(logService.findLogById(id));
}
@Override
public CommonResponse<String> addLog(LogAddReq req) {
return CommonResponse.success(logService.addLog(req));
}
@Override
public CommonResponse<List<String>> batchAddLogs(@RequestBody @Valid LogBatchAddReq req) {
return CommonResponse.success(logService.batchAddLogs(req));
}
}

View File

@ -0,0 +1,91 @@
package cn.axzo.log.platform.server.controller.web;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchDeleteReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.service.LogService;
import cn.azxo.framework.common.model.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 18:16
*/
@RestController
@RequestMapping("/webApi/log")
@RequiredArgsConstructor
public class WebLogController {
private final LogService logService;
/**
* 通过筛查条件查询日志列表
* <p>
* 1. 若所有条件为空则返回最近的1000条日志
* 2. 支持分页可通过开关控制默认开启最多返回1000条
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 日志列表
*/
@PostMapping(value = "/findLogsWithExample", consumes = "application/json")
CommonResponse<List<LogResp>> findLogsWithExample(@RequestBody @Valid LogFindReq req) {
return CommonResponse.success(logService.findLogsWithExample(req));
}
/**
* 通过日志id查询日志详情
*
* @param id 日志id
* @return 日志
*/
@GetMapping(value = "/findLogById")
CommonResponse<LogResp> findLogById(@RequestParam("id") @NotBlank(message = "id is required") String id) {
return CommonResponse.success(logService.findLogById(id));
}
/**
* 添加日志
*
* @param req 日志信息 {@link LogAddReq}
* @return 日志id
*/
@PostMapping(value = "/add", consumes = "application/json")
CommonResponse<String> addLog(@RequestBody @Valid LogAddReq req) {
return CommonResponse.success(logService.addLog(req));
}
/**
* 批量添加日志
*
* @param req 日志列表 {@link LogBatchAddReq}
* @return 日志id列表
*/
@PostMapping(value = "/add/batch", consumes = "application/json")
CommonResponse<List<String>> batchAddLogs(@RequestBody @Valid LogBatchAddReq req) {
return CommonResponse.success(logService.batchAddLogs(req));
}
/**
* 批量删除日志
*
* @param req 待删除日志id
*/
@PostMapping(value = "/delete/batch")
CommonResponse<Boolean> batchDeleteLogs(@RequestBody LogBatchDeleteReq req) {
return CommonResponse.success(logService.batchDeleteLogs(req));
}
}

View File

@ -0,0 +1,51 @@
package cn.axzo.log.platform.server.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 11:41
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "log")
public class LogEntity {
@Id
private String id;
/**
* 日志场景
*/
private String scene;
/**
* 日志级别, DEBUG, INFO, WARN, ERROR
*/
private String level;
/**
* 日志时间
*/
private Long timestamp;
/**
* 日志标签建议可以将服务名称操作等信息作为tag
*/
private List<String> tags;
/**
* 日志内容json格式
*/
private org.bson.Document message;
}

View File

@ -0,0 +1,15 @@
package cn.axzo.log.platform.server.mapper;
import cn.axzo.log.platform.server.entity.LogEntity;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 13:56
*/
@Repository
public interface LogMapper extends MongoRepository<LogEntity, String> {
}

View File

@ -0,0 +1,78 @@
package cn.axzo.log.platform.server.resolvers;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchDeleteReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.service.LogService;
import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 14:52
*/
@Component
@RequiredArgsConstructor
public class LogResolver implements GraphQLQueryResolver, GraphQLMutationResolver {
private final LogService logService;
/**
* 通过条件筛查日志
* <p>
* 若所有条件为空则返回按时间序倒排的1000条日志
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 按时间倒序排序的日志记录集合
*/
public List<LogResp> findLogsWithExample(LogFindReq req) {
return logService.findLogsWithExample(req);
}
/**
* 通过id查询日志
*
* @param id 日志id
* @return 日志记录
*/
public LogResp findLogById(String id) {
return logService.findLogById(id);
}
/**
* 添加日志
*
* @param req 日志
* @return 日志记录id
*/
public String addLog(LogAddReq req) {
return logService.addLog(req);
}
/**
* 批量添加日志
*
* @param req 日志列表 {@link LogBatchAddReq}
* @return 日志id列表
*/
public List<String> batchAddLogs(LogBatchAddReq req) {
return logService.batchAddLogs(req);
}
/**
* 批量删除日志
*
* @param req 待删除日志id
*/
public Boolean batchDeleteLogs(LogBatchDeleteReq req) {
return logService.batchDeleteLogs(req);
}
}

View File

@ -0,0 +1,27 @@
package cn.axzo.log.platform.server.service;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchDeleteReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 11:02
*/
public interface LogService {
String addLog(LogAddReq req);
LogResp findLogById(String id);
List<LogResp> findLogsWithExample(LogFindReq req);
List<String> batchAddLogs(LogBatchAddReq req);
Boolean batchDeleteLogs(LogBatchDeleteReq req);
}

View File

@ -0,0 +1,358 @@
package cn.axzo.log.platform.server.service.impl;
import cn.axzo.basics.common.util.NumberUtil;
import cn.axzo.framework.domain.ServiceException;
import cn.axzo.log.platform.client.model.Condition;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchAddReq;
import cn.axzo.log.platform.client.model.req.LogBatchDeleteReq;
import cn.axzo.log.platform.client.model.req.LogFindReq;
import cn.axzo.log.platform.client.model.resp.LogResp;
import cn.axzo.log.platform.server.entity.LogEntity;
import cn.axzo.log.platform.server.mapper.LogMapper;
import cn.axzo.log.platform.server.service.LogService;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.mongodb.client.result.DeleteResult;
import lombok.RequiredArgsConstructor;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 11:04
*/
@RefreshScope
@Service
@RequiredArgsConstructor
public class LogServiceImpl implements LogService {
private final LogMapper logMapper;
private final MongoTemplate mongoTemplate;
public static String MESSAGE_FIELD_PREFIX = "message.";
public static String MESSAGE_FIELD_ARRAY_DEFAULT_KEY = "\"defaultItems\"";
/**
* 是否开启分页查询
*/
@Value("${log.enable-page:true}")
public boolean enablePage;
/**
* 添加日志
*
* @param req 日志信息 {@link LogAddReq}
* @return 日志id
*/
@Override
public String addLog(LogAddReq req) {
LogEntity logEntity = buildToLogEntity(req);
logMapper.save(logEntity);
return logEntity.getId();
}
/**
* 通过日志id查询日志详情
*
* @param id 日志id
* @return 日志
*/
@Override
public LogResp findLogById(String id) {
return logMapper.findById(id)
.map(logEntity -> {
LogResp logResp = BeanUtil.copyProperties(logEntity, LogResp.class);
logResp.setMessage(logEntity.getMessage()
.toJson());
return logResp;
})
.orElse(null);
}
/**
* 通过筛查条件查询日志列表
* <p>
* 若所有条件为空则返回按时间序倒排的1000条日志
* </p>
*
* @param req 查询条件 {@link LogFindReq}
* @return 按时间倒序排序的日志列表
*/
@Override
public List<LogResp> findLogsWithExample(LogFindReq req) {
List<LogEntity> logs;
if (Objects.isNull(req) || isAllFieldsNullOrBlankOrEmpty(req)) {
// 按照时间倒序即字段timestamp查询1000条
logs = findTopLimitOrderById(-1, 1000L);
if (ObjectUtil.isEmpty(logs)) {
return Collections.emptyList();
}
} else {
// 根据条件查询
Query query = buildQueryCriteria(req);
logs = mongoTemplate.find(query, LogEntity.class);
if (CollUtil.isEmpty(logs)) {
return Collections.emptyList();
}
}
return logs.stream()
.map(logEntity -> {
LogResp logResp = BeanUtil.copyProperties(logEntity, LogResp.class, "message");
logResp.setMessage(logEntity.getMessage()
.toJson());
return logResp;
})
.collect(Collectors.toList());
}
/**
* 批量添加日志
*
* @param req 日志列表 {@link LogBatchAddReq}
* @return 日志id列表
*/
@Override
public List<String> batchAddLogs(LogBatchAddReq req) {
List<LogEntity> logEntities = req.getLogs()
.stream()
.map(this::buildToLogEntity)
.collect(Collectors.toList());
logMapper.saveAll(logEntities);
return logEntities.stream()
.map(LogEntity::getId)
.collect(Collectors.toList());
}
/**
* 批量删除日志
*
* @param req 待删除日志id
*/
@Override
public Boolean batchDeleteLogs(LogBatchDeleteReq req) {
if (Objects.isNull(req) || CollUtil.isEmpty(req.getIds())) {
return true;
}
DeleteResult result = mongoTemplate.remove(new Query(Criteria.where("_id").in(req.getIds())), LogEntity.class);
return result.wasAcknowledged();
}
/**
* 构建查询条件
*
* @param req 查询条件 {@link LogFindReq}
* @return Query对象
*/
@NotNull
private Query buildQueryCriteria(LogFindReq req) {
Criteria criteria = new Criteria();
if (CharSequenceUtil.isNotBlank(req.getScene())) {
criteria.and("scene")
.regex(req.getScene());
}
if (CharSequenceUtil.isNotBlank(req.getLevel())) {
criteria.and("level")
.is(req.getLevel());
}
if (CollUtil.isNotEmpty(req.getTags())) {
criteria.and("tags")
.all(req.getTags());
}
if (CollUtil.isNotEmpty(req.getMessageConditions())) {
buildMessageCriteria(req.getMessageConditions(), criteria);
}
if (NumberUtil.isPositiveNumber(req.getStart())) {
criteria.and("timestamp")
.gte(req.getStart());
}
if (NumberUtil.isPositiveNumber(req.getEnd())) {
criteria.and("timestamp")
.lte(req.getEnd());
}
Query query = new Query(criteria);
// 构建分页条件
if (enablePage) {
if (CharSequenceUtil.isNotBlank(req.getLastId())) {
criteria.and("_id").lt(new ObjectId(req.getLastId()));
} else if (NumberUtil.isPositiveNumber(req.getPageNum())) {
query.skip((req.getPageNum() - 1) * req.getPageSize());
}
if (NumberUtil.isNotPositiveNumber(req.getPageSize()) || req.getPageSize() > 1000) {
req.setPageSize(1000);
}
query.limit(req.getPageSize());
}
Sort sort = Sort.by(Sort.Direction.DESC, "_id");
query.with(sort);
return query;
}
private static void buildMessageCriteria(List<Condition> messageConditions, Criteria criteria) {
for (Condition condition : messageConditions) {
String key = condition.getKey();
Object value = condition.getValue();
String operator = condition.getOperator();
switch (operator) {
case "exists":
// 若message字段为json对象则判断其是否存在若message.items 字段为数组
criteria.and(MESSAGE_FIELD_PREFIX + key)
.exists(true);
break;
case "not exists":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.exists(false);
break;
case "eq":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.is(value);
break;
case "ne":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.ne(value);
break;
case "gt":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.gt(value);
break;
case "lt":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.lt(value);
break;
case "ge":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.gte(value);
break;
case "le":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.lte(value);
break;
case "like":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.regex(value.toString());
break;
case "not like":
criteria.and(MESSAGE_FIELD_PREFIX + key)
.not()
.regex(value.toString());
break;
default:
throw new ServiceException("operator is not supported");
}
}
}
/**
* 查询按timestamp字段指定顺序排序后的指定条数日志记录
*
* @param order 1:升序-1:降序
* @param size 查询条数
* @return 日志列表
*/
List<LogEntity> findTopLimitOrderById(Integer order, Long size) {
Criteria criteria = Criteria.where("timestamp")
.exists(true);
Query query = new Query(criteria);
query.with(Sort.by(order > 0 ? Sort.Direction.ASC : Sort.Direction.DESC, "timestamp"))
.limit(size.intValue());
return mongoTemplate.find(query, LogEntity.class);
}
/**
* 判断对象的所有字段是否都为空或空字符串
*
* @param obj 需要检查的对象
* @return 如果所有字段都是 null ""返回 true否则返回 false
*/
public static boolean isAllFieldsNullOrBlankOrEmpty(Object obj) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
try {
// 设置访问权限因为有些字段可能是私有的
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
if (!((String) value).trim()
.isEmpty()) {
return false; // 字段不是空字符串
}
} else if (value instanceof Collection) {
if (!((Collection<?>) value).isEmpty()) {
return false; // 字段不是空集合
}
} else if (value != null) {
return false; // 字段不为 null
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Error accessing field", e);
}
}
return true;
}
/**
* 判断是否是合法的json对象
*
* @param str 字符串
* @return true合法false非法
*/
public static Boolean isValidJsonObject(String str) {
if (JSONUtil.isTypeJSON(str)) {
try {
JSONUtil.parseObj(str);
return true;
} catch (Exception e1) {
try {
JSONUtil.parseArray(str);
return true;
} catch (Exception e2) {
return false;
}
}
}
return false;
}
@NotNull
private LogEntity buildToLogEntity(LogAddReq req) {
String message = req.getMessage();
if (JSONUtil.isTypeJSONArray(req.getMessage())) {
message = "{" + MESSAGE_FIELD_ARRAY_DEFAULT_KEY + ":" + req.getMessage() + "}";
}
// 判断req.message字段是否是一个合法的json字符串
if (Boolean.TRUE.equals(!isValidJsonObject(message))) {
throw new ServiceException("message is not a valid json string, message: " + message);
}
LogEntity logEntity = BeanUtil.copyProperties(req, LogEntity.class, "message");
logEntity.setTimestamp(System.currentTimeMillis());
logEntity.setMessage(Document.parse(message));
return logEntity;
}
}

View File

@ -49,8 +49,7 @@ public class LogSinkELKServiceImpl implements LogSinkELKService {
private Map<String, String> appKeyConfigMap = new HashMap<>();
@Value("${log-plat.sign.keysecret.config}")
public void setAppKeyConfigMap(String appKeyConfig) {
public void setAppKeyConfigMap(@Value("${log-plat.sign.keysecret.config}")String appKeyConfig) {
Map<String, String> configMap = JSONObject.parseObject(appKeyConfig, Map.class);
if (configMap != null) {
this.appKeyConfigMap = configMap;

View File

@ -4,7 +4,7 @@ spring:
cloud:
nacos:
config:
server-addr: ${NACOS_HOST:dev-nacos.axzo.cn}:${NACOS_PORT:80}
server-addr: ${NACOS_HOST:https://dev-nacos.axzo.cn}:${NACOS_PORT:443}
file-extension: yaml
namespace: ${NACOS_NAMESPACE_ID:35eada10-9574-4db8-9fea-bc6a4960b6c7}
prefix: ${spring.application.name}
@ -42,4 +42,7 @@ trade:
desensitization:
enabled: true #开启全局脱敏
crypt:
enable: true #开启全局加解密
enable: true #开启全局加解密
server:
shutdown: graceful

View File

@ -0,0 +1,58 @@
type Query {
findLogsWithExample(logFindReq: LogFindReq): [LogResp]
findLogById(id: ID!): LogResp
}
type Mutation {
addLog(logReq: LogAddReq): String!
batchAddLogs(logsReq: LogBatchAddReq): [String!]!
batchDeleteLogs(logsReq: LogBatchDeleteReq): Boolean
}
type LogResp {
id: ID!
scene: String!
level: String!
timestamp: String!
tags: [String]
message: String!
}
# 查询日志入参
input LogFindReq {
scene: String
level: String
tags: [String]
messageConditions: [Condition]
start: String
end: String
pageNum: Int
pageSize: Int
lastId: String
}
input Condition {
key: String!
operator: String!
# 可能是字符串,数字,或浮点数
value: String
}
# 添加日志入参
input LogAddReq {
scene: String!
level: String!
tags: [String]
message: String!
}
# 批量添加日志入参
input LogBatchAddReq {
logs: [LogAddReq]!
}
# 批量删除日志入参
input LogBatchDeleteReq {
ids: [ID]!
}

View File

@ -0,0 +1,35 @@
package cn.axzo.log.platform.server.api;
import cn.axzo.log.platform.client.model.req.LogAddReq;
import cn.axzo.log.platform.server.LogPlatApplicationTestBase;
import cn.axzo.log.platform.server.controller.api.LogController;
import cn.axzo.log.platform.server.service.MongoLogOperationTest;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/11 17:10
*/
public class LogApiTest extends LogPlatApplicationTestBase {
@Autowired
private LogController logApi;
@Test
public void addLog() {
MongoLogOperationTest.User user = new MongoLogOperationTest.User();
user.setName("test");
user.setAge(18);
LogAddReq logAddReq = LogAddReq.builder()
.scene("test")
.level("INFO")
.tags(Lists.newArrayList("tag1", "tag2", "tag3", "demo", "test"))
.message(JSONUtil.toJsonStr(user))
.build();
logApi.addLog(logAddReq);
}
}

View File

@ -0,0 +1,106 @@
package cn.axzo.log.platform.server.service;
import cn.axzo.log.platform.server.LogPlatApplicationTestBase;
import cn.axzo.log.platform.server.entity.LogEntity;
import cn.axzo.log.platform.server.mapper.LogMapper;
import cn.hutool.json.JSONUtil;
import com.google.common.collect.Lists;
import lombok.Data;
import org.bson.Document;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* @author chenwenjian
* @version 1.0
* @date 2024/9/10 16:37
*/
public class MongoLogOperationTest extends LogPlatApplicationTestBase {
@Autowired
private LogMapper logMapper;
@Test
public void insertOne() {
User user = new User();
user.setName("whymechen");
user.setAge(18);
LogEntity logEntity = LogEntity.builder()
.scene("add user")
.level("INFO")
.timestamp(System.currentTimeMillis())
.tags(Arrays.asList(new String[]{"user", "axzo-log-plat"}))
.message(Document.parse(JSONUtil.toJsonStr(user)))
.build();
logMapper.insert(logEntity);
}
@Test
public void insertMany() {
User user = new User();
user.setName("lucy");
user.setAge(20);
LogEntity logEntity = LogEntity.builder()
.scene("add user")
.level("INFO")
.timestamp(System.currentTimeMillis())
.tags(Arrays.asList(new String[]{"user", "axzo-log-plat"}))
.message(Document.parse(JSONUtil.toJsonStr(user)))
.build();
Animal animal = new Animal();
animal.setName("dog");
animal.setType("dog");
animal.setColor("black");
LogEntity logEntity2 = LogEntity.builder()
.scene("add animal")
.level("INFO")
.timestamp(System.currentTimeMillis())
.tags(Arrays.asList(new String[]{"animal", "axzo-log-plat"}))
.message(Document.parse(JSONUtil.toJsonStr(animal)))
.build();
logMapper.insert(Lists.newArrayList(logEntity, logEntity2));
}
@Test
public void findAll() {
List<LogEntity> all = logMapper.findAll();
System.out.println(all);
// logMapper.findAllByTimestampBetween(LocalDateTime.now().minusDays(1), LocalDateTime.now());
}
@Test
public void findTagWithUser() {
LogEntity logEntity = new LogEntity();
logEntity.setTags(Arrays.asList(new String[]{"user"}));
Example<LogEntity> example = Example.of(logEntity);
List<LogEntity> all = logMapper.findAll(example);
System.out.println(all);
// logMapper.findAllByTimestampBetween(LocalDateTime.now().minusDays(1), LocalDateTime.now());
}
public
@Data
static class User {
private String name;
private Integer age;
}
@Data
static class Animal {
private String name;
private String type;
private String color;
}
}