基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job日志分析系统(学习)

🚀 基于Kafka+ElasticSearch+MongoDB+Redis+XXL-Job的日志分析系统


🏗️ 系统架构

🎯 技术栈选型的深层考量

🚀 Apache Kafka - 消息传输的核心引擎
  • 在系统中的作用: 作为日志数据的缓冲层,解耦日志生产和消费,确保系统的高可用性
📊 MongoDB - 可靠的数据主存储
  • 选择理由: 文档型数据库天然适合存储结构化的日志数据
  • 关键特性: Change Streams功能实现数据变更的实时监听,副本集保证高可用
🔍 Elasticsearch - 强大的搜索分析引擎
  • 选择理由: 基于Lucene的全文搜索引擎,提供毫秒级的查询响应和强大的聚合分析能力
  • 在系统中的作用: 专门负责日志的全文检索、复杂查询和实时分析
Redis - 缓存层
  • 在系统中的作用: 缓存错误日志和热点查询结果,大幅提升系统响应速度
XXL-Job - 分布式任务调度
  • 在系统中的作用: 负责数据清理、统计分析等定时任务的执行

🎨 系统架构设计图

🔄 数据流转详细说明

1. 📥 日志收集阶段

各个微服务通过LogCollector统一收集日志,避免了直接与Kafka耦合,提供了更好的抽象层。

2. 🚀 消息传输阶段

Kafka的分区机制确保了数据的有序性和负载均衡,同时提供了消息的持久化保证。

3. ⚙️ 数据处理阶段

LogConsumer消费多个分区的数据,LogProcessor负责数据的清洗、验证和格式化。

4. 🎛️ 存储分发阶段

MultiStorageService根据业务规则智能地将数据分发到不同的存储系统。

5. 🔄 实时同步阶段

MongoDB Change Streams监听数据变更,实时同步到Elasticsearch,保证数据一致性。

6. 🌐 查询服务阶段

根据不同的查询需求,从最适合的存储系统获取数据。


🛠️ 核心组件深度实现

1. 📋 统一日志实体设计

🔑 核心字段说明
  • logId: 全局唯一标识符,用于日志去重和精确定位
  • timestamp: 日志产生的精确时间,支持毫秒级精度
  • level: 日志级别,支持标准的五级分类
  • serviceName: 服务标识,用于多服务环境下的日志分类
  • traceId: 分布式链路追踪ID,关联同一请求的所有日志
  • userId: 用户标识,支持用户行为分析和问题定位
java 复制代码
/**
 * 统一日志实体类 - 标准化的日志数据模型
 * 支持MongoDB文档存储,包含完整的日志元信息和链路追踪能力
 */
@Document(collection = "unified_logs")
public class UnifiedLogEntity {
    
    @Id
    private String id;
    
    @Field("logId")
    private String logId;           // 日志唯一标识
    
    @Field("timestamp")
    private LocalDateTime timestamp; // 日志时间戳
    
    @Field("level")
    private LogLevel level;         // 日志级别
    
    @Field("type")
    private LogType type;           // 日志类型
    
    @Field("serviceName")
    private String serviceName;     // 服务名称
    
    @Field("message")
    private String message;         // 日志消息
    
    @Field("traceId")
    private String traceId;         // 链路追踪ID
    
    @Field("userId")
    private String userId;          // 用户ID
    
    @Field("ipAddress")
    private String ipAddress;       // IP地址
    
    @Field("createTime")
    private LocalDateTime createTime; // 创建时间
    
    // 省略getter/setter方法
}

/**
 * 日志级别枚举 - 标准五级日志分类
 */
public enum LogLevel {
    DEBUG, INFO, WARN, ERROR, FATAL
}

/**
 * 日志类型枚举 - 按业务场景分类
 */
public enum LogType {
    APPLICATION, SYSTEM, SECURITY, PERFORMANCE
}

2. 📥 日志收集器 - 统一的数据入口

功能概述: 提供统一的日志收集接口,支持批量处理,将日志数据异步发送到Kafka消息队列,实现系统解耦。

java 复制代码
/**
 * 日志收集器 - 统一日志收集入口
 * 负责接收各微服务的日志数据,进行预处理后发送到Kafka
 */
@Slf4j
@Service
public class LogCollector {
    
    @Autowired
    private LogProducer logProducer;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    /**
     * 批量采集日志 - 支持高并发批量处理
     * 遍历日志列表,逐条发送到Kafka,提供详细的处理日志
     */
    public void collectBatch(List<UnifiedLogEntity> logs) {
        try {
            for (UnifiedLogEntity log : logs) {
                sendLogToKafka(log);
            }
            log.info("批量采集日志完成,共处理 {} 条日志", logs.size());
        } catch (Exception e) {
            log.error("批量采集日志失败: {}", e.getMessage(), e);
        }
    }
    
    /**
     * 发送日志到Kafka - 序列化日志对象并异步发送
     * 将日志实体转换为JSON格式,通过LogProducer发送到Kafka主题
     */
    private void sendLogToKafka(UnifiedLogEntity logEntity) {
        try {
            String logJson = objectMapper.writeValueAsString(logEntity);
            logProducer.sendLogMessage(logEntity.getLogId(), logJson);
        } catch (Exception e) {
            log.error("发送日志到Kafka失败: {}", e.getMessage(), e);
        }
    }
}

