본문 바로가기
개발

로그 하나 보려고 서버마다 접속하던 팀이 ELK 대신 Loki를 고른 이유: 사내 로그 모니터링 시스템 구축기

by owel.dev 2026. 10. 7.

배경

사내에서 운영하는 서버가 늘어나면서 다음과 같은 문제가 생겼습니다.

  • 여러 서버를 거쳐 처리되는 API에서 오류가 나면, 원인을 찾기 위해 각 서버에 일일이 접속해 로그를 확인해야 했습니다.
  • 오류 건수는 늘어나는데 이를 바로 감지할 방법이 없어, 사용자가 제보한 후에야 알게 되는 오류가 많아졌습니다.
  • API별 월간 호출 수나 평균 처리 시간 같은 통계가 필요한 일이 늘었지만, 로그가 서버마다 흩어져 있어 집계하기 어려웠습니다.

로그 모니터링 시스템이란

로그 모니터링 시스템은 여러 서버에서 발생하는 로그를 별도의 저장소에 모아 두고, 이를 조회하고 감시할 수 있게 해 주는 시스템입니다. 제공하는 기능은 다음과 같습니다.

  • 여러 서비스의 로그를 한 곳에서 통합 조회하는 기능
  • 서비스별, 키워드별, 날짜별로 로그를 필터링해 골라 보는 기능
  • 특정 키워드가 포함된 로그가 발생하면 메신저, 메일 등으로 알림을 보내는 기능
  • 로그를 집계해 호출 수, 처리 시간 등의 통계를 내는 기능

그리고 로그 모니터링 시스템을 사용하면 다음과 같은 이점이 있습니다.

  • 관리하는 서버가 많아도 로그를 한 곳에서 편하게 조회할 수 있습니다.
  • 오류나 이벤트가 발생했을 때 빠르게 알림을 받을 수 있습니다.
  • 로그가 서버 밖에 저장되므로, 서버가 죽거나 저장 공간이 가득 차 장애가 발생해도 로그가 유실되지 않습니다.
  • 서버가 로그 저장과 조회에 쓰던 자원을 비즈니스 로직 수행에 쓸 수 있습니다.
  • 서버가 죽거나 저장공간이 가득 차 장애가 발생해도, 로그가 서버 밖에 저장되기에 로그를 보호할 수 있습니다.
  • 비즈니스 로직 수행에 필요한 데이터는 비싸고 속도가 빠른 저장공간에, 로그는 저렴하고 느린 저장 공간에 나눠 저장해 비용을 줄일 수 있습니다.

이런 이유로 규모가 큰 서비스는 대부분 로그 모니터링 시스템을 운영합니다.

일반적인 구조와 기술스택

로그 모니터링 시스템은 보통 로그 수집, 로그 저장, 로그 시각화 세 요소로 이루어지며, 요소마다 별도의 도구를 사용합니다.

도입을 검토하던 당시 현업에서 많이 쓰이던 조합은 ELK 스택과 ALG 스택이었습니다.

역할 ELK 스택 ALG 스택
로그 수집 Logstash Alloy
로그 저장 Elasticsearch Loki
로그 시각화 Kibana Grafana
  • ELK 스택에서 각 서버에 설치하는 수집기로는 보통 Filebeat를 사용합니다. Logstash는 주로 Filebeat의 로그를 받아 가공하여 Elasticsearch로 보내는 역할을 담당합니다.
  • ALG 스택은 흔히 PLG 스택(Promtail, Loki, Grafana)으로 불리던 조합에서 수집기만 Promtail에서 Alloy로 바뀐 것입니다. Promtail은 2026년 3월에 지원이 종료되었고, 지금은 Alloy가 그 자리를 대신하고 있습니다.

로그 모니터링 시스템은 로그를 효율적으로 저장하면서도 빠르게 조회할 수 있어야 하는데, 이걸 좌우하는 게 로그 저장소입니다. 따라서 어떤 로그 저장소를 쓸 것인가가 기술 스택을 결정하게 됩니다. 그래서 각 스택의 로그 저장소인 Elasticsearch와 Loki의 특성을 비교해 기술스택을 고르기로 했습니다.

로그 저장소 비교: Elasticsearch와 Loki

Elasticsearch

Elasticsearch는 로그를 저장할 때 로그 본문 전체를 '역색인'으로 만듭니다. 역색인이란 로그 본문을 단어 단위로 쪼개고, 각 단어가 어느 로그에 등장하는지를 기록한 것입니다. 덕분에 단어나 키워드로 검색했을때 로그를 빠르게 검색할 수 있습니다. 반면 로그를 저장할 때마다 역색인을 갱신해야 하고 역색인 자체도 저장 공간을 차지해, 저장에 드는 비용이 큽니다.

Loki

