Grafana Alloy로 애플리케이션의 Metrics와 Logs 수집하기

Yechan Kim·2025년 6월 28일
0
post-thumbnail

이번 포스트에서는 Grafana Alloy를 활용해 애플리케이션의 메트릭(metrics)과 로그(logs)를 수집하는 방법에 대해 정리하였다.

Grafana Alloy란?

Grafana Alloy는 Grafana Labs에서 개발한 멀티 소스 원격 측정 수집기(Telemetry Collector)다.
Prometheus, Loki, Tempo, OpenTelemetry 등 다양한 원격 측정 데이터를 하나의 바이너리로 수집할 수 있는 통합형 오픈소스 에이전트다.

Grafana Alloy의 장점

1. 통합형 원격 측정 수집기

Prometheus, Loki, Tempo 등을 개별적으로 운영할 필요 없이 Alloy 하나로 통합 관리가 가능하다.

2. 모듈 기반 아키텍처

필요한 기능만 선택적으로 구성할 수 있으며, 모듈 간 연결도 명확하게 설정 가능하다.
구성 파일이 직관적이며 확장성이 뛰어나다.

3. 운영 간편화

Alloy는 단일 바이너리로 동작하므로 설치 및 배포가 매우 간편하다.
설정 변경 시에도 재시작 없이 적용할 수 있어 운영 중에도 안정적으로 설정을 변경할 수 있다.

4. 다양한 백엔드 연동

Grafana Cloud를 비롯하여 Prometheus, Loki, Tempo, OTLP 등 다양한 백엔드 시스템과 통합이 가능하다.
SaaS 환경에서도 손쉽게 연동할 수 있다.

5. 자체 관측 기능 제공

수집기 자체의 상태, 성능, 오류 등의 정보를 메트릭으로 제공하여, Alloy 자체도 Prometheus를 통해 모니터링할 수 있다.

Docker compose를 통한 설치

이전에 구성했던 CDC 파이프라인에서 사용된 인스턴스와 애플리케이션들을 Grafana Alloy를 이용해 모니터링해보았다.

docker-compose.yaml

