728x90
개요
- A 시스템에서 발생한 애플리케이션 로그를 Elasticsearch에 적재함
- 일정 주기로 ES를 조회함
- 최근 ERROR 로그만 골라서 LLM(OpenAI)에 보내 요약/분석함
- 분석 결과를 로그로 남기거나, 추후 Slack·이메일 알림으로 확장 가능하게 설계함
1. 로그 분석 시스템 환경 및 의존성
Gradle 의존성 추가
- 웹 API 호출용으로 spring-boot-starter-web 추가함
- Elasticsearch 조회를 위해 elasticsearch-java 클라이언트 의존성 추가함
- LLM 호출을 위해 openai-client 와 ktor-client-java 의존성 추가함
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
// Elasticsearch Java Client
implementation("co.elastic.clients:elasticsearch-java:8.11.0")
// OpenAI (aallam client + ktor engine)
implementation("com.aallam.openai:openai-client:3.7.0")
implementation("io.ktor:ktor-client-java:2.3.12")
implementation("org.jetbrains.kotlin:kotlin-reflect")
}
2. OpenAI & Elasticsearch 설정
application.yml 설정
- OpenAI API 키를 openai.api-key 로 yml에 저장함
- 실제 운영에서는 환경변수나 Jasypt 등으로 암호화해서 사용하는 걸 권장함
openai:
api-key: "..."
OpenAI 클라이언트 설정
- @Configuration 클래스로 OpenAI 클라이언트 Bean을 등록함
- @Value 로 yml에 설정한 openai.api-key 값을 주입받음
- 애플리케이션 어디에서나 OpenAI Bean을 주입받아 사용 가능해짐
@Configuration
class OpenAiConfig(
@Value("\${openai.api-key}") private val apiKey: String
) {
@Bean
fun openAiClient(): OpenAI = OpenAI(apiKey)
}
더보기
openAI 대신 무료 LLM으로 테스트하려면 Ollama + Llama3 + 로컬 LLM
- 스프링 서버 안에서 바로 LLM 돌릴 수 있음
- Ollama 설치
- ollama pull llama3
- Spring Boot에서 http://localhost:11434/api/generate 호출
Elasticsearch 클라이언트 설정
- 로컬에서 띄운 Elasticsearch(localhost:9200)에 연결하는 클라이언트를 생성함
- RestClientTransport + JacksonJsonpMapper 를 사용해 직렬화/역직렬화를 처리함
- 이후 서비스 레이어에서 ElasticsearchClient Bean을 주입받아 검색 요청을 수행함
@Configuration
class ElasticsearchConfig {
@Bean
fun elasticsearchClient(): ElasticsearchClient {
val restClient = RestClient.builder(
HttpHost("localhost", 9200, "http")
).build()
val transport = RestClientTransport(restClient, JacksonJsonpMapper())
return ElasticsearchClient(transport)
}
}
3. 최근 ERROR 로그 조회 서비스 구현
Elasticsearch 검색 쿼리 설계
- 인덱스 패턴: local-app-logs-*
- 필터 조건
- level = "ERROR"
- @timestamp 가 최근 N분 이내인 문서만 조회함
서비스 코드
- Instant.now() 기준으로 현재 시간을 UTC로 가져옴
- 파라미터 minutes 만큼 과거 시점을 계산해서 from 시간으로 사용함
- bool.must 안에 level=ERROR + @timestamp 범위 조건을 동시에 넣어 검색함
- 인덱스 패턴은 local-app-logs-* 로 지정해서 날짜별 인덱스를 모두 타겟팅함
- 검색 결과에서 _source의 스택트레이스 부분만 꺼내 반환함
@Service
class ErrorLogFetcher(
private val es: ElasticsearchClient,
) {
private val logger = LoggerFactory.getLogger(javaClass)
fun fetchRecentErrors(minutes: Long = 10): List<String> {
val nowUtc = Instant.now()
val fromUtc = nowUtc.minusSeconds(minutes * 60)
val nowIso = nowUtc.toString()
val fromIso = fromUtc.toString()
logger.info("Search ERROR logs from {} to {}", fromIso, nowIso)
val response = es.search(
{ s ->
s.index("local-app-logs-*").query { q ->
q.bool { b ->
b.must(listOf(Query.of {
it.term { t ->
t.field("level.keyword").value("ERROR")
}
}, Query.of {
it.range { r ->
r.field("@timestamp").gte(JsonData.of(fromIso)).lte(JsonData.of(nowIso))
}
}))
}
}
}, Map::class.java
)
val hits = response.hits().hits()
logger.info("Found {} error logs", hits.size)
return hits.mapNotNull { it.source()?.get("stack_trace").toString() }
}
}
4. OpenAI 클라이언트
@Service
class AiLogAnalyzer(
private val openAI: OpenAI
) {
fun analyzeErrorLogs(errorLogs: List<String>): String {
if (errorLogs.isEmpty()) {
return "최근 지정된 기간 동안 새로운 ERROR 로그가 없음"
}
// 너무 길어지지 않게 상위 5개만 사용함
val logText = logs.take(5).joinToString("\n\n---\n\n") { shortenStack(it) }
val prompt = """
너는 Spring/Java, Kotlin 백엔드 로그를 분석하는 전문가입니다.
아래는 서버에서 발생한 ERROR 로그들 입니다.
로그를 분석하여 다음을 출력하세요:
1) 가장 가능성 높은 root cause 1~3개
2) 발생 가능성이 높은 계층
3) 재발 방지를 위한 해결책
4) 현재 장애 영향도 (High / Medium / Low)
5) 문제가 재현될 수 있는 조건
--- LOGS START ---
$logText
--- LOGS END ---
""".trimIndent()
val completion = runBlocking {
openAI.chatCompletion(
ChatCompletionRequest(
model = ModelId("gpt-4o-mini"),
messages = listOf(
ChatMessage(
role = ChatRole.User,
content = prompt
)
)
)
)
}
return completion.choices.firstOrNull()?.message?.content
?: "LLM 응답이 비어 있음"
}
private fun shortenStack(stack: Any?): String {
val s = stack?.toString() ?: return ""
return if (s.length > 800) s.substring(0, 800) + "\n...(생략)" else s
}
}
@Component
class LogAnalysisScheduler(
private val errorFetcher: ErrorLogFetcher,
private val aiLogAnalyzer: AiLogAnalyzer
) {
private val logger = LoggerFactory.getLogger(javaClass)
// 1분마다 실행 (테스트용)
@Scheduled(fixedDelay = 60_000)
fun runAnalysis() = runBlocking {
logger.info("=== AI 로그 분석 스케줄 시작 ===")
val errorLogs = errorFetcher.fetchRecentErrors(minutes = 10))
val analysis = aiLogAnalyzer.analyzeLogs(errorLogs)
logger.info("=== AI 로그 분석 결과 ===\n{}", analysis)
}
}
728x90