3. 🚀 Kafka生产者 - 消息发送

功能概述: 负责将日志消息异步发送到Kafka,具备智能重试机制和死信队列处理,确保消息不丢失。

🔧 关键技术特性:

  • 异步发送: 使用CompletableFuture实现非阻塞发送
  • 智能重试: 自动重试机制,失败后延迟重试
  • 死信队列: 对于无法恢复的消息,自动路由到死信队列
java 复制代码
/**
 * Kafka日志生产者 - 高可靠性消息发送
 * 提供异步发送、重试机制、死信队列等企业级特性
 */
@Slf4j
@Component
public class LogProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    @Value("${log-analytics.kafka.log-topic:log-analytics-logs}")
    private String logTopic;

    /**
     * 发送日志消息 - 异步发送到Kafka主题
     * 使用CompletableFuture实现异步发送,提供发送结果回调处理
     */
    public void sendLogMessage(String logId, String logMessage) {
        log.debug("准备发送日志消息到Topic: {}, LogId: {}", logTopic, logId);
        
        CompletableFuture<SendResult<String, String>> future = 
            kafkaTemplate.send(logTopic, logId, logMessage);
        
        future.whenComplete((result, ex) -> {
            if (ex == null) {
                log.debug("日志消息发送成功 [Topic: {}, Partition: {}, Offset: {}, LogId: {}]", 
                        result.getRecordMetadata().topic(),
                        result.getRecordMetadata().partition(),
                        result.getRecordMetadata().offset(),
                        logId);
            } else {
                log.error("日志消息发送失败 [LogId: {}]: {}", logId, ex.getMessage(), ex);
                handleSendFailure(logId, logMessage, ex);
            }
        });
    }
    
    /**
     * 处理发送失败的情况 - 智能重试和死信队列机制
     * 实现延迟重试,重试失败后自动发送到死信队列
     */
    private void handleSendFailure(String logId, String logMessage, Throwable ex) {
        // 重试机制和死信队列处理
        log.warn("日志消息发送失败,将尝试重试或发送到死信队列 [LogId: {}]", logId);
        
        try {
            Thread.sleep(1000);
            CompletableFuture<SendResult<String, String>> retryFuture = 
                kafkaTemplate.send(logTopic, logId, logMessage);
            retryFuture.whenComplete((result, retryEx) -> {
                if (retryEx == null) {
                    log.info("日志消息重试发送成功 [LogId: {}]", logId);
                } else {
                    log.error("日志消息重试发送仍然失败 [LogId: {}]: {}", 
                            logId, retryEx.getMessage());
                    sendToDeadLetterQueue(logId, logMessage);
                }
            });
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("重试发送日志消息时被中断 [LogId: {}]", logId);
        }
    }
    
    /**
     * 死信队列处理 - 无法恢复的消息最终处理
     * 将无法正常发送的消息发送到专门的死信队列,便于后续人工处理
     */
    private void sendToDeadLetterQueue(String logId, String logMessage) {
        String deadLetterTopic = logTopic + "-dead-letter";
        try {
            kafkaTemplate.send(deadLetterTopic, logId, logMessage);
            log.info("日志消息已发送到死信队列 [LogId: {}, Topic: {}]", logId, deadLetterTopic);
        } catch (Exception e) {
            log.error("发送到死信队列也失败了 [LogId: {}]: {}", logId, e.getMessage(), e);
        }
    }
}

4. 📨 消息消费者 - 可靠的数据处理

功能概述: 从Kafka消费日志消息,进行数据解析、处理和存储分发,具备异常处理和重试机制。

java 复制代码
/**
 * Kafka日志消费者 - 可靠的消息处理
 * 监听Kafka主题,消费日志消息并进行后续处理流程
 */
@Slf4j
@Component
@Lazy(value = false)
public class LogConsumer {

    @Autowired
    private ObjectMapper objectMapper;
    
    @Autowired
    private LogProcessor logProcessor;
    
    @Autowired
    private MultiStorageService multiStorageService;

