使用注解将日志存入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. 易于扩展:可以轻松添加新的日志字段或处理逻辑

相关推荐
叫致寒吧1 小时前
Tomcat详解
java·tomcat
2501_941623325 小时前
人工智能赋能智慧农业互联网应用:智能种植、农业数据分析与产量优化实践探索》
大数据·人工智能
S***26755 小时前
基于SpringBoot和Leaflet的行政区划地图掩膜效果实战
java·spring boot·后端
马剑威(威哥爱编程)5 小时前
鸿蒙6开发视频播放器的屏幕方向适配问题
java·音视频·harmonyos
JIngJaneIL5 小时前
社区互助|社区交易|基于springboot+vue的社区互助交易系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·社区互助
晚风吹人醒.6 小时前
缓存中间件Redis安装及功能演示、企业案例
linux·数据库·redis·ubuntu·缓存·中间件
YangYang9YangYan6 小时前
网络安全专业职业能力认证发展路径指南
大数据·人工智能·安全·web安全
V***u4536 小时前
MS SQL Server partition by 函数实战二 编排考场人员
java·服务器·开发语言
这是程序猿6 小时前
基于java的ssm框架旅游在线平台
java·开发语言·spring boot·spring·旅游·旅游在线平台
i***t9196 小时前
基于SpringBoot和PostGIS的云南与缅甸的千里边境线实战
java·spring boot·spring