로그 수집 및 시각화란?
로그 수집 및 시각화는 애플리케이션과 인프라에서 발생하는 로그를 중앙으로 모으고, 검색과 분석이 가능한 형태로 관리하는 시스템이다.
게임 서버나 실시간 멀티플레이 플랫폼에서는 사용자 인증, 매칭, 게임 세션, 결제, 보상 지급, 데이터베이스 요청 등 다양한 작업에서 로그가 발생한다. 서버가 여러 인스턴스로 분산되면 로그도 여러 곳에 흩어지기 때문에 개별 서버에 접속해 로그 파일을 확인하는 방식만으로는 장애 원인을 빠르게 파악하기 어렵다.
이를 해결하기 위해 로그 수집기인 Grafana Alloy, 로그 저장 및 검색 시스템인 Loki, 로그 시각화 도구인 Grafana를 조합할 수 있다.
각 구성 요소는 다음과 같은 역할을 담당한다.
- Grafana Alloy: 서버와 애플리케이션의 로그를 수집하고 전송한다.
- Loki: 로그를 저장하고 레이블 기반으로 검색할 수 있도록 한다.
- Grafana: 수집된 로그를 검색하고 시각화하며 운영 상황을 분석한다.
이 구조를 사용하면 여러 서버에서 발생한 로그를 하나의 화면에서 조회할 수 있으며, 장애가 발생한 시점과 관련 요청을 추적하기 쉬워진다.
전체 아키텍처
일반적인 로그 수집 파이프라인은 다음과 같이 구성된다.
┌──────────────────────────────┐
│ Game Servers │
│ │
│ API Server │
│ WebSocket Server │
│ Matchmaking Server │
│ Background Worker │
└──────────────┬───────────────┘
│
│ Application Logs
▼
┌──────────────────────────────┐
│ Grafana Alloy │
│ │
│ Log Collection │
│ Parsing │
│ Label Processing │
│ Log Forwarding │
└──────────────┬───────────────┘
│
│ Push Logs
▼
┌──────────────────────────────┐
│ Loki │
│ │
│ Log Storage │
│ Label Indexing │
│ Log Query │
└──────────────┬───────────────┘
│
│ LogQL Query
▼
┌──────────────────────────────┐
│ Grafana │
│ │
│ Log Explorer │
│ Filtering and Search │
│ Dashboard │
│ Incident Analysis │
└──────────────────────────────┘
애플리케이션은 구조화된 로그를 출력하고, Alloy가 로그를 수집해 Loki로 전달한다. Grafana는 Loki를 데이터 소스로 등록한 뒤 LogQL을 사용해 필요한 로그를 조회한다.
여기서 중요한 점은 로그를 생성하는 시스템, 수집하는 시스템, 저장하는 시스템, 조회하는 시스템을 분리하는 것이다. 각 구성 요소를 독립적으로 확장하고 관리할 수 있으며, 애플리케이션이 특정 시각화 도구에 직접 의존하지 않도록 설계할 수 있다.
Grafana Alloy
Grafana Alloy는 로그, 메트릭, 트레이스 등의 텔레메트리 데이터를 수집하고 처리하는 오픈소스 수집기다. Grafana Agent의 후속 제품으로, 여러 관측 데이터 파이프라인을 하나의 에이전트에서 구성할 수 있다.
로그 수집에서는 주로 파일이나 컨테이너 로그를 읽고, 필요한 데이터를 가공한 뒤 Loki로 전달하는 역할을 담당한다.
주요 기능
- 로그 파일 및 지원되는 로그 소스 수집
- 로그 파싱과 처리
- 레이블 추가 및 변환
- Loki를 포함한 지원 대상 시스템으로 데이터 전송
- 수집 파이프라인의 상태 및 오류 모니터링
예를 들어 여러 게임 서버에서 다음과 같은 로그 파일을 생성한다고 가정한다.
/var/log/game-api/app.log
/var/log/matchmaking/app.log
/var/log/game-worker/app.log
Alloy는 설정된 경로에서 로그를 읽어 Loki로 전달할 수 있다. 컨테이너 환경에서는 배포 방식과 런타임에 맞춰 컨테이너 로그를 수집하도록 구성할 수도 있다.
Alloy 설정 예시
다음은 파일 로그를 수집해 Loki로 전달하는 기본적인 설정 예시다.
local.file_match "game_logs" {
path_targets = [
{
"__path__" = "/var/log/game-api/*.log",
"service" = "game-api",
"environment" = "production",
},
]
}
loki.source.file "game_logs" {
targets = local.file_match.game_logs.targets
forward_to = [loki.process.game_logs.receiver]
}
loki.process "game_logs" {
stage.json {
expressions = {
level = "level",
request_id = "requestId",
message = "message",
}
}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
이 설정은 로그 파일을 탐색하고, 파일 경로에 따라 수집 대상을 지정한 다음 JSON 로그를 파싱해 Loki로 전달한다.
JSON 파싱을 통해 로그 내부의 level, requestId, message 필드를 추출할 수 있다. 다만 추출한 필드가 자동으로 모두 Loki의 인덱싱 레이블이 되는 것은 아니다. 필요한 경우 별도의 처리 단계를 구성해야 한다.
또한 파일 경로, 읽기 권한, 로그 로테이션, 컨테이너 마운트 경로 등을 실제 배포 환경에 맞게 조정해야 한다.
Alloy 운영 시 고려 사항
Alloy는 로그를 수집하는 중간 계층이므로 수집 실패가 발생하면 로그 유실로 이어질 수 있다.
- 수집 대상 파일이 실제로 존재하는지 확인한다.
- 로그 파일에 대한 읽기 권한을 부여한다.
- 로그 로테이션 시 파일 추적이 정상적으로 이어지는지 확인한다.
- Loki 연결 실패와 재시도 동작을 모니터링한다.
- 수집기 재시작이나 디스크 부족 상황에서 로그가 어떻게 처리되는지 검증한다.
특히 로그를 파일로 출력하는 애플리케이션에서는 로그 로테이션과 파일 추적 정책이 중요하다. 파일이 교체되거나 잘렸을 때 수집기가 새로운 로그를 놓치지 않는지 확인해야 한다.
Loki
Loki는 Grafana에서 개발한 로그 집계 시스템이다. 로그 전체의 내용을 일반적인 전문 검색 인덱스로 구성하는 대신, 주로 레이블을 인덱싱하고 실제 로그 본문은 별도로 저장하는 방식을 사용한다.
이 구조는 레이블을 적절하게 설계했을 때 인덱스 비용을 줄이고 대량의 로그를 효율적으로 관리하는 데 유리하다.
레이블 기반 로그 관리
로그에는 서비스 이름, 실행 환경, 서버 역할과 같은 메타데이터를 레이블로 부여할 수 있다.
예를 들어 다음과 같은 로그가 존재한다고 가정한다.
{
"timestamp": "2026-10-24T09:30:00Z",
"level": "error",
"service": "matchmaking",
"requestId": "req-a81f",
"message": "Failed to allocate game room"
}
이 로그를 Loki에 저장할 때 다음과 같은 레이블을 활용할 수 있다.
service="matchmaking"
environment="production"
이후 특정 서비스에서 발생한 로그만 조회하거나 운영 환경의 오류만 필터링할 수 있다.
여기서 level이나 requestId 같은 필드는 로그 본문에 포함할 수 있으며, 반드시 레이블로 지정할 필요는 없다.
레이블 카디널리티
Loki를 설계할 때 가장 중요한 원칙 중 하나는 레이블의 카디널리티를 관리하는 것이다.
카디널리티는 레이블이 가질 수 있는 고유한 값의 수를 의미한다. service, environment, region처럼 값의 종류가 제한된 필드는 일반적으로 레이블로 사용하기 적합하다.
반면 다음과 같은 값은 레이블로 사용하지 않는 것이 좋다.
- 사용자 ID
- 요청 ID
- 세션 ID
- UUID
- 매 요청마다 달라지는 임의의 값
이러한 값을 레이블로 지정하면 고유한 레이블 조합이 급격히 증가해 인덱스와 메모리 사용량이 커질 수 있다.
예를 들어 사용자마다 별도의 레이블 조합을 생성하는 대신 service="game-api"와 같은 안정적인 레이블을 사용하고, 특정 사용자의 로그가 필요할 때 로그 본문에서 해당 ID를 검색하는 방식이 적합하다.
단, 로그 본문 검색은 선택한 로그 스트림의 범위와 저장된 데이터에 따라 처리 비용이 커질 수 있다. 따라서 적절한 레이블 선택과 검색 범위 제한이 중요하다.
LogQL
LogQL은 Loki에서 로그를 조회하기 위한 쿼리 언어다. 레이블 선택기로 로그 스트림을 선택하고, 파이프라인 연산자를 사용해 로그를 파싱하거나 필터링할 수 있다.
서비스 로그 조회 예시는 다음과 같다.
{service="game-api"}
특정 서비스의 오류 로그만 검색할 수도 있다.
{service="game-api"} |= "error"
JSON 로그를 파싱한 뒤 로그 레벨을 기준으로 필터링할 수도 있다.
{service="game-api"}
| json
| level="error"
요청 ID를 사용해 특정 요청과 관련된 로그를 찾는 예시는 다음과 같다.
{service="game-api"}
| json
| requestId="req-a81f"
이 방식은 요청 처리 과정에서 여러 로그가 발생했을 때 특정 요청의 흐름을 추적하는 데 유용하다.
다만 요청 ID가 로그마다 일관되게 기록되어 있어야 하며, 실제 JSON 필드 이름과 자료형에 맞춰 쿼리를 작성해야 한다.
Grafana
Grafana는 로그, 메트릭, 트레이스 등 다양한 관측 데이터를 조회하고 시각화하는 플랫폼이다. Loki를 데이터 소스로 연결하면 로그를 검색하고 대시보드에서 관련 지표와 함께 분석할 수 있다.
Loki 데이터 소스 등록
Grafana에서 Loki를 데이터 소스로 등록하면 LogQL 쿼리를 실행할 수 있다.
일반적인 구성 절차는 다음과 같다.
- Grafana의 데이터 소스 설정으로 이동한다.
- Loki 데이터 소스를 추가한다.
- Loki 서버의 주소를 입력한다.
- 연결 상태를 확인하고 설정을 저장한다.
- Explore 화면에서 LogQL 쿼리를 실행한다.
Docker Compose 환경에서 두 서비스가 같은 네트워크에 있다면 Loki의 서비스 이름을 주소로 사용할 수 있다.
http://loki:3100
이 주소는 예시이며, 실제 주소는 배포 환경과 네트워크 구성에 따라 달라진다.
로그 탐색과 대시보드
Grafana의 Explore 기능은 장애 원인을 조사할 때 유용하다. 특정 시간 범위와 레이블을 선택해 로그를 조회하고, 로그 본문을 확장해 상세 내용을 확인할 수 있다.
대시보드에서는 로그와 관련 메트릭을 함께 배치할 수 있다.
예를 들어 다음과 같은 항목을 구성할 수 있다.
- 서비스별 오류 발생량
- 최근 발생한 오류 로그
- API 요청 지연 시간
- WebSocket 연결 수
- 매칭 실패 횟수
- 데이터베이스 연결 오류
- 게임 세션 생성 실패
이렇게 구성하면 단순히 오류 메시지를 확인하는 것을 넘어 오류가 증가한 시점에 요청 지연 시간이나 연결 수가 함께 변했는지 비교할 수 있다.
다만 로그 자체의 건수를 시계열로 집계하려면 LogQL의 로그 메트릭 쿼리를 사용해야 하며, 지연 시간이나 연결 수처럼 애플리케이션이 별도로 계측하는 지표는 Prometheus 등의 메트릭 데이터 소스와 함께 구성하는 것이 적절하다.
로그 필터링 및 검색
로그 시스템의 가치는 로그를 저장하는 데서 끝나지 않는다. 장애 발생 시 필요한 로그를 빠르게 좁혀서 원인을 찾을 수 있어야 한다.
검색 효율을 높이려면 레이블 필터링, 본문 검색, 구조화된 필드 추출을 적절히 조합해야 한다.
서비스별 필터링
특정 서비스의 로그만 조회하려면 레이블 선택기를 사용한다.
{service="matchmaking"}
운영 환경의 로그만 확인하려면 환경 레이블을 추가한다.
{service="matchmaking", environment="production"}
이처럼 처음부터 검색 대상을 제한하면 불필요한 로그를 읽는 비용을 줄일 수 있다.
문자열 기반 검색
로그 본문에 포함된 특정 문자열을 검색할 수 있다.
{service="game-api"} |= "timeout"
특정 문자열을 제외하려면 다음과 같이 작성한다.
{service="game-api"} != "health check"
문자열 검색은 빠르게 조사할 때 편리하지만, 동일한 의미의 오류가 서로 다른 메시지로 기록되면 검색 결과가 누락될 수 있다. 따라서 오류 유형을 일관된 코드나 구조화된 필드로 기록하는 것이 좋다.
JSON 필드 기반 검색
구조화된 로그를 사용하면 로그 본문을 파싱해 필드별로 검색할 수 있다.
{service="game-api"}
| json
| level="error"
특정 오류 코드만 찾는 것도 가능하다.
{service="game-api"}
| json
| errorCode="ROOM_ALLOCATION_FAILED"
이 방식은 단순한 문자열 검색보다 명확한 조건을 구성할 수 있다. 다만 JSON 파싱 비용이 발생할 수 있으므로 먼저 레이블로 검색 범위를 제한하는 것이 좋다.
시간 범위 제한
장애 분석에서는 시간 범위를 정확히 설정해야 한다. 특정 시점의 오류를 조사할 때 전체 보관 기간의 로그를 검색하는 대신 장애가 시작된 시점과 종료된 시점을 중심으로 조회한다.
시간 범위는 다음과 같은 기준으로 좁힐 수 있다.
- 오류가 처음 발생한 시점
- 배포가 진행된 시점
- 사용자 신고가 접수된 시점
- 지연 시간이 급격히 증가한 시점
- 연결이 대량으로 종료된 시점
시간 범위와 서비스 레이블을 함께 지정하면 검색해야 할 데이터가 줄어들고 장애의 원인과 결과를 구분하기 쉬워진다.
실시간 게임 서버에 적용하기
실시간 게임 서버에서는 요청이 여러 서비스와 작업 큐를 거치기 때문에 로그를 개별적으로 확인하는 것만으로는 문제의 흐름을 파악하기 어렵다.
예를 들어 사용자가 매칭을 요청했지만 게임 룸에 입장하지 못했다고 가정한다.
Client
│
▼
API Server
│
▼
Matchmaking Server
│
▼
Room Allocation
│
▼
WebSocket Server
이 과정에서 각 서비스가 동일한 requestId 또는 적절한 상관관계 식별자를 기록한다면, Grafana에서 해당 식별자를 검색해 요청의 처리 과정을 추적할 수 있다.
로그는 다음과 같이 구성할 수 있다.
{
"level": "info",
"service": "game-api",
"requestId": "req-a81f",
"message": "Matchmaking requested"
}
{
"level": "info",
"service": "matchmaking",
"requestId": "req-a81f",
"message": "Match found"
}
{
"level": "error",
"service": "room-service",
"requestId": "req-a81f",
"message": "Room allocation failed"
}
이 로그를 검색하면 매칭 자체는 성공했지만 룸 할당 단계에서 실패했다는 사실을 확인할 수 있다.
실제 운영 환경에서는 requestId 외에도 sessionId, matchId, roomId 등을 기록할 수 있다. 다만 이런 값은 일반적으로 고카디널리티 레이블이 아니라 로그 본문 필드로 관리하는 편이 적절하다. 사용자 식별 정보나 인증 토큰 등 민감한 데이터는 로그에 그대로 기록하지 않아야 한다.
운영 환경에서 고려해야 할 사항
로그 보관 기간
모든 로그를 무기한 저장하면 스토리지 비용이 증가한다. 로그의 중요도와 분석 목적에 따라 보관 기간을 설정해야 한다.
- 디버그 로그는 짧은 기간 보관한다.
- 일반적인 애플리케이션 로그는 운영 정책에 맞춰 보관한다.
- 보안 및 정산 관련 로그는 감사 요구사항에 맞춰 보관한다.
Loki의 보관 정책은 사용하는 스토리지 백엔드와 배포 방식에 따라 달라질 수 있다. 보관 기간과 삭제 정책을 명시적으로 구성하고 실제 데이터가 정책에 따라 정리되는지 검증해야 한다.
로그 유실과 전송 실패
Alloy에서 Loki로 로그를 전송하는 과정에서 네트워크 장애나 저장소 오류가 발생할 수 있다.
이때 수집기의 재시도 동작만 믿기보다 버퍼링 정책, 재시작 이후의 로그 추적, 디스크 용량, 장애 복구 시나리오를 함께 검토해야 한다.
로그가 반드시 보존되어야 하는 시스템이라면 수집 파이프라인의 장애가 애플리케이션의 핵심 처리에 영향을 주지 않으면서도 필요한 로그를 보존할 수 있도록 별도의 내구성 전략을 설계해야 한다.
민감 정보 보호
로그에는 인증 정보, 사용자 식별자, 내부 서버 주소, 요청 데이터 등이 포함될 수 있다.
따라서 다음과 같은 정책을 적용하는 것이 좋다.
- 비밀번호와 액세스 토큰을 기록하지 않는다.
- 개인정보는 필요한 경우 마스킹하거나 제거한다.
- 로그 조회 권한을 역할별로 제한한다.
- 외부에 노출되는 로그 엔드포인트를 보호한다.
- 로그 접근 및 보관 정책을 점검한다.
로그 시스템은 장애 분석 도구인 동시에 운영 데이터가 집중되는 저장소다. 접근 제어와 데이터 보호를 일반 데이터베이스 수준으로 중요하게 다뤄야 한다.
로그와 메트릭의 역할 구분
로그는 개별 사건의 상세 내용을 설명하는 데 적합하다. 메트릭은 일정 시간 동안의 추세와 시스템 상태를 파악하는 데 적합하다.
예를 들어 API 오류율이 증가했다는 사실은 메트릭으로 확인하고, 어떤 요청에서 어떤 예외가 발생했는지는 로그로 확인한다.
두 데이터를 함께 사용하면 장애 감지부터 원인 분석까지 연결할 수 있다. 필요하다면 트레이싱 시스템까지 연동해 서비스 간 호출 관계와 지연 시간을 분석할 수 있다.