    /**
     * 处理日志消息 - Kafka监听器主方法
     * 监听指定主题,处理消费到的日志消息,包含完整的异常处理和确认机制
     */
    @KafkaListener(
            topics = "${log-analytics.kafka.log-topic:log-analytics-logs}",
            groupId = "${log-analytics.kafka.consumer-group-id:log-analytics-consumer-group}",
            containerFactory = "kafkaListenerContainerFactory"
    )
    public void processLogMessage(
            ConsumerRecord<String, String> record, 
            Acknowledgment ack,
            @Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
            @Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
            @Header(KafkaHeaders.OFFSET) long offset) {
        
        String logId = record.key();
        log.info("开始处理日志消息 [Topic: {}, Partition: {}, Offset: {}, LogId: {}]",
                topic, partition, offset, logId);
        
        try {
            String logMessage = record.value();
            
            // 1. 解析日志消息 - JSON反序列化为日志实体
            UnifiedLogEntity logEntity = parseLogMessage(logMessage);
            if (logEntity == null) {
                log.error("日志消息解析失败,跳过处理 [LogId: {}]", logId);
                ack.acknowledge();
                return;
            }
            
            // 2. 数据处理和清洗 - 数据验证和格式化
            UnifiedLogEntity processedLog = logProcessor.processLog(logEntity);
            
            // 3. 存储到多个存储引擎 - 智能存储分发
            multiStorageService.storeLog(processedLog);
            
            log.info("日志消息处理成功 [Topic: {}, Partition: {}, Offset: {}, LogId: {}]",
                    topic, partition, offset, logId);

        } catch (Exception e) {
            log.error("日志消息处理失败 [Topic: {}, Partition: {}, Offset: {}, LogId: {}]: {}", 
                    topic, partition, offset, logId, e.getMessage(), e);
            
            if (shouldRetry(e)) {
                throw new RuntimeException("日志处理失败,需要重试", e);
            } else {
                log.warn("日志消息处理失败但不重试,直接确认 [LogId: {}]", logId);
            }
        } finally {
            ack.acknowledge();
        }
    }

    /**
     * 解析日志消息 - JSON字符串转换为日志实体
     * 将Kafka消息中的JSON字符串反序列化为UnifiedLogEntity对象
     */
    private UnifiedLogEntity parseLogMessage(String logMessage) {
        try {
            return objectMapper.readValue(logMessage, UnifiedLogEntity.class);
        } catch (Exception e) {
            log.error("解析日志消息失败: {}", e.getMessage());
            return null;
        }
    }

    /**
     * 判断是否需要重试 - 智能异常分类处理
     * 根据异常类型判断是否为可重试的临时性异常
     */
    private boolean shouldRetry(Exception e) {
        // 网络异常、临时性异常等可以重试
        if (e instanceof java.net.ConnectException ||
            e instanceof java.net.SocketTimeoutException ||
            e instanceof org.springframework.dao.TransientDataAccessException) {
            return true;
        }
        return false;
    }
}

5. 🎛️ 智能多存储引擎服务 - 数据分发中心

功能概述: 根据业务规则智能地将日志数据分发到不同的存储系统,实现存储策略的统一管理和优化。

📊 存储策略详解

🍃 MongoDB主存储策略:

  • 所有日志数据首先写入MongoDB,确保数据的完整性和一致性

⚡ Redis缓存策略:

  • 错误级别日志:ERROR级别的日志自动缓存,便于快速故障排查

🔍 Elasticsearch同步策略:

  • 通过MongoDB Change Streams实现实时同步
  • 按服务名和日期自动创建索引,优化查询性能
  • 支持全文检索
java 复制代码
/**
 * 多存储引擎服务 - 智能存储分发中心
 * 根据业务规则将日志数据分发到MongoDB、Redis等不同存储系统
 */
@Slf4j
@Service
public class MultiStorageService {
    
    @Autowired
    private MongoLogService mongoLogService;
    
    @Autowired
    private LogCacheService logCacheService;
    
    /**
     * 存储单条日志到多个存储引擎
     * MongoDB作为主存储,Redis作为缓存,Elasticsearch通过Change Stream自动同步
     */
    public void storeLog(UnifiedLogEntity logEntity) {
        if (logEntity == null) {
            log.error("日志实体为空,跳过存储");
            return;
        }
        
        String logId = logEntity.getLogId();
        log.info("开始存储日志到存储引擎 [LogId: {}]", logId);
        
        // 异步并行存储到MongoDB和Redis - 提高存储性能
        CompletableFuture<Void> mongoFuture = CompletableFuture.runAsync(() -> {
            try {
                mongoLogService.saveLog(logEntity);
                log.info("日志存储到MongoDB成功 [LogId: {}]", logId);
            } catch (Exception e) {
                log.error("日志存储到MongoDB失败 [LogId: {}]: {}", logId, e.getMessage(), e);
                throw new RuntimeException("MongoDB存储失败", e);
            }
        });
        
        CompletableFuture<Void> cacheFuture = CompletableFuture.runAsync(() -> {
            try {
                if (shouldCache(logEntity)) {
                    logCacheService.cacheLog(logEntity);
                    log.info("日志缓存到Redis成功 [LogId: {}]", logId);
                }
            } catch (Exception e) {
                log.error("日志缓存到Redis失败 [LogId: {}]: {}", logId, e.getMessage(), e);
            }
        });
        
        // 等待存储操作完成 - 确保数据一致性
        CompletableFuture.allOf(mongoFuture, cacheFuture)
            .whenComplete((result, throwable) -> {
                if (throwable != null) {
                    log.error("存储引擎存储过程中发生异常 [LogId: {}]: {}", 
                            logId, throwable.getMessage());
                } else {
                    log.debug("日志存储完成,Elasticsearch将通过Change Stream自动同步 [LogId: {}]", 
                            logId);
                }
            }).join();
    }
    
