라즈베리파이에 Loki + Grafana로 로그 수집 시스템 구축하기

2025. 12. 22. 16:16·Infra/LogBack

 

 

문제

기존에는 EC2 환경에서 ELK 스택을 사용해 로그를 수집하고 있었다.
하지만 개인 프로젝트와 홈 서버 성격의 서비스까지 포함되면서 다음 문제가 발생했다.

  • ELK는 메모리 사용량이 너무 큼
  • 라즈베리파이의 RAM 용량은 4~8GB이지만 ELK를 가볍게 띄워도 3~4GB가 소요됨
  • 라즈베리파이의 디스크 IO 성능 제한

 

지금 환경에서 ELK는 기능은 충분하지만, 너무 무거우므로 “가벼운 로그 수집 시스템”이 필요하다.

  • 서비스별 로그 흐름 파악
  • 장애 발생 시 빠른 원인 추적
  • 저사양 환경에서도 안정적으로 동작
  • 운영 부담이 적을 것

 

 

 

Loki + Grafana + Promtail 조합

기존 로그 시스템과 달리 로그 본문을 인덱싱 하지 않고 로그에 붙은 라벨(Label)만 인덱싱하는 구조

  • 메모리 사용량이 낮고
  • 디스크 IO 부담이 적으며
  • 라즈베리파이 같은 저사양 환경에 적합하다

각 구성요소 역할

Loki: Grafana Labs에서 만든 로그 수집·저장 시스템

Grafana: 시각화 & 검색 UI ( “관측성(Observability)을 시각화하는 플랫폼”으로 Grafana 자체는 데이터를 저장하지 않는다.)

Promtail :  로그 수집 Agent (로그 파일을 읽어, 라벨을 붙인 뒤 Loki로 전달)

 

 

 

 

기존 로그 시스템(ELK)의 방식

Full Text Index 방식으로 로그 내용 전체를 인덱싱 ( 모든 필드, 모든 텍스트를 검색 가능 )

[ERROR] OrderService - orderId=1234 timeout occurred

문제점

  • 인덱스 용량 큼
  • 메모리 사용량 큼
  • 디스크 IO 부담

 

 

Loki의 접근 방식: 라벨(Label) 기반

❝ 로그의 내용을 인덱싱 하지 않으며, 대신 로그의 메타데이터(label)만 인덱싱 한다 ❞

라벨(Label)이란?

- 라벨은 로그 한 줄 한 줄에 붙는 검색용 메타데이터

 

이러한 예시 로그가 있다면

2025-12-19 11:36:29 ERROR OrderService orderId=1234 timeout

Loki에 넣을 때 이런 방식으로 로그를 넣게 된다.

즉 Loki는 오직 이 label만 인덱싱하여 로그 본문(message)은 인덱싱 되지 않는다.

labels:
  job: chill-logistics
  service: order-server
  level: ERROR
  instance: raspberrypi-01

 

라벨 기반의 장점

  • 로그 본문은 사용하지 않으며 메타데이터(label)만 인덱싱 사용
  • 인덱싱 비용 거의 없음
  • 로그 쓰기 속도 빠름

 

 

“ ELK : 일단 다 넣고 나중에 검색하자”

 

“ Loki : 어떤 기준으로 검색할지를 미리 정하자”

 

항목 ELK Loki
메모리 사용 높음 낮음
인덱싱 방식 Full Index Label 기반

 

 

 

 

 

 


Code

1. Loki + Grafana의 Docker Compose 파일로 저장 후 실행 시켜준다.

예시 : docker compose -f docker-compose-logging.yml up -d

services:
  loki:
    image: grafana/loki:2.9.8
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    restart: always
    volumes:
      - loki_data:/loki

  grafana:
    image: grafana/grafana:10.4.5
    container_name: grafana
    ports:
      - "3000:3000"
    restart: always
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      # 필요하면 아래도 추가
      # - GF_SERVER_ROOT_URL=http://<GRAFANA_HOST>:3000
    depends_on:
      - loki
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  loki_data:
  grafana_data:

 

 

 

 

