2.1 설계 방향

고객 서버 영역은 최소한의 설정만으로 로그 수집 환경을 구축하고, 운영 부담을 최소화하는 것을 목표로 설계했습니다.

2.1.1 핵심 설계 원칙

1. Zero-Code-Change 철학

고객이 기존 애플리케이션 코드를 거의 수정하지 않고도 로그 수집이 가능하도록 설계했습니다.

  • 라이브러리 추가: Gradle/Maven 의존성만 추가
  • 최소 설정: api-key 하나만 설정하면 자동 활성화
  • 자동 로깅: 어노테이션 기반으로 Controller, Service, Repository 계층 자동 수집

2. 경량 에이전트 운영

고객 서버에는 무거운 로그 처리 엔진을 두지 않고, 경량 수집 에이전트만 배치했습니다.

  • Fluent Bit: CPU 0.5코어, 메모리 512MB로 제한
  • 단일 책임: 파일 읽기 → 파싱 → 전송만 담당
  • 복잡한 처리는 AI 서버로 위임: Logstash, OpenSearch 등 무거운 작업은 외부에서 처리

3. 민감 정보 보호

로그가 고객 서버를 벗어나기 전에 민감 정보를 마스킹하도록 설계했습니다.

  • 어노테이션 기반: @Sensitive, @ExcludeValue로 필드 단위 제어
  • 자동 마스킹: 비밀번호, 토큰, 카드번호 등 자동 감지
  • 고객 제어: 어떤 데이터를 수집할지 고객이 직접 제어 가능

2.2 구성 요소

2.2.1 LogLens 라이브러리

애플리케이션에 통합되어 자동으로 로그를 생성하는 핵심 컴포넌트입니다.

기존 로깅 프레임워크(Log4j, Logback)는 개발자가 직접 logger.info()를 호출해야 하므로, 일관성 없는 로그가 생성되고 누락이 발생하기 쉽습니다. 이를 해결하기 위해 AOP(Aspect-Oriented Programming) 기반 자동 로깅을 도입했습니다.

동작 원리

사용자 요청 → Controller 메서드 호출

LogLens AOP가 자동으로 가로챔

[Request 로그 생성] → 메서드 실행 → [Response 로그 생성]

/logs/be/app.log 파일에 JSON 형식으로 기록