    /**
     * 判断日志是否需要缓存到Redis - 智能缓存策略
     * 根据日志级别、时间、链路追踪等维度判断是否需要缓存
     */
    private boolean shouldCache(UnifiedLogEntity logEntity) {
        if (logEntity == null) {
            return false;
        }
        
        // 缓存策略:ERROR级别的日志、最近1小时的日志、包含链路追踪ID的日志
        if (LogLevel.ERROR.equals(logEntity.getLevel()) ||
            LogLevel.FATAL.equals(logEntity.getLevel())) {
            return true;
        }
        
        if (logEntity.getTimestamp() != null && 
            logEntity.getTimestamp().isAfter(LocalDateTime.now().minusHours(1))) {
            return true;
        }
        
        if (logEntity.getTraceId() != null && !logEntity.getTraceId().isEmpty()) {
            return true;
        }
        
        return false;
    }
}

6. 🔄 MongoDB Change Stream同步服务 - 实时数据同步

功能概述: 监听MongoDB数据变更事件,实时同步数据到Elasticsearch,确保搜索引擎数据的实时性和一致性。

java 复制代码
/**
 * MongoDB变更流监听服务 - 实时数据同步
 * 监听MongoDB的Change Stream事件,自动同步新增数据到Elasticsearch
 */
@Service
@Slf4j
public class MongoChangeStreamService {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private EnhancedElasticsearchService elasticsearchService;

    /**
     * 启动Change Stream监听 - 应用启动后自动开始监听
     * 监听unified_logs集合的数据变更,实现实时同步
     */
    @Async
    @EventListener(ApplicationReadyEvent.class)
    public void listen() {
        mongoTemplate.getCollection("unified_logs")
                .watch()
                .forEach(this::processChange);
    }

    /**
     * 处理数据变更事件 - 解析变更类型并同步到Elasticsearch
     * 当MongoDB中有新数据插入时,自动索引到Elasticsearch
     */
    private void processChange(ChangeStreamDocument<org.bson.Document> changeStreamDocument) {
        OperationType operationType = changeStreamDocument.getOperationType();

        // 判断操作类型 - 目前只处理INSERT操作
        if (operationType == OperationType.INSERT) {
            Document document = changeStreamDocument.getFullDocument();
            UnifiedLogEntity unifiedLogEntity = mongoTemplate.getConverter().read(UnifiedLogEntity.class, document);
            elasticsearchService.indexLog(unifiedLogEntity);
            log.info("日志存储到ElasticSearch成功 [LogId: {}]", unifiedLogEntity.getLogId());
        }
    }
}

ElasticSearch服务,同步数据到ElasticSearch中并且提供查询索引功能

java 复制代码
@Slf4j
@Service
public class EnhancedElasticsearchService {
    
    @Autowired
    private ElasticsearchClient elasticsearchClient;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    private static final String INDEX_PREFIX = "log-analytics";
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    

    


    /**
     * 索引单条日志
     */
    public void indexLog(UnifiedLogEntity logEntity) {
        if (logEntity == null) {
            log.error("日志实体为空,跳过ElasticSearch索引");
            return;
        }
        
        try {
            String indexName = generateIndexName(logEntity);
            
            // 确保索引存在
            ensureIndexExists(indexName);
            
            // 转换为Map以便索引
            Map<String, Object> logMap = convertLogToMap(logEntity);
            
            IndexRequest<Map<String, Object>> request = IndexRequest.of(i -> i
                    .index(indexName)
                    .id(logEntity.getLogId())
                    .document(logMap)
            );
            
            elasticsearchClient.index(request);
            log.debug("日志索引到ElasticSearch成功 [LogId: {}, Index: {}]", 
                    logEntity.getLogId(), indexName);
            
        } catch (Exception e) {
            log.error("日志索引到ElasticSearch失败 [LogId: {}]: {}", 
                    logEntity.getLogId(), e.getMessage(), e);
            throw new RuntimeException("ElasticSearch索引失败", e);
        }
    }
    