2. 로그 저장 형태인 logback.xml 파일은 기존 ELK 형태의 방식과 같다

spring에서 resources 폴더 -> logback.xml 파일로 넣어주면 됩니다

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <!-- 로그 경로 -->
  <property name="LOG_PATH" value="/app/logs"/>
  <property name="INFO_PATH" value="${LOG_PATH}/info" />
  <property name="WARN_PATH" value="${LOG_PATH}/warn" />
  <property name="ERROR_PATH" value="${LOG_PATH}/error" />

  <!-- =============== 콘솔 =============== -->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- =============== INFO 전용 JSON 로그 파일 =============== -->
  <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${INFO_PATH}/application-info.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${INFO_PATH}/application-info-%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory> <!--  7일간 보관 -->
    </rollingPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <threadName/>
        <loggerName/>
        <message/>
        <arguments/>
        <stackTrace/>
          <mdc>
            <includeMdcKeyName>traceId</includeMdcKeyName>
          </mdc>
      </providers>
    </encoder>
  </appender>

  <!-- =============== WARN 전용 JSON 로그 =============== -->
  <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${WARN_PATH}/application-warn.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${WARN_PATH}/application-warn-%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory>
    </rollingPolicy>

    <!-- WARN만 -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>WARN</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>

    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <threadName/>
        <loggerName/>
        <message/>
        <arguments/>
        <stackTrace/>
        <mdc>
          <includeMdcKeyName>traceId</includeMdcKeyName>
        </mdc>
      </providers>
    </encoder>
  </appender>


  <!-- =============== ERROR 전용 JSON 로그 파일 =============== -->
  <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${ERROR_PATH}/application-error.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${ERROR_PATH}/application-error-%d{yyyy-MM-dd}.log</fileNamePattern>
      <maxHistory>7</maxHistory> <!-- 7일간 보관  -->
    </rollingPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>ERROR</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <logLevel/>
        <threadName/>
        <loggerName/>
        <message/>
        <arguments/>
        <stackTrace/>
        <mdc>
          <includeMdcKeyName>traceId</includeMdcKeyName>
        </mdc>
      </providers>
    </encoder>
  </appender>

  <!-- =============== 루트 로거 =============== -->
  <!--  root logger의 레벨을 설정으로 INFO 이상 레벨(INFO, WARN, ERROR) 만 출력한다 -->
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="INFO_FILE"/>
    <appender-ref ref="WARN_FILE"/>
    <appender-ref ref="ERROR_FILE"/>
  </root>

</configuration>

 

 

 

3. Loki + Grafana가 있는 서버로 보내기 위해 Promtail 사용

주의 : 해당 경로에 config.yml 파일이 존재해야 합니다.

/c/app/promtail/config.yml

