update
This commit is contained in:
453
src/main/java/research/loghunter/service/ScanService.java
Normal file
453
src/main/java/research/loghunter/service/ScanService.java
Normal file
@@ -0,0 +1,453 @@
|
||||
package research.loghunter.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import research.loghunter.entity.*;
|
||||
import research.loghunter.repository.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ScanService {
|
||||
|
||||
private final ServerRepository serverRepository;
|
||||
private final ServerLogPathRepository logPathRepository;
|
||||
private final PatternRepository patternRepository;
|
||||
private final ScanHistoryRepository scanHistoryRepository;
|
||||
private final ErrorLogRepository errorLogRepository;
|
||||
private final ScannedFileRepository scannedFileRepository;
|
||||
private final SftpService sftpService;
|
||||
private final LogParserService logParserService;
|
||||
|
||||
// 진행 상황 저장 (serverId -> ScanProgress)
|
||||
private final ConcurrentHashMap<Long, ScanProgress> progressMap = new ConcurrentHashMap<>();
|
||||
|
||||
// 로그 시간 파싱용 패턴들
|
||||
private static final List<DateTimeFormatter> DATE_FORMATTERS = List.of(
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss,SSS"), // 2026-01-06 09:57:01,114
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"), // 2026-01-06 09:57:01.114
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"), // 2026-01-06 09:57:01
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"), // ISO format
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
|
||||
DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm:ss"), // 06-Jan-2026 09:57:01
|
||||
DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm:ss"), // Jan 06, 2026 09:57:01
|
||||
DateTimeFormatter.ofPattern("MMM dd HH:mm:ss") // Jan 06 09:57:01 (년도 없음)
|
||||
);
|
||||
|
||||
// 로그 라인에서 날짜 추출용 정규식
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile(
|
||||
"(\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}[,.]?\\d{0,3})" +
|
||||
"|(\\d{2}-[A-Za-z]{3}-\\d{4} \\d{2}:\\d{2}:\\d{2})" +
|
||||
"|([A-Za-z]{3} \\d{2},? \\d{4} \\d{2}:\\d{2}:\\d{2})" +
|
||||
"|([A-Za-z]{3} \\d{2} \\d{2}:\\d{2}:\\d{2})"
|
||||
);
|
||||
|
||||
/**
|
||||
* 단일 서버 스캔
|
||||
*/
|
||||
@Transactional
|
||||
public ScanResult scanServer(Long serverId, Consumer<ScanProgress> progressCallback) {
|
||||
Server server = serverRepository.findById(serverId)
|
||||
.orElseThrow(() -> new RuntimeException("Server not found: " + serverId));
|
||||
|
||||
return executeScan(server, progressCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 활성 서버 스캔
|
||||
*/
|
||||
@Transactional
|
||||
public List<ScanResult> scanAllServers(Consumer<ScanProgress> progressCallback) {
|
||||
List<Server> servers = serverRepository.findByActiveTrue();
|
||||
return servers.stream()
|
||||
.map(server -> executeScan(server, progressCallback))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 스캔 실행
|
||||
*/
|
||||
private ScanResult executeScan(Server server, Consumer<ScanProgress> progressCallback) {
|
||||
ScanProgress progress = new ScanProgress(server.getId(), server.getName());
|
||||
progressMap.put(server.getId(), progress);
|
||||
|
||||
LocalDateTime scanStartTime = LocalDateTime.now();
|
||||
|
||||
// 스캔 이력 생성
|
||||
ScanHistory history = ScanHistory.builder()
|
||||
.server(server)
|
||||
.startedAt(scanStartTime)
|
||||
.status("RUNNING")
|
||||
.build();
|
||||
history = scanHistoryRepository.save(history);
|
||||
|
||||
try {
|
||||
// 1. 서버 시간 체크 (시차 확인)
|
||||
progress.setMessage("서버 시간 확인 중...");
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
int timeOffsetMinutes = checkAndUpdateServerTimeOffset(server);
|
||||
log.info("Server {} time offset: {} minutes", server.getId(), timeOffsetMinutes);
|
||||
|
||||
// 2. 활성 로그 경로 조회
|
||||
List<ServerLogPath> logPaths = logPathRepository.findByServerIdAndActiveTrue(server.getId());
|
||||
if (logPaths.isEmpty()) {
|
||||
throw new RuntimeException("등록된 로그 경로가 없습니다.");
|
||||
}
|
||||
|
||||
// 3. 활성 패턴 조회
|
||||
List<research.loghunter.entity.Pattern> patterns = patternRepository.findByActiveTrue();
|
||||
if (patterns.isEmpty()) {
|
||||
throw new RuntimeException("등록된 패턴이 없습니다.");
|
||||
}
|
||||
|
||||
progress.setTotalPaths(logPaths.size());
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
AtomicInteger totalFilesScanned = new AtomicInteger(0);
|
||||
AtomicInteger totalFilesSkipped = new AtomicInteger(0);
|
||||
AtomicInteger totalErrorsFound = new AtomicInteger(0);
|
||||
|
||||
for (ServerLogPath logPath : logPaths) {
|
||||
progress.setCurrentPath(logPath.getPath());
|
||||
progress.setMessage("파일 목록 조회 중...");
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
try {
|
||||
// 4. 해당 경로의 모든 파일 조회 (패턴 매칭)
|
||||
List<SftpService.RemoteFile> allFiles = sftpService.listAllFiles(server, logPath);
|
||||
|
||||
// 5. 스캔 대상 파일 필터링 (중복 제외)
|
||||
List<SftpService.RemoteFile> filesToScan = new ArrayList<>();
|
||||
for (SftpService.RemoteFile file : allFiles) {
|
||||
if (shouldScanFile(server.getId(), logPath.getId(), file)) {
|
||||
filesToScan.add(file);
|
||||
} else {
|
||||
totalFilesSkipped.incrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
progress.setTotalFiles(progress.getTotalFiles() + filesToScan.size());
|
||||
progress.setMessage(String.format("전체 %d개 중 %d개 스캔 대상 (기존 %d개 스킵)",
|
||||
allFiles.size(), filesToScan.size(), allFiles.size() - filesToScan.size()));
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
for (SftpService.RemoteFile file : filesToScan) {
|
||||
progress.setCurrentFile(file.name());
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
try {
|
||||
// 파일 내용 다운로드
|
||||
String content = sftpService.downloadFileContent(server, file.path());
|
||||
|
||||
// 패턴 매칭
|
||||
List<LogParserService.MatchResult> matches =
|
||||
logParserService.parseAndMatch(content, patterns, file.path());
|
||||
|
||||
// 에러 저장
|
||||
for (LogParserService.MatchResult match : matches) {
|
||||
// 로그에서 시간 파싱 (시차 보정 적용)
|
||||
LocalDateTime occurredAt = parseLogTime(
|
||||
match.context(),
|
||||
file.modifiedAt(),
|
||||
timeOffsetMinutes
|
||||
);
|
||||
|
||||
ErrorLog errorLog = ErrorLog.builder()
|
||||
.server(server)
|
||||
.pattern(match.pattern())
|
||||
.scanHistory(history)
|
||||
.filePath(match.filePath())
|
||||
.lineNumber(match.lineNumber())
|
||||
.summary(match.summary())
|
||||
.context(match.context())
|
||||
.severity(match.severity())
|
||||
.occurredAt(occurredAt) // 로그 내 시간 (보정됨)
|
||||
.scannedAt(scanStartTime) // 스캔 시작 시간
|
||||
.build();
|
||||
errorLogRepository.save(errorLog);
|
||||
totalErrorsFound.incrementAndGet();
|
||||
}
|
||||
|
||||
// 스캔 완료된 파일 기록
|
||||
saveScannedFile(server.getId(), logPath.getId(), file, matches.size());
|
||||
|
||||
totalFilesScanned.incrementAndGet();
|
||||
progress.setScannedFiles(totalFilesScanned.get());
|
||||
progress.setErrorsFound(totalErrorsFound.get());
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to process file {}: {}", file.path(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to process log path {}: {}", logPath.getPath(), e.getMessage());
|
||||
}
|
||||
|
||||
progress.setScannedPaths(progress.getScannedPaths() + 1);
|
||||
notifyProgress(progressCallback, progress);
|
||||
}
|
||||
|
||||
// 스캔 완료
|
||||
history.setStatus("SUCCESS");
|
||||
history.setFinishedAt(LocalDateTime.now());
|
||||
history.setFilesScanned(totalFilesScanned.get());
|
||||
history.setErrorsFound(totalErrorsFound.get());
|
||||
scanHistoryRepository.save(history);
|
||||
|
||||
// 서버 마지막 스캔 시간 업데이트
|
||||
server.setLastScanAt(LocalDateTime.now());
|
||||
if (totalErrorsFound.get() > 0) {
|
||||
server.setLastErrorAt(LocalDateTime.now());
|
||||
}
|
||||
serverRepository.save(server);
|
||||
|
||||
progress.setStatus("SUCCESS");
|
||||
progress.setMessage(String.format("완료! (스캔: %d, 스킵: %d, 에러: %d)",
|
||||
totalFilesScanned.get(), totalFilesSkipped.get(), totalErrorsFound.get()));
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
return new ScanResult(server.getId(), server.getName(), true,
|
||||
totalFilesScanned.get(), totalErrorsFound.get(), null);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Scan failed for server {}: {}", server.getId(), e.getMessage());
|
||||
|
||||
history.setStatus("FAILED");
|
||||
history.setFinishedAt(LocalDateTime.now());
|
||||
history.setErrorMessage(e.getMessage());
|
||||
scanHistoryRepository.save(history);
|
||||
|
||||
progress.setStatus("FAILED");
|
||||
progress.setMessage("스캔 실패: " + e.getMessage());
|
||||
notifyProgress(progressCallback, progress);
|
||||
|
||||
return new ScanResult(server.getId(), server.getName(), false, 0, 0, e.getMessage());
|
||||
} finally {
|
||||
progressMap.remove(server.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 시간 오프셋 확인 및 업데이트
|
||||
*/
|
||||
private int checkAndUpdateServerTimeOffset(Server server) {
|
||||
try {
|
||||
SftpService.TimeCheckResult result = sftpService.checkServerTime(server);
|
||||
|
||||
if (result.success()) {
|
||||
server.setTimeOffsetMinutes(result.offsetMinutes());
|
||||
server.setLastTimeSyncAt(LocalDateTime.now());
|
||||
serverRepository.save(server);
|
||||
|
||||
log.info("Server {} time sync - Server: {}, Local: {}, Offset: {} min",
|
||||
server.getName(), result.serverTime(), result.localTime(), result.offsetMinutes());
|
||||
|
||||
return result.offsetMinutes();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to check server time for {}: {}", server.getId(), e.getMessage());
|
||||
}
|
||||
|
||||
// 실패 시 기존 값 사용 (없으면 0)
|
||||
return server.getTimeOffsetMinutes() != null ? server.getTimeOffsetMinutes() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일을 스캔해야 하는지 확인 (중복 체크)
|
||||
* - 파일경로 + 크기가 동일하면 스킵
|
||||
* - 파일경로는 같은데 크기가 다르면 재스캔
|
||||
*/
|
||||
private boolean shouldScanFile(Long serverId, Long logPathId, SftpService.RemoteFile file) {
|
||||
Optional<ScannedFile> existing = scannedFileRepository.findByServerIdAndFilePathAndFileSize(
|
||||
serverId, file.path(), file.size());
|
||||
|
||||
if (existing.isPresent()) {
|
||||
log.debug("Skipping already scanned file: {} (size: {})", file.path(), file.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 파일경로는 같은데 크기가 다른 경우 확인 (변경된 파일)
|
||||
List<ScannedFile> previousVersions = scannedFileRepository.findByServerIdAndFilePath(serverId, file.path());
|
||||
if (!previousVersions.isEmpty()) {
|
||||
log.info("File size changed, will rescan: {} (old: {}, new: {})",
|
||||
file.path(), previousVersions.get(0).getFileSize(), file.size());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 스캔 완료된 파일 정보 저장
|
||||
*/
|
||||
private void saveScannedFile(Long serverId, Long logPathId, SftpService.RemoteFile file, int errorCount) {
|
||||
ScannedFile scannedFile = new ScannedFile();
|
||||
scannedFile.setServerId(serverId);
|
||||
scannedFile.setLogPathId(logPathId);
|
||||
scannedFile.setFilePath(file.path());
|
||||
scannedFile.setFileName(file.name());
|
||||
scannedFile.setFileSize(file.size());
|
||||
scannedFile.setScannedAt(LocalDateTime.now());
|
||||
scannedFile.setErrorCount(errorCount);
|
||||
|
||||
scannedFileRepository.save(scannedFile);
|
||||
log.debug("Saved scanned file record: {} (size: {}, errors: {})", file.path(), file.size(), errorCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그 내용에서 시간 파싱 (시차 보정 적용)
|
||||
*/
|
||||
private LocalDateTime parseLogTime(String logContent, LocalDateTime fallback, int offsetMinutes) {
|
||||
if (logContent == null || logContent.isEmpty()) {
|
||||
return applyOffset(fallback, offsetMinutes);
|
||||
}
|
||||
|
||||
// 첫 줄에서 시간 추출 시도
|
||||
String[] lines = logContent.split("\n");
|
||||
for (String line : lines) {
|
||||
LocalDateTime parsed = extractDateTime(line);
|
||||
if (parsed != null) {
|
||||
return applyOffset(parsed, offsetMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
return applyOffset(fallback, offsetMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열에서 날짜/시간 추출
|
||||
*/
|
||||
private LocalDateTime extractDateTime(String text) {
|
||||
Matcher matcher = DATE_PATTERN.matcher(text);
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String dateStr = matcher.group();
|
||||
|
||||
for (DateTimeFormatter formatter : DATE_FORMATTERS) {
|
||||
try {
|
||||
return LocalDateTime.parse(dateStr, formatter);
|
||||
} catch (DateTimeParseException e) {
|
||||
// 다음 포맷 시도
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간에 오프셋 적용
|
||||
* offset > 0: 로컬이 서버보다 앞섬 (로그시간 + offset = 로컬시간)
|
||||
*/
|
||||
private LocalDateTime applyOffset(LocalDateTime time, int offsetMinutes) {
|
||||
if (time == null || offsetMinutes == 0) {
|
||||
return time;
|
||||
}
|
||||
return time.plusMinutes(offsetMinutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 진행 상황 조회
|
||||
*/
|
||||
public ScanProgress getProgress(Long serverId) {
|
||||
return progressMap.get(serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스캔 이력 조회
|
||||
*/
|
||||
public List<ScanHistory> getHistory(Long serverId) {
|
||||
return scanHistoryRepository.findByServerIdOrderByStartedAtDesc(serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스캔된 파일 목록 조회 (서버별)
|
||||
*/
|
||||
public List<ScannedFile> getScannedFiles(Long serverId) {
|
||||
return scannedFileRepository.findByServerId(serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 스캔된 파일 기록 초기화 (재스캔 허용)
|
||||
*/
|
||||
@Transactional
|
||||
public void resetScannedFiles(Long serverId) {
|
||||
List<ScannedFile> files = scannedFileRepository.findByServerId(serverId);
|
||||
scannedFileRepository.deleteAll(files);
|
||||
log.info("Reset {} scanned file records for server {}", files.size(), serverId);
|
||||
}
|
||||
|
||||
private void notifyProgress(Consumer<ScanProgress> callback, ScanProgress progress) {
|
||||
if (callback != null) {
|
||||
callback.accept(progress);
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs
|
||||
public static class ScanProgress {
|
||||
private Long serverId;
|
||||
private String serverName;
|
||||
private String status = "RUNNING";
|
||||
private String message;
|
||||
private String currentPath;
|
||||
private String currentFile;
|
||||
private int totalPaths;
|
||||
private int scannedPaths;
|
||||
private int totalFiles;
|
||||
private int scannedFiles;
|
||||
private int errorsFound;
|
||||
|
||||
public ScanProgress(Long serverId, String serverName) {
|
||||
this.serverId = serverId;
|
||||
this.serverName = serverName;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getServerId() { return serverId; }
|
||||
public String getServerName() { return serverName; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
public String getCurrentPath() { return currentPath; }
|
||||
public void setCurrentPath(String currentPath) { this.currentPath = currentPath; }
|
||||
public String getCurrentFile() { return currentFile; }
|
||||
public void setCurrentFile(String currentFile) { this.currentFile = currentFile; }
|
||||
public int getTotalPaths() { return totalPaths; }
|
||||
public void setTotalPaths(int totalPaths) { this.totalPaths = totalPaths; }
|
||||
public int getScannedPaths() { return scannedPaths; }
|
||||
public void setScannedPaths(int scannedPaths) { this.scannedPaths = scannedPaths; }
|
||||
public int getTotalFiles() { return totalFiles; }
|
||||
public void setTotalFiles(int totalFiles) { this.totalFiles = totalFiles; }
|
||||
public int getScannedFiles() { return scannedFiles; }
|
||||
public void setScannedFiles(int scannedFiles) { this.scannedFiles = scannedFiles; }
|
||||
public int getErrorsFound() { return errorsFound; }
|
||||
public void setErrorsFound(int errorsFound) { this.errorsFound = errorsFound; }
|
||||
}
|
||||
|
||||
public record ScanResult(
|
||||
Long serverId,
|
||||
String serverName,
|
||||
boolean success,
|
||||
int filesScanned,
|
||||
int errorsFound,
|
||||
String error
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user