    /**
     * 搜索日志
     */
    public List<UnifiedLogEntity> searchLogs(String serviceName, 
                                           LogLevel level,
                                           String keyword,
                                           LocalDateTime startTime,
                                           LocalDateTime endTime,
                                           int from, int size) {
        try {
            String indexPattern = generateIndexPattern(serviceName);
            
            SearchRequest.Builder searchBuilder = new SearchRequest.Builder()
                    .index(indexPattern)
                    .from(from)
                    .size(size);
            
            // 构建查询条件
            Query query = buildQuery(
                    serviceName, level, keyword, startTime, endTime);
            
            searchBuilder.query(query);
            
            // 添加排序
            searchBuilder.sort(s -> s
                    .field(f -> f
                            .field("timestamp")
                            .order(SortOrder.Desc)
                    )
            );
            
            SearchResponse<Map> response = elasticsearchClient.search(searchBuilder.build(), Map.class);
            
            List<UnifiedLogEntity> logs = new ArrayList<>();
            for (Hit<Map> hit : response.hits().hits()) {
                try {
                    UnifiedLogEntity log = convertMapToLog(hit.source());
                    logs.add(log);
                } catch (Exception e) {
                    log.warn("转换搜索结果失败: {}", e.getMessage());
                }
            }
            
            log.info("ElasticSearch搜索完成,返回结果数量: {}", logs.size());
            return logs;
            
        } catch (Exception e) {
            log.error("ElasticSearch搜索失败: {}", e.getMessage(), e);
            return new ArrayList<>();
        }
    }

    
    /**
     * 删除过期索引
     */
    public void deleteExpiredIndices(int retentionDays) {
        try {
            LocalDate cutoffDate = LocalDate.now().minusDays(retentionDays);
            
            // 获取所有日志分析索引
            var indicesResponse = elasticsearchClient.cat().indices(i -> i.index(INDEX_PREFIX + "-*"));
            
            for (var indexInfo : indicesResponse.valueBody()) {
                String indexName = indexInfo.index();
                
                // 从索引名称中提取日期
                String dateStr = extractDateFromIndexName(indexName);
                if (dateStr != null) {
                    try {
                        LocalDate indexDate = LocalDate.parse(dateStr, DATE_FORMATTER);
                        
                        if (indexDate.isBefore(cutoffDate)) {
                            DeleteIndexRequest deleteRequest = DeleteIndexRequest.of(d -> d.index(indexName));
                            elasticsearchClient.indices().delete(deleteRequest);
                            log.info("删除过期ElasticSearch索引: {}", indexName);
                        }
                    } catch (Exception e) {
                        log.warn("解析索引日期失败 [Index: {}]: {}", indexName, e.getMessage());
                    }
                }
            }
            
        } catch (Exception e) {
            log.error("删除过期ElasticSearch索引失败: {}", e.getMessage(), e);
        }
    }

    
    /**
     * 生成索引名称
     */
    private String generateIndexName(UnifiedLogEntity logEntity) {
        String dateStr = logEntity.getTimestamp().toLocalDate().format(DATE_FORMATTER);
        return String.format("%s-%s-%s", INDEX_PREFIX, logEntity.getServiceName(), dateStr);
    }
    
    /**
     * 生成索引模式
     */
    private String generateIndexPattern(String serviceName) {
        if (serviceName != null && !serviceName.trim().isEmpty()) {
            return INDEX_PREFIX + "-" + serviceName + "-*";
        } else {
            return INDEX_PREFIX + "-*";
        }
    }
    
    /**
     * 确保索引存在
     */
    private void ensureIndexExists(String indexName) {
        try {
            ExistsRequest existsRequest = ExistsRequest.of(e -> e.index(indexName));
            boolean exists = elasticsearchClient.indices().exists(existsRequest).value();
            
            if (!exists) {
                createIndex(indexName);
            }
        } catch (Exception e) {
            log.error("检查索引是否存在失败 [Index: {}]: {}", indexName, e.getMessage(), e);
        }
    }
    
    /**
     * 创建索引
     */
    private void createIndex(String indexName) {
        try {
            CreateIndexRequest createRequest = CreateIndexRequest.of(c -> c
                    .index(indexName)
                    .mappings(createLogMapping())
                    .settings(s -> s
                            .numberOfShards("1")
                            .numberOfReplicas("1")
                            .refreshInterval(t -> t.time("1s"))
                    )
            );
            
            elasticsearchClient.indices().create(createRequest);
            log.info("创建ElasticSearch索引成功: {}", indexName);
            
        } catch (Exception e) {
            log.error("创建ElasticSearch索引失败 [Index: {}]: {}", indexName, e.getMessage(), e);
        }
    }
    
    /**
     * 创建日志映射
     */
    private TypeMapping createLogMapping() {
        Map<String, Property> properties = new HashMap<>();
        
        // 基础字段
        properties.put("logId", Property.of(p -> p.keyword(k -> k)));
        properties.put("serviceName", Property.of(p -> p.keyword(k -> k)));
        properties.put("timestamp", Property.of(p -> p.date(d -> d.format("yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss||epoch_millis"))));
        properties.put("level", Property.of(p -> p.keyword(k -> k)));
        properties.put("type", Property.of(p -> p.keyword(k -> k)));
        
        // 消息字段(支持全文搜索)
        properties.put("message", Property.of(p -> p.text(t -> t.analyzer("standard"))));
        
        // 链路追踪字段
        properties.put("traceId", Property.of(p -> p.keyword(k -> k)));
        
        // 用户和网络信息字段
        properties.put("userId", Property.of(p -> p.keyword(k -> k)));
        properties.put("ipAddress", Property.of(p -> p.ip(i -> i)));
        
        // 元数据字段
        properties.put("createTime", Property.of(p -> p.date(d -> d.format("yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss||epoch_millis"))));
        
        return TypeMapping.of(t -> t.properties(properties));
    }
    
