使用注解将日志存入Elasticsearch
方案一:使用Spring AOP + 自定义注解
1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EsLog {String module() default "";String operation() default "";boolean saveParams() default true;boolean saveResult() default false;String index() default "app-logs";
}2. 日志实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LogEntry {private String id;private String module;private String operation;private String method;private String className;private Object params;private Object result;private String userId;private String username;private String ip;private Long executionTime;private Date createTime;private Boolean success;private String errorMsg;
}3. AOP切面处理
@Aspect
@Component
@Slf4j
public class EsLogAspect {@Autowiredprivate ElasticsearchRestTemplate elasticsearchRestTemplate;@Autowiredprivate HttpServletRequest request;@Around("@annotation(esLog)")public Object around(ProceedingJoinPoint joinPoint, EsLog esLog) throws Throwable {long startTime = System.currentTimeMillis();LogEntry logEntry = new LogEntry();try {// 设置基本信息setupBasicInfo(joinPoint, esLog, logEntry);// 执行目标方法Object result = joinPoint.proceed();// 记录成功信息long endTime = System.currentTimeMillis();logEntry.setSuccess(true);logEntry.setExecutionTime(endTime - startTime);if (esLog.saveResult()) {logEntry.setResult(result);}// 异步保存到ESsaveToEsAsync(logEntry);return result;} catch (Exception e) {// 记录异常信息long endTime = System.currentTimeMillis();logEntry.setSuccess(false);logEntry.setErrorMsg(e.getMessage());logEntry.setExecutionTime(endTime - startTime);saveToEsAsync(logEntry);throw e;}}private void setupBasicInfo(ProceedingJoinPoint joinPoint, EsLog esLog, LogEntry logEntry) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();logEntry.setId(UUID.randomUUID().toString());logEntry.setModule(esLog.module());logEntry.setOperation(esLog.operation());logEntry.setMethod(method.getName());logEntry.setClassName(method.getDeclaringClass().getName());logEntry.setCreateTime(new Date());// 获取请求参数if (esLog.saveParams()) {Object[] args = joinPoint.getArgs();String[] paramNames = signature.getParameterNames();Map<String, Object> params = new HashMap<>();for (int i = 0; i < args.length; i++) {// 过滤敏感参数if (!isSensitiveParam(paramNames[i])) {params.put(paramNames[i], args[i]);}}logEntry.setParams(params);}// 获取用户信息setupUserInfo(logEntry);// 获取IP地址logEntry.setIp(getClientIp());}@Asyncpublic void saveToEsAsync(LogEntry logEntry) {try {IndexQuery indexQuery = new IndexQueryBuilder().withId(logEntry.getId()).withObject(logEntry).build();elasticsearchRestTemplate.index(indexQuery, IndexCoordinates.of(logEntry.getIndex()));} catch (Exception e) {log.error("保存日志到ES失败: {}", e.getMessage(), e);}}private String getClientIp() {String ip = request.getHeader("X-Forwarded-For");if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}private void setupUserInfo(LogEntry logEntry) {// 从SecurityContext获取用户信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.isAuthenticated()) {Object principal = authentication.getPrincipal();if (principal instanceof UserDetails) {UserDetails userDetails = (UserDetails) principal;logEntry.setUsername(userDetails.getUsername());}}}private boolean isSensitiveParam(String paramName) {return paramName.toLowerCase().contains("password") || paramName.toLowerCase().contains("token") ||paramName.toLowerCase().contains("secret");}
}方案二:使用Logback直接输出到ES
1. 添加依赖
<dependency><groupId>net.logstash.logback</groupId><artifactId>logstash-logback-encoder</artifactId><version>7.2</version>
</dependency>2. logback-spring.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration><appender name="ES" class="org.elasticsearch.logback.appender.ElasticsearchAppender"><url>http://localhost:9200</url><index>app-logs</index><type>_doc</type><connectTimeout>3000</connectTimeout><errorsToStderr>true</errorsToStderr><includeCallerData>true</includeCallerData><logsToStderr>false</logsToStderr><maxQueueSize>1024</maxQueueSize><readTimeout>3000</readTimeout></appender><appender name="ASYNC_ES" class="ch.qos.logback.classic.AsyncAppender"><appender-ref ref="ES" /><queueSize>1024</queueSize><discardingThreshold>0</discardingThreshold><includeCallerData>true</includeCallerData><maxFlushTime>0</maxFlushTime><neverBlock>true</neverBlock></appender><logger name="com.example.annotation" level="INFO" additivity="false"><appender-ref ref="ASYNC_ES" /></logger><root level="INFO"><appender-ref ref="ASYNC_ES" /></root>
</configuration>方案三:结合业务使用的示例
1. 在Service中使用注解
@Service
@Slf4j
public class UserService {@EsLog(module = "用户管理", operation = "创建用户", saveParams = true)public User createUser(CreateUserRequest request) {// 业务逻辑return userRepository.save(request.toUser());}@EsLog(module = "用户管理", operation = "删除用户", saveParams = false)public void deleteUser(Long userId) {userRepository.deleteById(userId);}@EsLog(module = "用户管理", operation = "查询用户列表", saveResult = true)public Page<User> getUserList(UserQuery query) {return userRepository.findByCondition(query);}
}2. 配置类
@Configuration
@EnableElasticsearchRepositories
@EnableAsync
@EnableAspectJAutoProxy
public class EsLogConfig {@Beanpublic ElasticsearchRestTemplate elasticsearchRestTemplate() {return new ElasticsearchRestTemplate(elasticsearchClient());}@Beanpublic RestHighLevelClient elasticsearchClient() {ClientConfiguration clientConfiguration = ClientConfiguration.builder().connectedTo("localhost:9200").build();return RestClients.create(clientConfiguration).rest();}
}方案四:高级特性
1. 条件日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConditionalEsLog {String module();String operation();LogLevel level() default LogLevel.INFO;String condition() default "";Class<? extends LogEvaluator> evaluator() default DefaultLogEvaluator.class;
}public interface LogEvaluator {boolean shouldLog(ProceedingJoinPoint joinPoint);
}2. 批量操作日志
@Component
public class BatchLogProcessor {private final List<LogEntry> logBuffer = new ArrayList<>();private final int batchSize = 100;@Scheduled(fixedRate = 5000) // 每5秒执行一次@Asyncpublic void batchSaveLogs() {if (logBuffer.isEmpty()) {return;}List<LogEntry> logsToSave;synchronized (logBuffer) {logsToSave = new ArrayList<>(logBuffer);logBuffer.clear();}List<IndexQuery> queries = logsToSave.stream().map(log -> new IndexQueryBuilder().withId(log.getId()).withObject(log).build()).collect(Collectors.toList());elasticsearchRestTemplate.bulkIndex(queries, IndexCoordinates.of("app-logs"));}public void addLog(LogEntry logEntry) {synchronized (logBuffer) {logBuffer.add(logEntry);if (logBuffer.size() >= batchSize) {batchSaveLogs();}}}
}总结
这种基于注解的ES日志方案具有以下优点:
无侵入性:通过注解实现,不干扰业务逻辑
灵活配置:可以控制记录参数、返回值等
异步处理:不影响主业务流程性能
结构化存储:便于后续查询和分析
易于扩展:可以轻松添加新的日志字段或处理逻辑
