로그 시스템 구축하기 — Pino, Grafana Alloy, Loki, Grafana #Node.js

게임 서버를 운영할 때 로그는 단순히 콘솔에 출력하는 디버깅 정보가 아니다. 서버 오류를 추적하고, 유저 접속 문제를 분석하고, 여러 서버에서 발생한 문제를 통합적으로 조사하기 위한 운영 데이터다.

Node.js 게임 서버에서 Pino로 로그를 생성하고, Grafana Alloy로 수집한 뒤, Loki에 저장하고 Grafana에서 검색하는 로그 시스템을 구축해 보자.

로그 시스템의 전체 구조

각 구성 요소는 명확하게 역할이 분리되어 있다.

  • Pino: Node.js 애플리케이션의 로그 생성
  • stdout / stderr: 로그가 출력되는 표준 출력 및 표준 에러 스트림
  • Docker: 컨테이너의 출력 로그 수집
  • Grafana Alloy: 로그 수집 및 전송
  • Grafana Loki: 로그 저장 및 검색
  • Grafana: 로그 조회 및 모니터링 UI

전체 데이터 흐름은 다음과 같다.

Node.js Game Server
        |
        v
      Pino
        |
        v
 stdout / stderr
        |
        v
 Docker Container Logs
        |
        v
 Grafana Alloy
        |
        v
      Loki
        |
        v
     Grafana UI

핵심은 게임 서버가 Loki에 직접 로그를 전송하는 것이 아니라, 로그 생성과 수집 및 저장을 분리한다는 점이다.

Pino — 구조화된 로그 생성

Pino는 Node.js 환경에서 사용하는 고성능 로깅 라이브러리다.

console.log()로 문자열만 출력할 수도 있지만, 서버 운영에서는 로그 레벨과 필드가 구조화된 로그가 유리하다.

설치

npm install pino

ES Modules를 사용한다면 package.json에 다음 설정을 추가한다.

{
  "type": "module"
}

로거 구현

src/logger.js

import pino from 'pino';

export const logger = pino({
  name: 'game-server',
  level: process.env.LOG_LEVEL || 'info',
});

이제 애플리케이션 어디에서든 공통 로거를 사용할 수 있다.

게임 서버에서 로그 기록

src/server.js

import express from 'express';
import { logger } from './logger.js';

const app = express();

app.get('/health', (req, res) => {
  logger.info({
    event: 'health_check',
    method: req.method,
    path: req.path,
  }, 'Health check requested');

  res.json({ status: 'ok' });
});

app.get('/test-error', (req, res) => {
  logger.error({
    event: 'test_error',
    userId: 'user-123',
  }, 'Test error occurred');

  res.status(500).json({ error: 'Test error' });
});

app.listen(3000, () => {
  logger.info({ port: 3000 }, 'Game server started');
});

Pino는 기본적으로 JSON 형식의 로그를 표준 출력으로 기록한다.

출력 예시는 다음과 같다.

{
  "level": 30,
  "time": 1792580000000,
  "pid": 1,
  "hostname": "game-server",
  "name": "game-server",
  "event": "health_check",
  "method": "GET",
  "path": "/health",
  "msg": "Health check requested"
}

이러한 구조화 로그는 특정 이벤트, 유저, 매치 등을 기준으로 검색하기 쉽다.

Docker — 콘솔 로그 수집

Docker로 애플리케이션을 실행하면 컨테이너의 표준 출력과 표준 에러에 기록된 로그를 Docker 로깅 시스템이 수집할 수 있다.

Dockerfile

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY src ./src

EXPOSE 3000

CMD ["node", "src/server.js"]

컨테이너를 실행하면 Pino의 로그를 다음 명령으로 확인할 수 있다.

docker logs game-server

여기서 중요한 점은 Alloy가 실제 터미널 화면을 캡처하는 것이 아니라 Docker가 관리하는 컨테이너 로그를 수집한다는 것이다.

console.log()와 console.error()로 출력한 로그 역시 해당 출력 스트림에 기록되므로 수집 대상이 될 수 있다.

Grafana Alloy — 로그 수집 및 전송

Alloy는 로그, 메트릭, 트레이스 등을 수집하고 전달하는 관측 데이터 수집기다.

이번 구성에서는 Docker 컨테이너 로그를 읽어 Loki로 전달한다.

설정 파일 작성

monitoring/config.alloy

discovery.docker "containers" {
  host = "unix:///var/run/docker.sock"
}

loki.source.docker "game_logs" {
  host    = "unix:///var/run/docker.sock"
  targets = discovery.docker.containers.targets

  labels = {
    job = "game-server"
  }

  forward_to = [loki.write.local.receiver]
}

loki.write "local" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

각 컴포넌트의 역할은 다음과 같다.

  • discovery.docker: Docker 컨테이너를 검색한다.
  • loki.source.docker: 컨테이너 로그를 읽는다.
  • labels: 수집한 로그에 검색용 라벨을 부여한다.
  • loki.write: 로그를 Loki의 HTTP API로 전송한다.