services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    container_name: zookeeper
    ports:
      - "9998:9998"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    container_name: kafka
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

  mysql:
    image: mysql:8.0
    container_name: mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: user
      TZ: Asia/Seoul
    volumes:
      - ./mysql/db:/var/lib/mysql
    command: [
      "--server-id=1",
      "--log-bin=mysql-bin",
      "--binlog-format=ROW",
      "--binlog-row-image=FULL"
    ]
      
  mongodb:
    image: mongodb/mongodb-community-server:latest
    container_name: mongodb
    ports:
      - "27017:27017"
    environment:
      MONGODB_INITDB_ROOT_USERNAME: root
      MONGODB_INITDB_ROOT_PASSWORD: password
    restart: unless-stopped

  connect:
    image: debezium/connect:2.5
    container_name: connect
    depends_on:
      - kafka
      - mysql
    ports:
      - "8083:8083"
      - "9999:9999"
    environment:
      BOOTSTRAP_SERVERS: kafka:9092
      GROUP_ID: 1
      CONFIG_STORAGE_TOPIC: connect-configs
      OFFSET_STORAGE_TOPIC: connect-offsets
      STATUS_STORAGE_TOPIC: connect-status
      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      CONNECT_REST_ADVERTISED_HOST_NAME: connect
      CONNECT_PLUGIN_PATH: /kafka/connect,/usr/share/java

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: elasticsearch
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      discovery.type: single-node
      ES_JAVA_OPTS: -Xms16g -Xmx16g
      ingest.geoip.downloader.enabled: false
      xpack.security.enabled: false
      xpack.security.transport.ssl.enabled: false
      xpack.security.http.ssl.enabled: false
      xpack.monitoring.collection.enabled: true
    ulimits:
      memlock:
        soft: -1
        hard: -1

  kibana:
    image: docker.elastic.co/kibana/kibana:8.12.0
    container_name: kibana
    depends_on:
      - elasticsearch
    ports:
      - "5601:5601"
    environment:
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
      XPACK_SECURITY_ENABLED: "false"

  alloy:
    image: grafana/alloy:latest
    container_name: alloy
    privileged: true
    volumes:
      - ./alloy:/etc/alloy
      - /var/run/docker.sock:/var/run/docker.sock 
      - /:/rootfs:ro
      - /proc:/rootfs/proc:ro
      - /sys:/rootfs/sys:ro
      - /var/run/docker.sock:/rootfs/var/run/docker.sock:ro
      - /run/udev:/run/udev:ro
    ports:
      - "12345:12345"
    command: >
      run --server.http.listen-addr=0.0.0.0:12345
          --storage.path=/var/lib/alloy/data
          /etc/alloy/config/config.alloy
    mem_limit: 512m
    cpus: 1.0
    restart: unless-stopped
    
  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - 3000:3000
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning/:/etc/grafana/provisioning/
    restart: always
    depends_on:
      - alloy
      - loki
      
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus:/etc/prometheus
      - prometheus-data:/prometheus
    ports:
      - 9090:9090
    command:
      - '--web.enable-remote-write-receiver'
      - '--web.enable-admin-api'
      - '--storage.tsdb.path=/prometheus'
      - '--config.file=/etc/prometheus/prometheus.yml'
    restart: always

  loki:
    image: grafana/loki:3.4.1
    container_name: loki
    user: "$UID:$GID"
    ports:
      - 3100:3100
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - ./loki:/etc/loki
      - ./loki-data/tsdb:/tmp/loki/tsdb
      - ./loki-data/chunks:/tmp/loki/chunks
      - ./loki-data/rules:/tmp/loki/rules
      - ./loki-data/compactor:/tmp/loki/compactor
    restart: always
  
  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name: kafka-ui
    ports:
      - "8080:8080"
    environment:
      - KAFKA_CLUSTERS_0_NAME=CDC
      - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
      - KAFKA_CLUSTERS_0_ZOOKEEPER=zookeeper:2181
    depends_on:
      - kafka
      - zookeeper

각 서비스는 다음과 같은 기능을 담당한다:

  • Zookeeper: Kafka 브로커의 상태를 관리
  • Kafka: 변경 이벤트를 전달하는 메시지 브로커
  • MySQL: source DB
  • MongoDB: target DB
  • Elasticsearch: 검색용 target DB
  • Debezium Connect: Kafka Source/Sink Connector를 등록하고 실행
  • Alloy: 메트릭 및 로그를 수집하고 전송하는 수집기
  • Grafana: 수집된 데이터 시각화 대시보드
  • Prometheus: 수집된 메트릭 저장소
  • Loki: 수집된 로그 저장소
  • Kafka UI: Kafka 토픽 상태 및 메시지 확인용 UI

Grafana Alloy에 대한 설정은 다음과 같이 설정했다:

  • privileged: 호스트의 시스템 리소스에 대한 접근 권한을 부여 여부
  • mem_limit: 최대 사용 가능한 메모리 용량
  • cpus: 최대 사용 가능한 CPU 코어
  • volumes: 다음과 같은 이유로 alloy 컨테이너에 마운트 했다.
    ./alloy -> /etc/alloy: 구성 파일 위치 (예: config.alloy)
    /var/run/docker.sock -> /var/run/docker.sock: Docker 데몬 접근 (컨테이너 메트릭 수집 등)
    / -> /rootfs:ro: 전체 루트 파일 시스템을 읽기 전용으로 마운트→ node-level 시스템 정보 수집
    /proc -> /rootfs/proc:ro: 프로세스 관련 정보 접근 (/proc/meminfo, /proc/stat)
    /sys -> /rootfs/sys:ro: 커널/하드웨어 정보 수집 (CPU, Memory 등)
    /var/run/docker.sock -> /rootfs/var/run/docker.sock:ro: Docker 메타 정보 수집 (read-only)
    /run/udev -> /run/udev:ro: 장치 정보 (udev) 접근, 하드웨어 메트릭 수집용
  • command: Grafana Alloy를 실행하기 위한 명령어
    run: Alloy 실행 명령
    --server.http.listen-addr=0.0.0.0:12345: Alloy의 HTTP 서버를 모든 인터페이스에서 12345 포트로 개방
    --storage.path=/var/lib/alloy/data: 내부에서 사용하는 임시/지속 데이터 저장 경로
    /etc/alloy/config/config.alloy: 실제 실행할 설정 파일 경로