- config.yml 파일 (/*.log 부분 때문에 주석으로 인식 중이므로 무시해도 됩니다.)

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  # 로깅 서버(Loki) 주소
  - url: http://221.xxx.xxx.xxx:3100/loki/api/v1/push

scrape_configs:
  - job_name: chill-logistics-info
    static_configs:
      - targets: [localhost]
        labels:
          job: chill-logistics
          service: business  # 어떤 서비스인지 예로 user-server, firm-server처럼 구체적으로 사용이 좋지만 로컬 개발용으로 한번에 하므로
          level: INFO
          __path__: /app/logs/info/*.log
    pipeline_stages:
      - json:
          expressions:
            timestamp: timestamp
            logLevel: logLevel
            threadName: threadName
            loggerName: loggerName
            message: message
            traceId: traceId
      - timestamp:
          source: timestamp
          format: RFC3339Nano

  - job_name: chill-logistics-warn
    static_configs:
      - targets: [localhost]
        labels:
          job: chill-logistics
          service: business
          level: WARN
          __path__: /app/logs/warn/*.log
    pipeline_stages:
      - json:
          expressions:
            timestamp: timestamp
            logLevel: logLevel
            threadName: threadName
            loggerName: loggerName
            message: message
            traceId: traceId
      - timestamp:
          source: timestamp
          format: RFC3339Nano

  - job_name: chill-logistics-error
    static_configs:
      - targets: [localhost]
        labels:
          job: chill-logistics
          service: business
          level: ERROR
          __path__: /app/logs/error/*.log
    pipeline_stages:
      - json:
          expressions:
            timestamp: timestamp
            logLevel: logLevel
            threadName: threadName
            loggerName: loggerName
            message: message
            traceId: traceId
      - timestamp:
          source: timestamp
          format: RFC3339Nano

 

 

- config.yml 파일을 경로에 넣고, Promtail Docker Compose 파일 생성 (스프링 서버가 돌아가는 서버에서 실행 시켜준다.)

예시 : docker compose -f docker-compose-promtail.yml up -d

services:
  promtail:
    image: grafana/promtail:2.9.8
    container_name: promtail
    restart: always
    command: -config.file=/etc/promtail/config.yml
    volumes:
      - /c/app/promtail/config.yml:/etc/promtail/config.yml:ro
      - /c/app/logs:/app/logs:ro
      - promtail_positions:/tmp
    ports:
      - "${PROMTAIL_PORT}:${PROMTAIL_PORT}"

volumes:
  promtail_positions:

 

 

 

 

 

- 모든 서버를 실행 후 로그가 발생한다면 Loki + Grafana가 실행 중인 서버에 3000번 포트로 들어가면 Grafana에 들어갈 수 있습니다.

- 예시 : http://221.xxx.xxx.xxx:3000  (만약 로그인 창이 뜬다면 초기 값은   ID : admin     PW : admin 을 넣어주면 됩니다.)

 

 

 

 

 

결론

1. 라즈베리파이와 같은 저사양 환경에서 ELK 스택은 기능적으로는 충분하지만, 운영 비용과 리소스 측면에서 과한 선택이었다.

2. Loki + Grafana + Promtail 조합은 로그 본문을 인덱싱 하지 않고 라벨 기반으로 접근하는 구조 덕분에 제한된 자원 환경에서도 안정적인 로그 수집과 조회를 가능하게 했다.

 

 

'Infra > LogBack' 카테고리의 다른 글

Spring Boot + Logback 구조적 JSON 로그 만들기 (Logging - 2)  (0) 2025.11.29
FileBeat를 활용해 로그 전송 (Loggin - 3)  (0) 2025.11.22
스프링 로그 분석 환경 구축 Elasticsearch + Kibana 설치 (Logging - 1)  (0) 2025.11.21
'Infra/LogBack' 카테고리의 다른 글
  • Spring Boot + Logback 구조적 JSON 로그 만들기 (Logging - 2)
  • FileBeat를 활용해 로그 전송 (Loggin - 3)
  • 스프링 로그 분석 환경 구축 Elasticsearch + Kibana 설치 (Logging - 1)
kimfishes
kimfishes
kimfishes 님의 블로그 입니다.
  • kimfishes
    kimfishes 님의 블로그
    kimfishes
  • 전체
    오늘
    어제
    • 전체 (18) N
      • Infra (5)
        • AWS (0)
        • LogBack (4)
      • Spring Boot (13) N
        • LLM (4) N
      • 일상 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    캐시 스탬피드
    Redis
    실시간 알림 시스템
    Discord 알림 연동
    스프링 알림 시스템
    traceId
    promtail
    LLM
    Qdrant
    Pre-Signed URL
    로깅
    loging
    ollama
    ELK
    pgvector
    Spring boot
    UUID v7
    spring ai
    분산 락
    cache stampede
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
kimfishes
라즈베리파이에 Loki + Grafana로 로그 수집 시스템 구축하기
상단으로

티스토리툴바