본문 바로가기
text/common

fluent-bit 를 이용해 로그 모으기

by hoonzii 2025. 6. 16.
반응형

배경: Docker Swarm 환경에서의 로그 수집

회사에서 서버 이전 작업을 진행하면서 기존 서비스들을 Docker Swarm 위에 올리는 작업을 수행하게 되었습니다.

동시에 구성해 놨던 Loki + Grafana 환경에 전체 컨테이너 로그를 모니터링할 수 있도록 연동하는 과제가 주어졌습니다.

 

fluent-bit란?

fluent-bit는 lightweight 로그 수집 도구로, 다양한 로그 소스를 수집한 뒤 태그를 붙이고,

필터링하여 원하는 로그 저장소로 전송할 수 있도록 도와주는 log collector입니다.

공식 사이트에서는 다음과 같이 소개하고 있습니다.

 

로그를 수집하고, tag를 붙이고, 필터를 통해 가공한 뒤 Loki, Elasticsearch, Kafka, Splunk 등 다양한 저장소로 전송할 수 있습니다.

이 모든 기능을 단일 binary로 제공하며, 메모리 사용량도 적어 리소스가 한정된 환경에서도 사용하기 적합합니다.

 

 

시스템 구성

시스템 구성은 다음과 같습니다.

  • Docker Swarm은 다음과 같이 구성되어 있습니다.
    • manager, node1, node2

간략히 나타낸 docker swarm 환경

목표

  • 모든 Docker 노드의 로그를 Loki로 수집
  • 로그에 서비스 이름 라벨 부여
  • fluent-bit 컨테이너는 각 노드당 1개만 실행

목표 아키텍쳐

 

작업 설명을 들을때도, 이전 springboot -> promtail -> loki를 예제로 다뤄봤기 때문에 별 어려움이 없을 거라 생각했습니다만...

 

문제 발생

Docker 로그는 다음 경로에 저장됩니다.

/var/lib/docker/containers/<container_id>/<container_id>-json.log

 

하지만 다음과 같은 문제가 존재합니다.

  • 컨테이너 ID는 알 수 있지만, docker swarm 서비스 이름은 알 수 없음
  • Docker API를 통해 swarm의 서비스 이름을 조회할 수 없음
  • 로그 내용에도 서비스 이름 정보가 없음

해결방법

fluent-bit의 경우, 지정된 config 파일을 읽고 파일내 지정된 대로 pipeline을 구성해 데이터를 수집 및 전달하게 됩니다.

각 설정은 input, filter, output으로 나뉘고, 각 단계별로 document에 적힌 대로 시스템 내 built-in 된 api를 이용하거나

lua 언어로 전처리를 할수 있게 되어 있습니다.

 

제가 작성한 fluent-bit.conf를 같이 보자면

[SERVICE]
    Flush        1
    Log_Level    info