Grafana Alloy 설정

Alloy 컨테이너는 ./alloy 디렉토리를 /etc/alloy로 마운트하여 구성 파일을 외부에서 관리할 수 있도록 하였다.
설정 파일(config.alloy)이 없으면 Alloy는 정상적으로 실행되지 않으므로 반드시 작성해야 한다.

필자는 docker 환경에서 사용하기 위한 설정을 구성하였다.
공식 문서를 참고하면 다른 환경에 대한 설정에 대한 설명도 있으니 참고하면 좋다.

config.alloy

// livedebugging 설정
livedebugging {
  enabled = true
}

// Logging 설정
logging {
  level  = "info"
  format = "logfmt"
}

// Docker 컨테이너 메타데이터 수집 (모든 컨테이너 대상)
discovery.docker "docker" {
  host = "unix:///var/run/docker.sock"
}

// 컨테이너 이름에서 서비스 이름 추출 (Loki 로그용)
discovery.relabel "docker_relabel" {
  targets = discovery.docker.docker.targets

  rule {
    source_labels = ["__meta_docker_container_name"]
    regex         = "/(.*)"
    target_label  = "service_name"
  }
}

// Docker 로그 수집 및 Loki로 전송
loki.source.docker "docker_logs" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.relabel.docker_relabel.output
  labels     = { platform = "docker" }
  forward_to = [loki.write.DB_Server.receiver]
}

// Loki에 로그 전송
loki.write "server_name" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

// Prometheus로 지표 전송
prometheus.remote_write "server_name" {
  endpoint {
    url = "http://prometheus:9090/api/v1/write"
  }
}

// MySQL metrics 추출
prometheus.exporter.mysql "mysql_exporter" {
  data_source_name = "root:password@(mysql:3306)/"
  enable_collectors = ["info_schema.processlist"]
}

// MongoDB metrics 추출
prometheus.exporter.mongodb "mongodb_exporter" {
  mongodb_uri = "mongodb://root:password@mongodb:27017"
}

// Node metrics 추출
prometheus.exporter.unix "node_exporter" {
  rootfs_path = "/rootfs"
  procfs_path = "/rootfs/proc"
  sysfs_path  = "/rootfs/sys"
}

// Kafka metrics 추출
prometheus.exporter.kafka "kafka_exporter" {
    kafka_uris = ["kafka:9092"]
}

// ElasticSearch metrics 추출
prometheus.exporter.elasticsearch "elasticsearch_exporter" {
    address = "http://elasticsearch:9200"
}

// Zookeeper JMX Exporter 지표 수집
prometheus.scrape "zookeeper_metrics" {
  targets = [{ __address__ = "zookeeper:9998" }]
  forward_to = [prometheus.remote_write.server_name.receiver]
  job_name = "zookeeper"
}

// Debezium JMX Exporter 지표 수집
prometheus.scrape "debezium_metrics" {
  targets = [{ __address__ = "connect:9999" }]
  forward_to = [prometheus.remote_write.server_name.receiver]
  job_name = "connect"
}

// MySQL Exporter 지표 수집
prometheus.scrape "mysql_metrics" {
  targets         = prometheus.exporter.mysql.mysql_exporter.targets
  scrape_interval = "15s"
  forward_to      = [prometheus.remote_write.server_name.receiver]
}

// MongoDB Exporter 지표 수집
prometheus.scrape "mongodb_metrics" {
  targets         = prometheus.exporter.mongodb.mongodb_exporter.targets
  scrape_interval = "15s"
  forward_to      = [prometheus.remote_write.server_name.receiver]
}