    /**
     * 构建查询条件
     */
    private co.elastic.clients.elasticsearch._types.query_dsl.Query buildQuery(
            String serviceName, 
            LogLevel level,
            String keyword,
            LocalDateTime startTime,
            LocalDateTime endTime) {
        
        var boolQuery = BoolQuery.of(b -> {
            var builder = b;
            
            // 服务名过滤
            if (serviceName != null && !serviceName.trim().isEmpty()) {
                builder = builder.filter(f -> f.term(t -> t.field("serviceName").value(serviceName)));
            }
            
            // 日志级别过滤
            if (level != null) {
                builder = builder.filter(f -> f.term(t -> t.field("level").value(level.name())));
            }
            
            // 时间范围过滤
            if (startTime != null || endTime != null) {
                builder = builder.filter(f -> f.range(r -> {
                    var rangeBuilder = r.field("timestamp");
                    if (startTime != null) {
                        rangeBuilder = rangeBuilder.gte(JsonData.of(startTime.toString()));
                    }
                    if (endTime != null) {
                        rangeBuilder = rangeBuilder.lte(JsonData.of(endTime.toString()));
                    }
                    return rangeBuilder;
                }));
            }
            
            // 关键词搜索
            if (keyword != null && !keyword.trim().isEmpty()) {
                builder = builder.must(m -> m.multiMatch(mm -> mm
                        .query(keyword)
                        .fields("message", "serviceName")
                        .type(TextQueryType.BestFields)
                ));
            }
            
            return builder;
        });
        
        return Query.of(q -> q.bool(boolQuery));
    }
    
    /**
     * 将日志实体转换为Map
     */
    private Map<String, Object> convertLogToMap(UnifiedLogEntity logEntity) {
        try {
            String json = objectMapper.writeValueAsString(logEntity);
            return objectMapper.readValue(json, Map.class);
        } catch (Exception e) {
            log.error("转换日志实体为Map失败: {}", e.getMessage(), e);
            return new HashMap<>();
        }
    }
    
    /**
     * 将Map转换为日志实体
     */
    private UnifiedLogEntity convertMapToLog(Map<String, Object> map) {
        try {
            String json = objectMapper.writeValueAsString(map);
            return objectMapper.readValue(json, UnifiedLogEntity.class);
        } catch (Exception e) {
            log.error("转换Map为日志实体失败: {}", e.getMessage(), e);
            return null;
        }
    }
    
    /**
     * 从索引名称中提取日期
     */
    private String extractDateFromIndexName(String indexName) {
        try {
            // 索引名称格式: log-analytics-application-yyyy-MM-dd
            String[] parts = indexName.split("-");
            if (parts.length >= 3) {
                // 取最后三部分作为日期
                int len = parts.length;
                return parts[len-3] + "-" + parts[len-2] + "-" + parts[len-1];
            }
        } catch (Exception e) {
            log.warn("从索引名称提取日期失败 [Index: {}]: {}", indexName, e.getMessage());
        }
        return null;
    }
}

7. ⚡ Redis缓存服务

功能概述: 提日志缓存服务,支持错误日志专门缓存

java 复制代码
/**
 * Redis日志缓存服务 - 高性能缓存管理
 * 提供日志数据的高速缓存,支持错误日志专门存储和智能缓存策略
 */
@Slf4j
@Service
public class LogCacheService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    private static final String LOG_KEY_PREFIX = "log-analytics:log:";
    private static final String ERROR_LOG_SET_KEY = "log-analytics:error-logs";
    private static final Duration LOG_TTL = Duration.ofHours(2);

    /**
     * 缓存单条日志 - 智能缓存策略
     * 将日志数据序列化后存储到Redis,错误日志额外存储到专门的有序集合
     */
    public void cacheLog(UnifiedLogEntity logEntity) {
        if (logEntity == null || logEntity.getLogId() == null) {
            log.warn("日志实体或LogId为空,跳过Redis缓存");
            return;
        }
        
        try {
            String key = LOG_KEY_PREFIX + logEntity.getLogId();
            String value = objectMapper.writeValueAsString(logEntity);
            redisTemplate.opsForValue().set(key, value, LOG_TTL);
            
            // 如果是错误日志,额外缓存到错误日志集合中
            if (logEntity.getLevel() != null && 
                (logEntity.getLevel().name().equals("ERROR") || 
                 logEntity.getLevel().name().equals("FATAL"))) {
                cacheErrorLog(logEntity);
            }
            
        } catch (Exception e) {
            log.error("日志缓存到Redis失败 [LogId: {}]: {}", 
                    logEntity.getLogId(), e.getMessage(), e);
        }
    }
    
    /**
     * 缓存错误日志到专门的集合 - 错误日志优先处理
     * 使用Redis有序集合存储错误日志,按时间戳排序,便于快速查询
     */
    private void cacheErrorLog(UnifiedLogEntity logEntity) {
        try {
            String logJson = objectMapper.writeValueAsString(logEntity);
            double score = System.currentTimeMillis();
            redisTemplate.opsForZSet().add(ERROR_LOG_SET_KEY, logJson, score);
            redisTemplate.expire(ERROR_LOG_SET_KEY, Duration.ofHours(24));
            
            log.debug("错误日志已缓存到Redis [LogId: {}]", logEntity.getLogId());
            
        } catch (Exception e) {
            log.error("缓存错误日志失败 [LogId: {}]: {}", 
                    logEntity.getLogId(), e.getMessage(), e);
        }
    }

    /**
     * 获取所有缓存的日志 - 分页查询支持
     * 从Redis获取缓存的日志数据,支持按服务名过滤和分页查询
     */
    public List<UnifiedLogEntity> getAllCachedLogs(String serviceName, int page, int size) {
        List<UnifiedLogEntity> logs = new ArrayList<>();
        
        try {
            Set<String> keys = redisTemplate.keys(LOG_KEY_PREFIX + "*");
            
            if (keys != null && !keys.isEmpty()) {
                List<String> keyList = new ArrayList<>(keys);
                
                int start = page * size;
                int end = Math.min(start + size, keyList.size());
                
                if (start < keyList.size()) {
                    List<String> pageKeys = keyList.subList(start, end);
                    
                    for (String key : pageKeys) {
                        try {
                            String value = redisTemplate.opsForValue().get(key);
                            if (value != null) {
                                UnifiedLogEntity logEntity = 
                                    objectMapper.readValue(value, UnifiedLogEntity.class);
                                
                                if (serviceName == null || serviceName.trim().isEmpty() || 
                                    serviceName.equals(logEntity.getServiceName())) {
                                    logs.add(logEntity);
                                }
                            }
                        } catch (Exception e) {
                            log.warn("解析Redis日志数据失败 [Key: {}]: {}", key, e.getMessage());
                        }
                    }
                }
            }
            
            // 按时间戳倒序排列 - 最新日志优先显示
            logs.sort((a, b) -> b.getTimestamp().compareTo(a.getTimestamp()));
            
        } catch (Exception e) {
            log.error("从Redis获取缓存日志失败: {}", e.getMessage(), e);
        }
        
        return logs;
    }
}