Loki는 로그 본문을 색인하지 않고, 수집기가 로그와 함께 보낸 라벨들만 색인합니다. 검색할 때는 라벨로 먼저 필터링한 뒤, 그 안에서 본문을 직접 훑어 조건에 맞는 로그를 찾습니다. 그래서 색인을 만들고 저장하는 비용은 Elasticsearch보다 훨씬 적지만, 본문을 직접 훑어야 하므로 검색은 더 느립니다. 다만 라벨로 검색 범위를 충분히 좁히면 훑어야 할 양이 줄어, 실제로 쓰기에 무리 없는 속도가 나옵니다.

저장과 확장 방식

Elasticsearch와 Loki는 데이터를 저장하는 위치도 다르며, 이 차이가 확장 방식의 차이로 이어집니다.

 

Elasticsearch는 기본적으로 색인과 로그 본문을 검색을 수행하는 데이터 노드의 디스크에 저장합니다. 저장 공간과 연산 자원이 묶여 있어, 저장 공간을 수평적으로 확장하려면 연산 자원도 함께 늘려야 하고 샤드 관리도 필요합니다.

 

반면, Loki는 색인과 로그 본문을 모두 S3 같은 별도의 스토리지에 저장하고, 검색할 때 필요한 부분만 불러옵니다. 저장 공간과 연산 자원이 분리되어 있어, 필요한 쪽만 따로 확장할 수 있습니다. 또한 저장 공간은 클라우드 공급자가 사용량에 맞춰 알아서 늘려 주므로, 직접 관리할 필요가 없습니다.

 

Loki는 색인과 로그 본문을 모두 S3 같은 외부 스토리지에 저장하고, 검색할 때 필요한 부분만 불러옵니다. 저장 공간과 연산 자원이 분리되어 있어, 필요한 자원만 따로 확장할 수 있습니다. 그리고 데이터가 S3에 저장되는 만큼, 저장 공간 확장을 클라우드 공급자가 알아서 해 줘 편리하다는 장점도 있습니다.

 

지금까지 비교한 내용을 정리하면 다음과 같습니다.

항목 Elasticsearch Loki
색인 대상 로그 본문의 모든 단어 라벨
단어 검색 속도 빠름 느림 (라벨로 좁힌 뒤 본문 탐색)
색인 생성, 저장 비용 높음 낮음
데이터 저장 위치 데이터 노드의 디스크 S3
용량 확장 디스크 증설이나 데이터 노드 추가, 샤드 관리 별도 작업 없음 (사용량에 따라 자동 확장)

선택

기술 스택으로는 Loki를 저장소로 쓰는 ALG 스택을 선택했습니다.

 

사내 서버 환경에서는 여러 키워드를 조합한 복잡한 검색이 필요하지 않아, Elasticsearch의 강점인 검색을 살릴 일이 많지 않습니다. 반면 로그는 많이 발생하므로, 저장 비용이 낮고 확장과 관리가 간편한 Loki의 장점이 크게 작용합니다. 그래서 Loki가 더 적합하다고 생각했습니다.

구축

모니터링 서버 한 대를 배포해 Loki와 Grafana를 두고, 로그를 수집할 서버마다 Alloy를 설치했습니다. 수집한 로그는 S3 버킷에 저장합니다.

 

이후 나오는 설정들은 Loki 3.7, Alloy 1.20 버전을 기준으로 합니다.

Loki: 로그를 S3에 저장

Loki는 모든 컴포넌트를 한 프로세스로 실행하는 모놀리식 모드로 띄웠습니다. 하루 로그량이 약 20GB 이하인 지금의 규모에 맞는 방식입니다(Loki 문서). 설정의 핵심은 저장 위치를 S3로 지정하는 부분입니다.

auth_enabled: false

server:
  http_listen_port: 3100

common:
  path_prefix: /var/lib/loki
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory
  storage:
    s3:
      region: ap-northeast-2
      bucketnames: <버킷 이름>

schema_config:
  configs:
    - from: 2025-01-01        # 이 날짜 이후의 로그에 아래 형식을 적용
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 90d       # 보관 기간

compactor:
  working_directory: /var/lib/loki/compactor
  retention_enabled: true     # 보관 기간이 지난 로그 삭제
  delete_request_store: s3

 

common.storage.s3와 object_store: s3 설정으로 로그 본문(청크)과 색인이 모두 버킷에 저장됩니다. 그래서 Loki 서버를 새로 띄워도 버킷에 올라간 로그는 그대로 조회됩니다. 버킷 접근 권한은 서버의 IAM 역할이나 액세스 키로 부여합니다.

 

다만 최근 로그는 버킷으로 올라가기 전까지 Loki 서버의 메모리와 디스크(WAL)에만 있습니다. 기본 설정에서는 이 시간이 길게는 2시간 정도이며, 그 사이에 서버 디스크에 문제가 생기면 아직 올리가지 않은 로그는 유실됩니다.

 

retention_period는 로그 보관 기간입니다. compactor의 retention_enabled를 켜 두었으므로, 이 기간이 지난 로그는 compactor가 삭제합니다.

Alloy: 로그 파일을 읽어 Loki로 전송

각 서버의 Alloy는 서비스가 남기는 로그 파일을 읽어 Loki로 보냅니다. 설정은 모든 서버가 같고, 서버마다 __path__와 service_name만 바꿔 사용합니다.