[INPUT]
    Name            tail
    Path            /var/lib/docker/containers/*/*.log
    Parser          docker
    Docker_Mode     On
    Tag             docker.<file_name>
    Tag_Regex       (?<file_name>[a-z0-9]*)-json.log

[FILTER]
    Name    lua
    Match   docker.*
    Script  docker-metadata.lua
    Call    enrich_with_docker_metadata

[OUTPUT]
    Name       loki
    Match      docker.*
    Host       <loki_server_ip>
    Port       <loki_server_port>
    Labels     job=fluent-bit
    label_keys $service_name

 

input으로 tail을 이용해 실제 container별 로그에 접근하고, 값을 읽어오게끔 합니다. 이때 수집되는 로그에 tag를 붙여주는데

양식은 docker.container_id가 되게끔 구성해 줍니다.

 

filter로 lua를 사용하고, 사용될 lua는 docker-metadata.lua입니다.

원하는 작업은 docker container_id => service_name으로 바꾸는 작업이 됩니다.

(물론 lua 언어는 굉장히 생소하고, 들어본 적도 없기 때문에 아래 정보를 참고했고, gpt에게 부탁했습니다;;)

더보기

lua 코드

 

local DOCKER_VAR_DIR  = '/var/lib/docker/containers/'
local CONFIG_SUFFIX   = '/config.v2.json'

local DOCKER_CONTAINER_METADATA = {
    ['docker.container_name']   = '"Name"%s*:%s*"/?(.-)"',
    ['docker.container_image']  = '"Image"%s*:%s*"/?(.-)"',
    ['docker.container_started']= '"StartedAt"%s*:%s*"(.-)"',
    ['service_name']            = '"cohttp://m.docker.swarm.service.name"%s*:%s*"(.-)"'
}

local cache = {}

local function read_meta(container_id)
    local path = DOCKER_VAR_DIR .. container_id .. CONFIG_SUFFIX
    local fh = io.open(path, 'r')
    if not fh then return nil end

    local meta = {}
    for line in fh:lines() do
        for key, regex in pairs(DOCKER_CONTAINER_METADATA) do
            local v = line:match(regex)
            if v then meta[key] = v end
        end
    end
    fh:close()

    if next(meta) then
        cache[container_id] = meta
        return meta
    end
    return nil
end

function enrich_with_docker_metadata(tag, ts, record)
    local container_id = tag:match('docker%.([0-9a-f]+)$')
    if not container_id then return 0 end

    local out = {}
    for k,v in pairs(record) do out[k] = v end
    out['docker.container_id'] = container_id

    local meta = cache[container_id] or read_meta(container_id)
    if meta then
        for k,v in pairs(meta) do
            out[k] = v
        end
    end

    return 2, ts, out
end

output으로는 로그를 보낼 loki 서버를 지정해 줍니다. loki 내 service 별로 구분하기 위해 label_keys를 지정해 적어줍니다.

 

 

실제로 회사 내 loki+grafana 확인 시 동작하는 걸 확인했고, 다 적용하고 나니 요새 고민하던 것에 적용해야겠다고 생각이 들었습니다.

 

실제 적용 예시: 개인 Spring Boot 프로젝트

기존 Spring Boot 프로젝트의 로그도 Loki로 연동하고 싶었던 참에, 이번 구성이 적용 가능했습니다.

기존 logback 기반에서 log4j로 변경할 필요 없이, 외부 로그 파일만 잘 수집해 주면 되는 구조 덕분에

모든 Docker 컨테이너의 로그를 일관되게 수집할 수 있었습니다.

구성 순서 요약

  1. Loki 컨테이너 실행 (MinIO 연동 포함)
    • 주의사항: config.yml에서 S3 연동 설정 시 MinIO endpoint, access key, bucket 설정이 정확히 일치해야 함
    • 실패 시: Loki가 부팅되자마자 종료된다면 volume mount 경로나 credentials 문제일 수 있음
  2. Grafana 컨테이너 실행 및 Loki와 연동
    • 주의사항: Grafana 첫 로그인 시 기본 계정(admin/admin) 변경 필요
    • 실패 시: Data Source에 Loki URL이 잘못되어 있거나 Loki가 아직 부팅되지 않은 상태에서 Grafana가 먼저 붙으려 한 것일 수 있음
  3. Fluent-bit 구성 및 로그 수집 확인
    • 주의사항: fluent-bit.conf의 Tag와 Filter 매칭 규칙이 실제 로그 경로 및 Lua 필터와 맞는지 반드시 확인
    • 실패 시: 아무 로그도 나타나지 않는 경우, Tag_Regex나 Lua 필터의 정규식 오류일 수 있음 연동 포함)
  4. Grafana 컨테이너 실행 및 Loki와 연동
  5. Fluent-bit 구성 및 로그 수집 확인

docker-compose.yml은 다음과 같습니다.

services:
  loki:
    image: grafana/loki:2.9.3
    container_name: loki
    restart: always
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/config.yml
    volumes:
      - ./loki/config.yml:/etc/loki/config.yml
    depends_on:
      - minio
    networks:
      - hoonzi_network

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    ports:
      - "3000:3000"
    depends_on:
      - loki
    networks:
      - hoonzi_network

  fluent-bit:
    image: fluent/fluent-bit:4.0.3
    container_name: fluent-bit
    restart: always
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./fluent-bit:/fluent-bit/etc
    ports:
      - "2020:2020"
    depends_on:
      - loki
    networks:
      - hoonzi_network

 

잘 올라왔는지 확인

 

grafana에서 loki 랑 연결

 

fluent-bit.conf도 마찬가지로 작성한 뒤,

설정하고 docker-compose up -d 실행 시 loki로 인입 및 grafana에 표시되는지 확인하면 됩니다.

 

 

마무리

이 구성을 통해 Docker Swarm 환경에서도 컨테이너별 로그를 서비스 단위로 분류하고,  Loki + Grafana를 활용해 시각화 및 검색까지 가능해졌습니다.  설정은 다소 복잡하지만, 일단 구축하면 운영 편의성이 크게 향상됩니다.

 

실제 운영 중에는 다음과 같은 개선 효과가 있었습니다.

  • 기존 potainer의 경우, 로그 확인 시 여러 노드의 로그를 시간정렬 없이 가져오기 때문에 보기 복잡했다면 서비스에서 간헐적으로 발생하는 오류 로그를 빠르게 탐지하고, 시간대별로 필터링하여 분석하는 데 큰 도움이 되었습니다.
  • 한 노드에서만 발생하는 에러도 쉽게 확인할 수 있었고, 컨테이너 ID가 아닌 서비스 단위로 구분되다 보니 원인 파악이 훨씬 빨라졌습니다.
반응형

댓글