使用注解将日志存入Elasticsearch

方案一:使用Spring AOP + 自定义注解

1. 自定义注解

java 复制代码
@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. 日志实体类

java 复制代码
@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切面处理

java 复制代码
@Aspect
@Component
@Slf4j
public class EsLogAspect {
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    @Autowired
    private 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);
            }
            
            // 异步保存到ES
            saveToEsAsync(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());
    }
    
    @Async
    public 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. 添加依赖

java 复制代码
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.2</version>
</dependency>

2. logback-spring.xml配置

java 复制代码
<?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中使用注解

java 复制代码
@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. 配置类

java 复制代码
@Configuration
@EnableElasticsearchRepositories
@EnableAsync
@EnableAspectJAutoProxy
public class EsLogConfig {
    
    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate() {
        return new ElasticsearchRestTemplate(elasticsearchClient());
    }
    
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo("localhost:9200")
            .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
}

方案四:高级特性

1. 条件日志注解

java 复制代码
@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. 批量操作日志

java 复制代码
@Component
public class BatchLogProcessor {
    
    private final List<LogEntry> logBuffer = new ArrayList<>();
    private final int batchSize = 100;
    
    @Scheduled(fixedRate = 5000) // 每5秒执行一次
    @Async
    public 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日志方案具有以下优点:

  1. 无侵入性:通过注解实现,不干扰业务逻辑

  2. 灵活配置:可以控制记录参数、返回值等

  3. 异步处理:不影响主业务流程性能

  4. 结构化存储:便于后续查询和分析

  5. 易于扩展:可以轻松添加新的日志字段或处理逻辑

相关推荐
滴滴滴嘟嘟嘟.3 小时前
全屏定时提醒工具
java·开发语言
用户0806765692533 小时前
蓝桥云课-罗勇军算法精讲课(Python版)视频教程
后端
用户0806765692533 小时前
C#.NET高级班进阶VIP课程
后端
用户401426695853 小时前
Pandas数据分析实战(完结)
后端
用户84298142418103 小时前
js中如何隐藏eval关键字?
前端·javascript·后端
用户9884740373813 小时前
reCAPTCHA v2与v3的核心差异及应对方案
后端
せいしゅん青春之我3 小时前
【JavaEE初阶】网络原理——TCP处理先发后至问题
java·网络·笔记·网络协议·tcp/ip·java-ee
安卓开发者3 小时前
Docker 安装和配置 Elasticsearch 完整指南
elasticsearch·docker·容器
北邮-吴怀玉3 小时前
5.2 大数据方法论与实践指南-存储元数据治理
大数据·数据治理·元数据