http://loki:3100은 Docker Compose 내부 네트워크에서 Loki 서비스에 접근하기 위한 주소다.

Docker 소켓은 컨테이너 관리 권한과 연결될 수 있으므로 운영 환경에서는 접근 권한을 제한해야 한다.

Loki — 로그 저장 및 검색

Loki는 로그를 수집하고 저장하며, LogQL을 사용해 검색할 수 있도록 하는 로그 집계 시스템이다.

Loki 자체가 애플리케이션 로그를 자동으로 가져오는 것은 아니다. Alloy 같은 수집기가 로그를 전달해야 한다.

예를 들어 다음과 같은 로그가 저장되어 있다고 가정하자.

{
  "event": "player_connected",
  "userId": "user-123",
  "matchId": "match-456",
  "msg": "Player connected"
}

Grafana에서 Loki를 데이터 소스로 연결하면 LogQL로 로그를 검색할 수 있다.

게임 서버 로그 전체 조회

{job="game-server"}

특정 메시지 검색

{job="game-server"} |= "Player connected"

JSON 필드를 기준으로 검색

{job="game-server"} | json | event="player_connected"

JSON 파싱 결과의 필드명과 데이터 형식에 따라 쿼리를 조정하면 된다.

Loki는 로그를 저장하고 검색하는 데 적합하지만, 대량의 게임 이벤트를 집계해 복잡한 유저 행동 통계를 분석하는 전용 OLAP 데이터베이스를 대체하는 것은 아니다.

Docker Compose — 전체 시스템 실행

다음 설정으로 게임 서버와 로그 모니터링 시스템을 함께 실행할 수 있다.

docker-compose.yml

services:
  game-server:
    build: .
    container_name: game-server
    init: true
    ports:
      - "3000:3000"
    environment:
      LOG_LEVEL: info
    restart: unless-stopped

  loki:
    image: grafana/loki:3.6.0
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - loki-data:/loki
    restart: unless-stopped

  alloy:
    image: grafana/alloy:latest
    command:
      - run
      - --server.http.listen-addr=0.0.0.0:12345
      - /etc/alloy/config.alloy
    volumes:
      - ./monitoring/config.alloy:/etc/alloy/config.alloy:ro
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - loki
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      - loki
    restart: unless-stopped

volumes:
  loki-data:
  grafana-data:

위 설정은 로컬 테스트용 기본 예시다. 실제 배포 전에는 Alloy와 Grafana의 이미지 버전도 검증해 고정하고, 각 서비스의 저장소 권한과 네트워크 접근 정책을 확인해야 한다.

실행

docker compose up -d --build

각 서비스는 다음 주소에서 확인할 수 있다.

서비스주소
게임 서버http://localhost:3000
Grafanahttp://localhost:3001
Alloy 상태 페이지http://localhost:12345

Grafana에 로그인한 후 데이터 소스를 추가한다.

  1. Connections → Data sources로 이동한다.
  2. Add data source를 선택한다.
  3. Loki를 선택한다.
  4. URL에 http://loki:3100을 입력한다.
  5. Save & test를 실행한다.
  6. Explore 메뉴에서 {job="game-server"} 쿼리를 실행한다.

정상적으로 구성되었다면 게임 서버에서 발생한 로그를 웹 UI에서 검색할 수 있다.

운영 환경에서 고려할 사항

로그 전송과 게임 로직 분리

로그 수집 시스템의 장애가 게임 플레이를 중단시키지 않도록 해야 한다. 로그 수집과 전송을 비동기적으로 구성하고, 장애 시 로그가 어떻게 처리되는지도 검토해야 한다.

로그 보관 기간

로그가 계속 누적되면 디스크 사용량이 증가한다. Loki의 보관 정책과 저장소 용량을 관리해야 한다.

민감한 정보 제거

비밀번호, 인증 토큰, 세션 키 등 민감한 정보를 로그에 기록하지 않는다. IP 주소와 유저 식별 정보 역시 필요한 범위에서만 기록한다.

중요한 게임 데이터는 별도 저장

재화 변경, 결제, 거래처럼 정합성과 감사 추적이 중요한 데이터는 일반적인 운영 로그만으로 관리하지 않는다. 트랜잭션과 영속성을 고려한 별도의 저장 구조가 필요하다.

정리

Pino, Alloy, Loki, Grafana는 서로 다른 책임을 가진다.

  • Pino는 애플리케이션 로그를 생성한다.
  • Docker는 컨테이너의 출력 로그를 수집한다.
  • Alloy는 로그를 읽고 Loki로 전달한다.
  • Loki는 로그를 저장하고 검색할 수 있게 한다.
  • Grafana는 로그를 시각적으로 조회할 수 있게 한다.

이 구조의 핵심은 게임 서버에서 로그를 생성하는 작업과 로그를 저장·검색하는 작업을 분리하는 것이다.

개발 초기에는 Pino와 콘솔 로그만으로 시작해도 충분하다. 서버가 여러 대로 늘어나거나 운영 중 장애를 빠르게 추적해야 할 때 Alloy, Loki, Grafana를 추가하면 된다.

참고 자료:

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다