[Grafana Basic] Spring Boot 로그를 Loki로 직접 전송하기

y001·2026년 2월 22일

Grafana Basic

목록 보기
7/8
post-thumbnail

1. Overview

이번 글에서는 Spring Boot 애플리케이션에서 발생한 로그를 Logback을 통해 Loki로 직접 전송하고, Grafana에서 조회하는 구조를 정리한다. Spring Boot(Logback) → Loki → Grafana이다. 이전 글에서 다룬 파일 → Filebeat → Elasticsearch 구조가 파일 기반 수집 파이프라인이었다면, 이번 방식은 애플리케이션이 로그를 생성하는 즉시 Loki로 HTTP Push를 수행하는 구조이다. 즉, 로그 파일을 남기고 수집기가 이를 tailing 하는 단계가 존재하지 않는다. Logback Appender가 로그 이벤트를 직렬화하여 Loki의 /loki/api/v1/push 엔드포인트로 전송하고, Loki는 이를 라벨 기반으로 인덱싱하여 저장한다. Grafana는 Loki를 데이터소스로 연결해 LogQL로 조회한다.

Loki의 핵심은 Elasticsearch처럼 전체 로그 본문을 역색인하지 않는다는 점이다. 인덱스는 라벨에 대해서만 생성되고, 실제 로그 내용은 청크 파일로 저장된다. 따라서 설계의 중심은 “어떤 라벨을 붙일 것인가”에 있다. 이번 글에서는 가장 단순한 형태로 app 라벨만 사용한다.

2. Spring Boot

Spring Boot는 로그를 생성하는 주체이며, Logback 설정을 통해 Loki 전송을 담당한다. 이번 구성에서는 Spring Boot는 호스트에서 실행하고, Loki와 Grafana는 docker-compose로 실행한다. 따라서 Loki의 접근 주소는 localhost:3100이다.

2.1 Gradle 의존성

Loki로 로그를 전송하기 위해 loki4j Logback Appender를 추가한다.

implementation 'com.github.loki4j:loki-logback-appender:1.5.1'

loki4j는 Logback 이벤트를 Loki Push API 스펙에 맞는 JSON 구조로 변환하여 HTTP로 전송한다. 별도의 수집 에이전트 없이 애플리케이션에서 직접 Loki로 로그를 push할 수 있게 해준다.

2.2 logback-spring.xml

핵심은 Loki Appender 설정이다. Logback은 로깅 이벤트를 Appender 단위로 처리한다. 여기서는 콘솔 출력과 Loki 전송을 동시에 수행한다.

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

    <include resource="org/springframework/boot/logging/logback/base.xml"/>

    <springProperty scope="context"
                    name="appName"
                    source="spring.application.name"/>

    <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
        <http>
            <url>http://localhost:3100/loki/api/v1/push</url>
        </http>
        <format>
            <label>
                <pattern>app=${appName}</pattern>
            </label>
        </format>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="LOKI"/>
    </root>

</configuration>

이 설정은 모든 로그를 Loki로 전송한다. app 라벨에는 spring.application.name 값이 들어간다. 불필요한 라벨이 많아지면 스트림 수가 급격히 증가하고 성능과 저장 비용에 영향을 준다. 이번 예제에서는 최소 구성을 위해 app 하나만 사용한다.

Spring Boot가 호스트에서 실행되므로 localhost:3100을 사용한다. 만약 Spring Boot를 컨테이너로 실행한다면 이 값은 동작하지 않는다. 그 경우에는 http://loki:3100/...과 같이 Docker 네트워크 기준 주소를 사용해야 한다.

2.3 테스트용 컨트롤러

로그 발생 여부를 확인하기 위해 단순한 엔드포인트를 만든다.

@RestController
public class DemoController {

    @GetMapping("/success")
    public String success() {
        return "ok";
    }

    @GetMapping("/error")
    public String error() {
        throw new ResponseStatusException(
                HttpStatus.INTERNAL_SERVER_ERROR,
                "forced error"
        );
    }
}

애플리케이션 실행 후 /success/error를 호출하면 INFO 및 ERROR 로그가 발생한다. 이 로그가 Loki로 전송되어야 한다.

3. Docker Compose

3-1. docker compose

version: "3.8"

services:
  loki:
    image: grafana/loki:2.9.8
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/config.yaml
    volumes:
      - ./monitoring/loki-config.yaml:/etc/loki/config.yaml:ro
      - loki-data:/loki

  grafana:
    image: grafana/grafana-oss:11.1.5
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - loki
    volumes:
      - grafana-storage:/var/lib/grafana

volumes:
  loki-data:
  grafana-storage:

실행은 docker compose up -d로 수행한다.

3-2. 연결 확인

Loki가 정상적으로 기동되었는지 확인한다.

http://localhost:3100/ready

ready가 반환되면 수신 준비가 완료된 상태이다.

3-3. Loki에 로그가 들어갔는지 확인

curl http://localhost:3100/loki/api/v1/labels

라벨 목록이 반환되면 Loki는 로그를 수신한 상태이다.

curl http://localhost:3100/loki/api/v1/label/app/values

demo가 보이면 Spring Boot에서 전송한 로그가 정상적으로 저장된 것이다. 여기서 값이 보이지 않는다면 URL, 포트, 네트워크 문제를 점검해야 한다.

4. Grafana에서 Loki 데이터소스 등록

4-1. 데이터소스 등록

Grafana에 접속하여 Loki를 데이터소스로 등록한다. URL은 http://loki:3100이다. Grafana 컨테이너 내부에서 Loki는 서비스 이름 loki로 접근해야 한다. Save & Test가 성공하면 연결은 완료된다.

4-2. LogQL 기본 쿼리

앱 로그 전체 조회는 다음과 같다.

{app="demo"}

ERROR 로그만 조회하려면 문자열 필터를 사용한다.

{app="demo"} |= "ERROR"

LogQL은 라벨 조건으로 스트림을 선택하고, 이후 파이프 연산자를 통해 로그 본문을 필터링한다. 라벨은 인덱스를 사용하므로 빠르게 필터링되지만, 문자열 조건은 본문 스캔이 발생한다.

0개의 댓글