백엔드 로그 (Spring Boot)(백엔드 라이브러리)

  • 설치: Gradle/Maven 의존성 추가
  • 설정: api-key 하나만 필수
  • 출력: /logs/be/*.log (JSON 포맷)
  • 특징: TraceID 자동 생성, 계층별 자동 분류(Controller/Service/Repository)

프론트엔드 로그 (React)

  • 설치: npm 패키지 설치
  • 설정: initLogLens({ domain: "..." })
  • 출력: 백엔드 /api/logs/frontend 엔드포인트로 전송 → /logs/fe/app.log 파일 기록
  • 특징: 함수 래핑(withLogLens, useLogLens)으로 실행 시간 자동 측정

프론트엔드 로그를 직접 Kafka로 전송하지 않고 백엔드를 경유하도록 설계한 이유:

  1. 보안: Kafka 브로커 주소를 프론트엔드에 노출하지 않음
  2. 검증: 백엔드에서 로그 형식 검증 및 필터링 가능
  3. 통합 관리: 백엔드/프론트엔드 로그를 동일한 파일 시스템에서 관리

2.2.2 Fluent Bit

파일로 저장된 로그를 실시간으로 읽어서 파싱·정규화·전송하는 경량 에이전트입니다.

왜 Fluent Bit을 선택했는가?

비교 항목Fluent BitLogstashFilebeat
메모리 사용량~450KB~1GB~50MB
CPU 사용량매우 낮음높음낮음
파싱 기능강력강력제한적
Lua 스크립트지원미지원미지원
멀티라인 파싱지원지원지원

Fluent Bit은 가장 경량이면서도 강력한 파싱 기능을 제공하므로, 고객 서버에 최소한의 부담만 주면서 1차 정규화를 수행할 수 있습니다.

역할

파일 모니터링 (tail input)
  → JSON 파싱
  → MySQL 멀티라인 로그 파싱
  → Lua 스크립트로 필드 정규화 (transform.lua)
  → Kafka로 전송 (snappy 압축)

1차 정규화가 필요한 이유

Kafka로 전송하기 전에 고객 서버에서 1차 정규화를 수행하는 이유:

  1. 네트워크 대역폭 절약: 불필요한 필드 제거, 압축으로 전송량 감소
  2. AI 서버 부하 분산: Logstash가 모든 정규화를 담당하면 병목 발생
  3. 즉각적인 필터링: Health Check, 스케줄러 로그 등 불필요한 로그를 조기에 제외

2.2.3 transform.lua

Fluent Bit에서 실행되는 Lua 스크립트로, 로그 필드를 자동으로 분류하고 정규화합니다.

왜 Lua 스크립트를 사용했는가?

Fluent Bit의 기본 필터만으로는 복잡한 조건부 로직을 처리하기 어렵습니다. 예를 들어:

  • layer 필드가 “CONTROLLER”일 때 → “Controller”로 정규화
  • layer가 없으면 logger 필드에서 추출
  • 프론트엔드 로그의 logs 배열을 개별 레코드로 분리

이런 복잡한 로직은 Lua 스크립트로만 구현 가능합니다.

주요 처리 로직

source_type 자동 분류

if record["layer"] == "FE" or record["component_name"] == "Frontend" then
    record["source_type"] = "FE"
elseif record["logger"] and string.find(record["logger"], "mysql") then
    record["source_type"] = "INFRA"
else
    record["source_type"] = "BE"
end

layer 정규화

local layer_map = {
    ["CONTROLLER"] = "Controller",
    ["SERVICE"] = "Service",
    ["REPOSITORY"] = "Repository",
    ["controller"] = "Controller",
    ["service"] = "Service",
    ["repository"] = "Repository"
}
record["layer"] = layer_map[record["layer"]] or record["layer"]

프론트엔드 배열 로그 처리

-- 프론트엔드는 로그를 배열로 보냄
-- { "logs": [ {log1}, {log2}, ... ] }
-- 이를 개별 레코드로 분리하여 Kafka에 전송
if record["logs"] and type(record["logs"]) == "table" then
    for _, log_entry in ipairs(record["logs"]) do
        -- 재귀 호출로 각 로그를 개별 처리
        transform_log(tag, timestamp, log_entry)
    end
    return -1  -- 원본 레코드는 삭제
end

MySQL 슬로우 쿼리 파싱

-- MySQL Slow Query Log 예시:
-- # Query_time: 1.234567  Lock_time: 0.000123  Rows_sent: 100  Rows_examined: 10000
-- SELECT * FROM users WHERE ...
 
if record["message"] and string.find(record["message"], "Query_time:") then
    local query_time = string.match(record["message"], "Query_time:%s*([%d%.]+)")
    local lock_time = string.match(record["message"], "Lock_time:%s*([%d%.]+)")
    local rows_sent = string.match(record["message"], "Rows_sent:%s*(%d+)")
    
    record["log_details"] = {
        query_time = tonumber(query_time),
        lock_time = tonumber(lock_time),
        rows_sent = tonumber(rows_sent)
    }
end

왜 이런 정규화가 필요한가?

각 고객의 로그 형식이 조금씩 다를 수 있습니다(대소문자, 필드명 등). 이를 표준 형식으로 통일해야:

  1. OpenSearch 인덱싱 효율 향상: 동일한 필드명으로 매핑
  2. 검색 정확도 증가: “Controller”와 “CONTROLLER”를 구분하지 않음
  3. AI 분석 용이: 정규화된 데이터로 패턴 학습

2.2.4 MySQL 로그 수집

데이터베이스의 에러 로그슬로우 쿼리 로그를 수집하여 성능 문제를 조기에 발견합니다.

왜 MySQL 로그를 수집하는가?

애플리케이션 로그만으로는 데이터베이스 성능 문제를 파악하기 어렵습니다. 예를 들어:

  • 슬로우 쿼리: 인덱스 미사용, 비효율적인 JOIN
  • 에러 로그: 연결 실패, 권한 문제, 테이블 락

이런 문제는 MySQL 로그를 직접 수집해야만 발견할 수 있습니다.

설정 방식

mysql-logging.cnf

[mysqld]
# Error Log
log-error=/var/log/mysql/error.log
log_error_verbosity=2
 
# Slow Query Log
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=0.5  # ← 0.5초 이상 걸리는 쿼리 기록

Docker 볼륨 공유

volumes:
  # MySQL 컨테이너 → 호스트 → Fluent Bit 컨테이너
  - ../logs/infra/mysql:/var/log/mysql

MySQL 컨테이너와 Fluent Bit 컨테이너가 같은 디렉토리를 마운트하여, Fluent Bit이 MySQL 로그를 실시간으로 읽을 수 있습니다.

멀티라인 파싱

MySQL 슬로우 쿼리 로그는 한 쿼리가 여러 줄에 걸쳐 기록됩니다:

# Time: 2025-01-15T10:30:45.123456+09:00
# User@Host: app[app] @ localhost [127.0.0.1]
# Query_time: 1.234567  Lock_time: 0.000123
SELECT * FROM users
WHERE created_at > '2024-01-01'
ORDER BY created_at DESC;

Fluent Bit의 Multiline 옵션으로 이를 하나의 레코드로 병합합니다:

[INPUT]
    Name              tail
    Path              /logs/infra/mysql/slow.log
    Multiline         On
    Parser_Firstline  mysql_slow_firstline  # "# Time:" 패턴 감지
    Parser_1          mysql_slow_body       # 나머지 라인 파싱

2.3 고객 관점에서의 이점

2.3.1 간편한 통합

Before (기존 방식)

1. 로깅 라이브러리 선택 (Log4j, Logback, ...)
2. 로그 포맷 설계 (JSON? 텍스트?)
3. 로그 수집 도구 설치 (Filebeat, Logstash, ...)
4. 로그 저장소 구축 (ELK Stack, Splunk, ...)
5. 대시보드 개발
6. 알림 시스템 구축

After (LogLens 도입)

1. Gradle 의존성 추가
2. api-key 설정
3. docker-compose up (Fluent Bit)

2.3.3 민감 정보 보호

고객이 직접 제어 가능

// 비밀번호는 절대 로그에 남기지 않음
public void login(String username, @Sensitive String password) {
    // password는 "***"로 마스킹됨
}
 
// 파일 데이터는 로그에서 완전히 제외
public void uploadFile(Long userId, @ExcludeValue byte[] fileData) {
    // fileData는 로그에 기록되지 않음
}
 
// Health Check는 로그 수집 제외
@NoLogging
public void healthCheck() {
    // 이 메서드는 로그에 남지 않음
}

2.3.4 운영 부담 최소화

자동화된 관리

  1. 오프셋 관리: Fluent Bit이 읽은 위치를 자동 저장 (재시작 시 중복 방지)
  2. 로그 로테이션: Docker logging driver가 자동으로 파일 크기 제한 (max-size: 10m)
  3. Health Check: Fluent Bit HTTP API (:2020/api/v1/health)로 상태 확인
  4. 자동 재시작: restart: unless-stopped 옵션으로 장애 시 자동 복구

모니터링 최소화

고객은 Fluent Bit의 Health 상태만 확인하면 됩니다:

curl http://localhost:2020/api/v1/health
# {"status": "ok"}

2.4 설계 트레이드오프

2.4.1 1차 정규화의 한계

선택한 방향: Fluent Bit에서 기본적인 정규화만 수행

장점:

  • 고객 서버 부하 최소화
  • Fluent Bit 설정 파일이 간결함

단점:

  • 복잡한 정규화는 Logstash로 미뤄짐
  • 고객 서버에서 필터링하지 못한 로그도 Kafka로 전송됨

보완책: Logstash에서 2차 정규화 수행

2.4.2 파일 기반 로그 수집과 보관 정책

고객 서버에서는 로그를 먼저 파일로 저장한 뒤, Fluent Bit이 이 파일을 읽어 Kafka로 전송하는 방식으로 구성했습니다.

애플리케이션 → 파일 로그 생성 → Fluent Bit 수집 → Kafka

이 구조는 고객 서버의 운영 부담을 최소화하면서도, 로그의 안정적인 보관과 전달을 보장합니다.

파일 기반 접근을 선택한 이유

(1) 고객의 로그 소유권을 보장하기 위해

고객 서버에서 발생하는 로그는 고객의 자산이므로, 원본 로그가 반드시 고객 환경에 남아 있어야 합니다.

  • 외부 전송만 할 경우 고객이 원본 로그를 확인할 수 없음
  • 장애/보안 사고/법적 분쟁 발생 시 원본 로그가 필수
  • 고객 팀이 자체 분석 도구를 사용할 수 있어야 함

따라서 로그는 고객 환경에서 먼저 파일로 기록하도록 했습니다.

(2) 장애 상황에서도 안전한 로그 확보

파일 기반 수집은 네트워크나 에이전트 장애가 발생해도 로그가 디스크에 남기 때문에 데이터 유실 가능성이 낮습니다.

  • Fluent Bit 오류 → 파일은 그대로 남아 재시작 후 다시 읽기
  • Kafka 장애 → 파일에 계속 기록되므로 유실 없음
  • 서버 재부팅 → Fluent Bit이 오프셋 기반으로 이어서 수집

장애가 잦은 운영 환경에서도 안정적인 로그 수집이 가능합니다.

로그 파일 관리 정책

로그가 무한히 쌓이지 않도록 LogLens 라이브러리는 다음과 같은 자동 관리 기능을 제공합니다.

(1) 파일 크기 기반 분리

기록 중인 로그 파일이 일정 크기(예: 100MB)를 넘으면 자동 분리·압축됩니다.

app.log
app.log.2025-11-28.1.gz
app.log.2025-11-28.2.gz
app.log.2025-11-28.3.gz

압축 저장으로 디스크 사용량을 최소화합니다.

(2) 시간 기반 자동 정리

최근 2일치 로그만 유지하고, 그보다 오래된 파일은 자동 삭제합니다.

  • 11월 28일 기준 → 11월 26일 이전 로그 삭제
  • 디스크 사용량이 일정하게 유지됨
  • 고객이 추가로 관리할 필요 없음

2.4.3. 프론트엔드 로그 백엔드 경유

선택한 방향: React → 백엔드 /api/logs/frontend → 파일 → Fluent Bit

대안: React → Kafka 직접 전송

왜 백엔드를 경유하는가?

  1. 보안: Kafka 브로커 주소를 클라이언트에 노출하지 않음
  2. 검증: 백엔드에서 악의적인 로그 필터링 가능
  3. 백엔드 TraceID 연계: 백엔드 요청과 프론트엔드 이벤트를 같은 TraceID로 추적

단점:

  • 백엔드 API 호출 횟수 증가
  • 프론트엔드 로그가 백엔드 장애 시 수집 불가

보완책:

  • 30초마다 배치 전송으로 API 호출 최소화
  • 백엔드 장애 시 브라우저 LocalStorage에 임시 저장