// 1. 수집할 로그 파일과, 로그에 붙일 라벨
local.file_match "app" {
  path_targets = [{
    __path__     = "/var/log/order-api/*.log",
    service_name = "order-api",
    host         = constants.hostname,
  }]
}

// 2. 로그 파일을 읽음 (처음 보는 파일은 맨 앞부터)
loki.source.file "app" {
  targets    = local.file_match.app.targets
  forward_to = [loki.write.default.receiver]
}

// 3. Loki로 전송
loki.write "default" {
  endpoint {
    url = "http://<모니터링 서버 주소>:3100/loki/api/v1/push"
  }
}

 

Alloy는 처음 보는 파일을 맨 앞부터 읽고, 읽은 시각을 로그의 시각으로 기록합니다. 그래서 Alloy를 처음 설치하면 이미 쌓여 있던 로그까지 모두 올라가며, 이 로그에는 실제 발생 시각이 아니라 읽은 시각이 붙습니다. 기존 로그를 올리지 않으려면 loki.source.file에 tail_from_end = true를 넣고, 로그에 적힌 시각을 쓰려면 loki.process의 stage.timestamp로 시각을 파싱합니다.

 

라벨은 서비스 이름(service_name)과 서버 이름(host) 두 가지만 직접 지정했습니다. 여기에 Alloy가 파일 경로를 담은 filename 라벨을 자동으로 붙입니다.

 

라벨을 이 정도로만 둔 것은 Loki가 라벨 조합마다 로그를 따로 묶어 저장하기 때문입니다. 요청 ID처럼 값의 종류가 많은 정보를 라벨로 만들면 묶음이 지나치게 많아져, 색인이 커지고 조회가 느려집니다. 이런 정보는 라벨로 만들지 않고 본문에 둔 채, 조회할 때 본문 검색으로 찾는 것이 좋습니다.

Grafana: 조회, 알림, 통계

Grafana에 Loki를 데이터 소스로 추가하면 LogQL로 로그를 조회할 수 있습니다. 도입 배경에서 든 세 가지 문제는 다음과 같이 해결됩니다. 예시는 서비스가 요청마다 아래와 같은 JSON 로그를 남긴다고 가정합니다.

{
    "time":"2026-10-06T10:15:32.120Z",
    "level":"INFO",
    "request_id":"req-7f3a",
    "method":"POST",
    "path":"/api/orders",
    "status":200,
    "duration_ms":87,
    "message":"request completed"
}

 

여러 서버의 로그를 한곳에서 조회합니다.

요청 ID로 검색하면 그 요청을 처리한 모든 서비스의 로그가 한 화면에 나옵니다.

{service_name=~"order-api|payment-api"} |= "req-7f3a"

오류를 알림으로 받습니다

최근 5분간 ERROR 로그 수를 서비스별로 세는 쿼리를 Grafana 알림 규칙으로 등록하고, 값이 0보다 크면 메신저로 알림이 오게 했습니다.

sum by (service_name) (count_over_time({service_name=~".+"} |= "ERROR" [5m]))

주의할 점은 오류가 없을 때 이 쿼리가 0이 아니라 빈 결과를 돌려준다는 것입니다. Grafana는 기본 설정에서 빈 결과를 '데이터 없음(No Data)' 상태로 보고 알림을 보냅니다. 그래서 알림 규칙에서 데이터가 없을 때의 상태를 Normal로 지정해야 불필요한 알림이 오지 않습니다 (Grafana 문서).

통계를 쿼리로 냅니다

별도 앱이나 DBMS 없이, 최근 30일간 API별 요청 수와 평균 처리 시간(ms)을 집계합니다. 아래에서 첫 번째 쿼리가 요청 수, 두 번째 쿼리가 평균 처리 시간입니다. Grafana에서는 쿼리 유형을 Instant로 두고 실행합니다.

sum by (path) (count_over_time({service_name="order-api"} | json path | path != "" [30d]))
avg_over_time({service_name="order-api"} | json path, duration_ms | unwrap duration_ms [30d]) by (path)

 

다만 집계 쿼리도 해당 기간의 로그를 모두 읽어 계산하므로, 로그가 많고 기간이 길수록 오래 걸립니다. Loki의 기본 쿼리 제한 시간은 1분이며, 이를 넘기면 쿼리가 실패합니다.

마무리

서버가 늘어나며 생긴 세 가지 문제는 로그 모니터링 시스템을 구축해 해결했습니다. 이제 여러 서버의 로그를 한곳에서 조회하고, 오류를 알림으로 받고, API 통계를 쿼리로 냅니다.

 

기술 스택을 고를 때는 수집기나 시각화 도구보다 로그 저장소를 먼저 비교한 것이 도움이 되었습니다. 저장 비용, 검색 속도, 확장 방식이 모두 저장소의 색인 방식과 저장 위치에서 갈리기 때문입니다. 로그 모니터링 시스템 도입을 검토한다면 이 두 가지부터 비교해 보기를 권합니다.