package com.demo.seecanalysisbackend.service; import com.demo.seecanalysisbackend.dto.LogSearchRequest; import com.demo.seecanalysisbackend.dto.LogSearchResponse; import com.demo.seecanalysisbackend.model.LogEntry; import com.demo.seecanalysisbackend.repository.LogEntryRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class LogSearchService { private final LogEntryRepository logEntryRepository; private static final Map SORT_FIELD_MAPPING = createSortFieldMapping(); private static final Set ALLOWED_SORT_PROPERTIES = Set.of( "timestamp", "level", "serviceName", "traceId", "spanId", "bizName", "className", "methodName", "thread", "application", "rawMessage" ); public LogSearchResponse searchLogs(LogSearchRequest request) { log.info("Searching logs with criteria: {}", request); Pageable pageable = createPageable(request); Page logPage = performSearch(request, pageable); List logDtos = logPage.getContent().stream() .map(this::convertToDto) .collect(Collectors.toList()); return LogSearchResponse.builder() .logs(logDtos) .totalCount(logPage.getTotalElements()) .currentPage(logPage.getNumber()) .pageSize(logPage.getSize()) .totalPages(logPage.getTotalPages()) .hasNext(logPage.hasNext()) .hasPrevious(logPage.hasPrevious()) .build(); } private Page performSearch(LogSearchRequest request, Pageable pageable) { if (request.getSearchText() != null && !request.getSearchText().trim().isEmpty()) { return logEntryRepository.findByTimestampBetweenAndMessage( request.getStartTime(), request.getEndTime(), request.getSearchText(), pageable); } if (request.getBizNames() != null && !request.getBizNames().isEmpty()) { return logEntryRepository.findByTimestampBetweenAndBizNameIn( request.getStartTime(), request.getEndTime(), request.getBizNames(), pageable); } if (request.getServices() != null && !request.getServices().isEmpty()) { String serviceName = request.getServices().getFirst(); return logEntryRepository.findByTimestampBetweenAndServiceName( request.getStartTime(), request.getEndTime(), serviceName, pageable); } if (request.getTraceId() != null && !request.getTraceId().trim().isEmpty()) { return logEntryRepository.findByTimestampBetweenAndTraceId( request.getStartTime(), request.getEndTime(), request.getTraceId(), pageable); } if (request.getLevels() != null && !request.getLevels().isEmpty()) { return logEntryRepository.findByTimestampBetweenAndLevelIn( request.getStartTime(), request.getEndTime(), request.getLevels(), pageable); } if (request.getApplications() != null && !request.getApplications().isEmpty()) { return logEntryRepository.findByTimestampBetweenAndApplicationIn( request.getStartTime(), request.getEndTime(), request.getApplications(), pageable); } return logEntryRepository.findByTimestampBetween( request.getStartTime(), request.getEndTime(), pageable); } private Pageable createPageable(LogSearchRequest request) { Sort.Direction direction = "asc".equalsIgnoreCase(request.getSortDirection()) ? Sort.Direction.ASC : Sort.Direction.DESC; String requestedSortBy = request.getSortBy() == null ? "timestamp" : request.getSortBy(); String normalizedSortProperty = SORT_FIELD_MAPPING.getOrDefault(requestedSortBy, requestedSortBy); String actualSortField = ALLOWED_SORT_PROPERTIES.contains(normalizedSortProperty) ? normalizedSortProperty : "timestamp"; return PageRequest.of(request.getPage(), request.getSize(), Sort.by(direction, actualSortField)); } private static Map createSortFieldMapping() { Map mapping = new HashMap<>(); // Accept external ES field names and normalize them to entity property names. mapping.put("@timestamp", "timestamp"); mapping.put("log_level", "level"); mapping.put("service_name", "serviceName"); mapping.put("trace_id", "traceId"); mapping.put("span_id", "spanId"); mapping.put("biz_name", "bizName"); mapping.put("bizName", "bizName"); mapping.put("timestamp", "timestamp"); mapping.put("level", "level"); mapping.put("serviceName", "serviceName"); mapping.put("traceId", "traceId"); mapping.put("spanId", "spanId"); return mapping; } private LogSearchResponse.LogEntryDto convertToDto(LogEntry logEntry) { return LogSearchResponse.LogEntryDto.builder() .id(logEntry.getId()) .timestamp(logEntry.getTimestamp() != null ? logEntry.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : null) .rawMessage(logEntry.getRawMessage()) .serviceName(logEntry.getServiceName()) .thread(logEntry.getThread()) .level(logEntry.getLevel()) .className(logEntry.getClassName()) .methodName(logEntry.getMethodName()) .traceId(logEntry.getTraceId()) .spanId(logEntry.getSpanId()) .bizName(logEntry.getBizName()) .context(logEntry.getContext()) .build(); } }