方案一:使用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日志方案具有以下优点:
-
无侵入性:通过注解实现,不干扰业务逻辑
-
灵活配置:可以控制记录参数、返回值等
-
异步处理:不影响主业务流程性能
-
结构化存储:便于后续查询和分析
-
易于扩展:可以轻松添加新的日志字段或处理逻辑