8. ⏰ 分布式定时任务系统 - 数据生命周期管理

功能概述: 基于XXL-Job实现的分布式定时任务,负责清理过期数据,维护系统存储空间和性能。

java 复制代码
/**
 * 数据清理定时任务 - 系统维护和优化
 * 定期清理各存储系统中的过期数据,保持系统性能和存储空间
 */
@Slf4j
@Component
public class DataCleanupJob {
    
    @Autowired
    private MongoLogService mongoLogService;
    
    @Autowired
    private EnhancedElasticsearchService elasticsearchService;
    
    @Autowired
    private LogCacheService logCacheService;

    /**
     * 清理过期日志数据 - 定时清理任务主方法
     * XXL-Job配置:每天凌晨2点执行,Cron表达式:0 0 2 * * ?
     * 清理30天前的历史数据,释放存储空间
     */
    @XxlJob("cleanupExpiredLogs")
    public void cleanupExpiredLogs() {
        log.info("开始执行日志数据清理任务...");
        
        try {
            // 清理30天前的日志数据
            LocalDateTime expireTime = LocalDateTime.now().minusDays(30);
            
            // 清理MongoDB过期日志 - 删除主存储中的历史数据
            long mongoDeletedCount = mongoLogService.deleteExpiredLogs(expireTime);
            log.info("MongoDB清理过期日志完成,删除数量: {}", mongoDeletedCount);
            
            // 清理Elasticsearch过期索引 - 删除搜索引擎中的历史索引
            elasticsearchService.deleteExpiredIndices(30);
            log.info("Elasticsearch清理过期索引完成");
            
            // 清理Redis过期缓存 - 清理缓存中的过期数据
            logCacheService.cleanupExpiredCache();
            log.info("Redis清理过期缓存完成");
            
            log.info("日志数据清理任务执行完成");
            
        } catch (Exception e) {
            log.error("日志数据清理任务执行失败: {}", e.getMessage(), e);
            throw new RuntimeException("数据清理任务失败", e);
        }
    }
}

9. 🌐 RESTful API服务 - 统一查询接口

功能概述: 提供RESTful风格的API接口,支持多维度日志查询、错误日志快速检索和演示数据生成。

🎯 核心接口功能

🔍 日志搜索接口 (/search-logs):

  • 支持多维度组合查询(服务名、日志级别、关键词、时间范围)
  • 基于Elasticsearch的全文检索能力
  • 支持复杂的查询语法和聚合分析
  • 提供分页和排序功能

❌ 错误日志接口 (/error-logs):

  • 专门针对错误日志的快速查询
  • 直接从Redis缓存获取,响应速度极快
java 复制代码
/**
 * 日志分析演示控制器
 * 提供日志分析系统的演示功能和测试接口
 */
@RestController
@RequestMapping("/api/demo")
@CrossOrigin(origins = "*")
public class LogAnalyticsDemoController {

    @Autowired
    private LogCollector logCollector;

    @Autowired
    private EnhancedElasticsearchService elasticsearchService;

    @Autowired
    private MongoLogService mongoLogService;

    @Autowired
    private LogCacheService logCacheService;

    @Autowired
    private MultiStorageService multiStorageService;