// Node Exporter 지표 수집
prometheus.scrape "node_metrics" {
  targets         = prometheus.exporter.unix.node_exporter.targets
  scrape_interval = "15s"
  forward_to      = [prometheus.remote_write.server_name.receiver]
}

// Kafka Exporter 지표 수집
prometheus.scrape "kafka_metrics" {
  targets         = prometheus.exporter.kafka.kafka_exporter.targets
  scrape_interval = "15s"
  forward_to      = [prometheus.remote_write.server_name.receiver]
}

// Elasticsearch Exporter 지표 수집
prometheus.scrape "elasticsearch_metrics" {
  targets         = prometheus.exporter.elasticsearch.elasticsearch_exporter.targets
  scrape_interval = "15s"
  forward_to      = [prometheus.remote_write.server_name.receiver]
}

Prometheus 설정

Prometheus는 Alloy에서 수집한 지표를 저장하고 Grafana를 통해 시각화하는 역할을 한다.
remote_write 설정을 통해 외부에서 전송된 지표를 수신할 수 있도록 하였다.
Prometheus 또한 설정 파일을 외부에서 관리할 수 있다.

proemtheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

remote_write: ## 해당 설정을 추가하여, 외부에서 metrics 수집한 정보를 저장할 수 있도록 한다.
  - url: "http://localhost:9090/api/v1/write"

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

Loki 설정

로그 수집은 Grafana Alloy에서 Loki로 전송되며, Loki는 해당 로그를 저장하고 Grafana를 통해 시각화할 수 있다.
Loki는 외부에서 로그를 Push 받을 수 있는 설정이 기본적으로 가능하므로 간단한 설정으로 운영할 수 있다.
필자는 다음과 같이 설정을 하였다.
Loki 또한 설정 파일을 외부에서 관리할 수 있다.

local-config.yaml

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9095

ingester:
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
    final_sleep: 0s
  chunk_idle_period: 5m
  max_chunk_age: 1h
  chunk_target_size: 1048576

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  max_streams_per_user: 10000

schema_config:
  configs:
    - from: 2022-01-01
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /tmp/loki/index
    cache_location: /tmp/loki/cache
    shared_store: filesystem

  filesystem:
    directory: /tmp/loki/chunks

compactor:
  working_directory: /tmp/loki/compactor
  shared_store: filesystem

ruler:
  storage:
    type: local
    local:
      directory: /tmp/loki/rules
  rule_path: /tmp/loki/rules-temp
  ring:
    kvstore:
      store: inmemory
  enable_api: true
  
volumes:
  - ./loki/data:/tmp/loki

Grafana Alloy 실행

모든 설정이 완료되면 다음 명령어로 컨테이너들을 실행할 수 있다.

sudo docker compose up -d

Grafana Alloy 대시보드

Grafana Alloy는 자체 웹 UI를 제공하여 구성된 모듈들을 시각적으로 확인할 수 있다.

  • 설정된 모듈과 연결 상태 확인 가능
  • 각 모듈을 클릭하면 상세 설정과 상태 확인 가능
  • Graph 탭을 통해 전체 흐름을 시각적으로 확인 가능

수집된 메트릭 & 로그 확인

Grafana Dashboard를 구성하면 실제로 메트릭과 로그가 정상적으로 수집되고 있는지 시각적으로 확인할 수 있다.

마무리

Grafana Alloy를 활용하면 기존 Prometheus, Loki, Tempo 등 개별 도구들을 하나의 수집기로 통합할 수 있어 매우 효율적인 운영이 가능하다.
Docker Compose 환경에서 손쉽게 설치 및 구성이 가능하며, Grafana를 통해 시각적으로 직관적인 대시보드까지 구성할 수 있다.

추가적인 구성 요소 및 기능은 공식 문서를 참고하면 더욱 다양한 활용이 가능하다.

참고 자료

0개의 댓글