    /**
     * 生成演示日志数据
     */
    @GetMapping("/generate-logs")
    public ResponseEntity<Map<String, Object>> generateDemoLogs(@RequestParam(defaultValue = "100") int count) {
        try {
            List<UnifiedLogEntity> logs = new ArrayList<>();
            Random random = new Random();
            
            String[] services = {"user-service", "order-service"};
            String[] messages = {
                "User login successful",
                "Order created successfully", 
                "Payment processed",
                "Inventory updated",
                "Database connection timeout",
                "Service unavailable"
            };

            for (int i = 0; i < count; i++) {
                UnifiedLogEntity log = new UnifiedLogEntity();
                log.setLogId(UUID.randomUUID().toString());
                log.setTimestamp(LocalDateTime.now().minusDays(2)); // 过去24小时内
                log.setLevel(LogLevel.values()[random.nextInt(LogLevel.values().length)]);
                log.setType(LogType.APPLICATION);
                log.setServiceName(services[random.nextInt(services.length)]);
                log.setMessage(messages[random.nextInt(messages.length)]);
                log.setTraceId("trace-" + UUID.randomUUID().toString().substring(0, 8));
                log.setUserId("user-" + (1000 + random.nextInt(9000)));
                log.setIpAddress("192.168.1." + (1 + random.nextInt(254)));
                log.setCreateTime(LocalDateTime.now());
                
                logs.add(log);
            }

            // 批量收集日志
            logCollector.collectBatch(logs);

            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("message", "成功生成 " + count + " 条演示日志");
            response.put("count", count);
            
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "生成演示日志失败: " + e.getMessage());
            return ResponseEntity.internalServerError().body(response);
        }
    }

    /**
     * 基于Elasticsearch的日志搜索
     */
    @GetMapping("/search-logs")
    public ResponseEntity<Map<String, Object>> searchLogs(
            @RequestParam(required = false) String serviceName,
            @RequestParam(required = false) String level,
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) String startTime,
            @RequestParam(required = false) String endTime,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        try {
            // 解析时间参数
            LocalDateTime start = null;
            LocalDateTime end = null;
            
            if (startTime != null && !startTime.trim().isEmpty()) {
                start = LocalDateTime.parse(startTime);
            }
            if (endTime != null && !endTime.trim().isEmpty()) {
                end = LocalDateTime.parse(endTime);
            }
            
            // 解析日志级别
            LogLevel logLevel = null;
            if (level != null && !level.trim().isEmpty()) {
                try {
                    logLevel = LogLevel.valueOf(level.toUpperCase());
                } catch (IllegalArgumentException e) {
                    Map<String, Object> response = new HashMap<>();
                    response.put("success", false);
                    response.put("message", "无效的日志级别: " + level);
                    return ResponseEntity.badRequest().body(response);
                }
            }
            
            // 执行搜索
            List<UnifiedLogEntity> logs = elasticsearchService.searchLogs(
                    serviceName, logLevel, keyword, start, end, page * size, size);
            
            Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("data", logs);
            response.put("page", page);
            response.put("size", size);
            response.put("total", logs.size());
            response.put("timestamp", LocalDateTime.now());
            
            return ResponseEntity.ok(response);
            
        } catch (Exception e) {
            Map<String, Object> response = new HashMap<>();
            response.put("success", false);
            response.put("message", "日志搜索失败: " + e.getMessage());
            response.put("timestamp", LocalDateTime.now());
            return ResponseEntity.internalServerError().body(response);
        }
    }


    /**
     * 获取错误日志
     */
    @GetMapping("/error-logs")
    public ResponseEntity<Map<String, Object>> getErrorLogs() {

        List<UnifiedLogEntity> errorLogs = logCacheService.getErrorLogs();
        Map<String, Object> response = new HashMap<>();
            response.put("success", true);
            response.put("errorLogs", errorLogs);
            response.put("count", errorLogs.size());
            response.put("timestamp", LocalDateTime.now());
            return ResponseEntity.ok(response);
    }



}

🎯 总结

这个日志分析系统通过Kafka实现高吞吐量的消息传输,MongoDB提供可靠的数据存储,Elasticsearch支持强大的搜索分析,Redis提供高速缓存,XXL-Job负责定时任务调度。

相关推荐
single-life35 分钟前
Linux 下 部署es+nebula(附带内网部署方式)
linux·运维·elasticsearch·nebula
DBA小马哥1 小时前
文档型数据库MongoDB迁移替换至金仓数据库上线流程周期全解析
数据库·mongodb·文档型数据库
一个向上的运维者2 小时前
实战解析|EFK 日志系统数据同步问题全解(附核心配置模板)
elasticsearch·云原生
cicada153 小时前
分享一个git日常开发流程
大数据·git·elasticsearch
_oP_i4 小时前
Windows 下往 Elasticsearch 添加数据
大数据·windows·elasticsearch
咕叽咕叽的汪4 小时前
Es/Kibana7.17.9中数据迁移到openSearch3.4.0【DockerDesktop模拟】
运维·spring boot·elasticsearch·docker·容器·devops
萧曵 丶4 小时前
Elasticsearch 高频面试题(高级 Java 开发版)
java·elasticsearch
萧曵 丶4 小时前
Kafka 全面解析
分布式·kafka
余道各努力,千里自同风4 小时前
node.js 操作 MongoDB
数据库·mongodb·node.js
oMcLin4 小时前
如何在 Fedora 34 上通过配置 MongoDB 集群,提升电商平台的用户数据存储与查询响应速度?
数据库·mongodb