팀 프로젝트 festabook에서 학습한 내용을 정리한 글입니다.
💭 들어가며
지난 글에 이어, 본 글에서는 Alloy 기반 수집 파이프라인을 설계하고 구축한 과정을 정리한다. 또한 Alloy는 서버 노드 단위로 설정되는 구성 요소이기 때문에, 실제 운영 환경에서 함께 사용되는 Node Exporter 설정까지 함께 다룬다.
✅ 전체 아키텍처

Grafana 공식 문서에 따르면 Promtail은 Deprecated 상태이며, 로그 수집 에이전트로는 Grafana Alloy 사용이 권장된다. 이에 따라 본 구성에서는 Alloy를 중심으로 Grafana Observability 스택을 구축했다.

본 글에서는 사진의 왼쪽에 표시된 Spring Server 노드에 설치되는 Alloy와 Node Exporter 설정 방법을 설명한다.
앞선 글에서 설명했듯이, Alloy는 Promtail과 달리 Metric, Log, Trace를 단일 에이전트에서 함께 수집할 수 있으며, 관측 파이프라인을 하나의 설정 체계로 관리할 수 있다는 장점이 있다. 단일 Alloy 인스턴스는 잠재적으로 단일 장애점(SPOF)이 될 수 있으나, 다중 에이전트 구성으로 완화할 수 있다. 본 구성에서는 필요하지 않다고 판단하여 단일 Alloy 인스턴스로 구성했다.

Alloy의 수집 방식은 데이터 유형에 따라 다르게 구성된다. 위 사진은 Grafana 공식 문서에 제시된 Alloy 배포 아키텍처로, 이를 참고해 본 아키텍처를 설계했다. 메트릭의 경우 Spring Boot의 API 엔드포인트를 Alloy가 Polling(Scrape) 방식으로 수집하며, 로그는 Spring Boot가 생성하는 로그 파일을 공유된 폴더에서 읽어오는 방식으로 수집한다. 트레이스의 경우 Spring Boot에서 생성된 Span을 OTLP gRPC 방식으로 Alloy에 직접 Push하도록 구성되어 있다. 또한 Node Exporter를 별도로 실행하여, 호스트 레벨 메트릭 역시 Alloy가 특정 URI를 통해 Polling 방식으로 수집한다.
✅ Spring Boot 설정
▶ Prometheus 관련 설정
Spring Boot 애플리케이션에서 Prometheus 메트릭을 노출하기 위한 최소 설정이다. Alloy는 이 엔드포인트를 Scrape 대상으로 사용해 애플리케이션 메트릭을 수집한다.
🔽 Spring Boot 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
}
🔽 application.yml
management:
endpoints:
web:
exposure:
include: prometheus
- Actuator 엔드포인트는 기본적으로 외부 노출이 제한된다.
- include: prometheus 설정을 통해 /actuator/prometheus 엔드포인트만 선택적으로 노출한다.
🔽 확인
Spring Server에서 /actuator/prometheus 접근 시

Prometheus에서 /target 접근 시

▶ Loki 관련 설정
Loki는 애플리케이션 로그를 파일(또는 stdout) 기반으로 수집하고, 라벨을 기준으로 조회 성능을 확보하는 구조다. 따라서 핵심은 로그를 일관된 형식으로 남기고, Alloy가 읽을 수 있는 위치로 내보내는 것이다. 본 구성에서는 애플리케이션 레이어 전반의 호출을 추적하기 위해 AOP 기반 로그를 남기고, 해당 로그 파일을 Alloy가 읽어 Loki로 전송하도록 설계했다.
🔽 Log AOP
아래 Aspect는 Controller → Service → Repository 등 애플리케이션 레이어에서 호출되는 메서드를 가로채서, 호출 시작 시점에 어떤 메서드가 호출되었는지, 호출 종료 시점에 얼마나 걸렸는지(실행 시간)를 구조화 형태로 기록한다.
@Slf4j
@Aspect
@Component
@Profile("prod | dev")
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class LoggingAspect {
@Around("com.daedan.festabook.global.logging.LoggingPointcuts.applicationLayers()")
public Object allLayersLogging(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = joinPoint.getSignature().getName();
MethodEventLog methodEvent = MethodEventLog.from(className, methodName);
log.info("", kv("event", methodEvent));
stopWatch.start();
try {
return joinPoint.proceed();
} finally {
stopWatch.stop();
long executionTime = stopWatch.getTotalTimeMillis();
MethodLog methodLog = MethodLog.from(className, methodName, executionTime);
log.info("", kv("event", methodLog));
}
}
}
▶ Tempo 관련 설정
Tempo는 애플리케이션에서 생성된 Trace 데이터를 저장하고 조회하기 위한 백엔드다. 본 구성에서는 Spring Boot → (OTLP) → Alloy → Tempo 흐름으로 트레이스를 수집한다. 즉, Spring Boot는 OTLP로 Span을 생성해 Alloy로 전송하고, Alloy가 이를 Tempo로 전달한다.
🔽 Spring Boot 의존성
dependencies {
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
}
🔽 application.yml
management:
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: "http://localhost:4317" # Alloy로 전송
transport: grpc
- endpoint: http://localhost:4317
- Spring Boot가 OTLP 데이터를 전송할 대상이다. 본 구성에서는 Alloy가 동일 노드에서 OTLP 수신 포트를 열고 있기 때문에 localhost를 사용한다.
- transport: grpc
- 전송 방식 설정이다. gRPC/HTTP 두 가지 방식이 있고, 환경에 따라 선택한다.
🔽 Trace AOP
아래 Aspect는 애플리케이션 레이어의 메서드 실행을 감싸서 Span을 직접 생성한다. 이를 통해 어떤 클래스, 메서드에서 시간이 소요되었는지를 트레이스로 추적할 수 있다.
@Aspect
@Component
@Profile("prod | dev")
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TracingAspect {
private final Tracer tracer;
@Value("${env}")
private String env;
@Around("com.daedan.festabook.global.logging.LoggingPointcuts.applicationLayers()")
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = joinPoint.getSignature().getName();
String spanName = className + "::" + methodName;
Span span = tracer.spanBuilder(spanName).startSpan();
span.setAttribute("env", env); // env 설정
try (Scope scope = span.makeCurrent()) {
return joinPoint.proceed();
} finally {
span.end();
}
}
}
- 우리 아키텍처에서는 환경별(dev, prod)로 대시보드를 명확히 분리할 필요가 있었다. Prometheus와 Loki는 Polling 기반 수집 구조를 사용하기 때문에, 수집 단계인 config.alloy에서 env 값을 라벨로 설정해 환경을 구분할 수 있다. 반면 Tempo는 Loki처럼 라벨 기반 인덱싱 구조가 아니며, Alloy 단계에서 로그나 메트릭처럼 자유롭게 라벨을 추가해 조회를 최적화하는 방식이 제한적이다. 이에 따라 본 구성에서는 환경 구분 정보(dev, prod)를 Trace 데이터 자체에 포함시켰다. 구체적으로는 Span을 생성하는 시점에 env 값을 Span의 attribute로 직접 주입함으로써, Grafana에서 Trace 조회 시 해당 attribute를 기준으로 환경별 필터링이 가능하도록 구성했다.
✅ Alloy 설정
파일 구조는 다음과 같다.
alloy/
├── docker-compose.yml
└── config.alloy
- docker-compose.yml
- Alloy 컨테이너 실행 정의 (+ Node Exporter)
- config.alloy
- Alloy 수집 파이프라인 설정
🔽 전체 docker-compose.yml
services:
alloy:
image: grafana/alloy:latest
container_name: alloy
restart: unless-stopped
volumes:
- ./config.alloy:/etc/alloy/config.alloy
- /var/log/festabook:/var/log/app
command:
- "run"
- "--server.http.listen-addr=0.0.0.0:12345"
- "--storage.path=/var/lib/alloy"
- "--stability.level=experimental"
- "/etc/alloy/config.alloy"
ports:
- "12345:12345" # Alloy UI
- "4317:4317" # OTLP gRPC
networks:
- observability
node-exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
networks:
- observability
networks:
observability:
driver: bridge
▶ Alloy
🔽 Alloy Docker (docker-compose.yml)
services:
alloy:
image: grafana/alloy:latest
container_name: alloy
restart: unless-stopped
volumes:
- ./config.alloy:/etc/alloy/config.alloy
- /var/log/festabook:/var/log/app
command:
- "run"
- "--server.http.listen-addr=0.0.0.0:12345"
- "--storage.path=/var/lib/alloy"
- "--stability.level=experimental"
- "/etc/alloy/config.alloy"
ports:
- "12345:12345" # Alloy UI
- "4317:4317" # OTLP gRPC
networks:
- observability
- 4317 포트는 OTLP gRPC 수신 포트, 12345는 Alloy 자체 상태를 확인하기 위한 UI 포트다.
▶ Prometheus 관련 설정
🔽 config.alloy
Prometheus 관련 Alloy 설정에 대한 공식 문서는 다음을 참고한다.
- Grafana 공식 문서
prometheus.scrape "spring" {
targets = [
{
__address__ = "172.17.0.1:{스프링헬스체크포트}",
env = "dev",
app = "festabook-server",
},
]
metrics_path = "/actuator/prometheus"
scrape_interval = "15s"
scrape_timeout = "10s"
forward_to = [prometheus.remote_write.to_prom.receiver]
}
prometheus.remote_write "to_prom" {
endpoint {
// 전송 대상
url = "https://{그라파나프로메테우스전송엔드포인트}/api/v1/write"
}
}
- env, app 라벨을 수집 단계에서 직접 설정해 환경별, 서비스별 대시보드 분리가 가능하다.
🔽 확인
Alloy GUI 12345 포트 접속 시

▶ Node Exporter 관련 설정
🔽 Node Exporter Docker (docker-compose.yml)
services:
node-exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
networks:
- observability
🔽 config.alloy
prometheus.scrape "node" {
targets = [
{
__address__ = "node-exporter:9100",
env = "dev",
app = "festabook-server",
},
]
scrape_interval = "15s"
scrape_timeout = "10s"
forward_to = [prometheus.remote_write.to_prom.receiver]
}
- env, app 라벨을 수집 단계에서 직접 설정해 환경별, 서비스별 대시보드 분리가 가능하다.
🔽 확인
Alloy GUI 12345 포트 접속 시

▶ Loki 관련 설정
🔽 config.alloy
Loki 관련 Alloy 설정에 대한 공식 문서는 다음을 참고한다.
- Grafana 공식 문서
- Grafana 공식 문서
local.file_match "local_files" {
path_targets = [
{
__path__ = "/var/log/app/*.json",
env = "dev",
app = "festabook-server",
},
]
sync_period = "5s"
}
loki.source.file "log_scrape" {
targets = local.file_match.local_files.targets
forward_to = [loki.process.filter_logs.receiver]
tail_from_end = true
}
loki.process "filter_logs" {
stage.drop {
source = ""
expression = ".*Connection closed by authenticating user root"
drop_counter_reason = "noisy"
}
forward_to = [loki.write.grafana_loki.receiver]
}
loki.write "grafana_loki" {
endpoint {
// 전송 대상
url = "https://{그라파나로키전송엔드포인트}/loki/api/v1/push"
}
}
- env, app 라벨을 수집 단계에서 직접 설정해 환경별, 서비스별 대시보드 분리가 가능하다.
🔽 확인

▶ Tempo 관련 설정
🔽 config.alloy
Tempo 관련 Alloy 설정에 대한 공식 문서는 다음을 참고한다.
- Grafana 공식 문서
- Grafana 공식 문서
otelcol.receiver.otlp "default" {
grpc { endpoint = "0.0.0.0:4317" }
output {
traces = [otelcol.processor.memory_limiter.default.input]
}
}
otelcol.processor.memory_limiter "default" {
check_interval = "1s"
limit_percentage = 90
spike_limit_percentage = 95
output {
traces = [otelcol.exporter.otlphttp.default.input]
}
}
otelcol.exporter.otlphttp "default" {
client {
// 전송 대상
endpoint = "https://{그라파나템포전송엔드포인트}/v1/traces"
}
}
🔽 확인
Alloy GUI 12345 포트 접속 시

📍 참고 자료
'DevOps > Log&Monitoring' 카테고리의 다른 글
| [Log&Monitoring] Grafana Observability 구축 (4) - Mimir, Object Storage 백업 (0) | 2025.12.29 |
|---|---|
| [Log&Monitoring] Grafana Observability 구축 (3) - Grafana 대시보드 (0) | 2025.12.29 |
| [Log&Monitoring] Grafana Observability 구축 (1) - Metric, Log, Trace 아키텍처 (0) | 2025.12.28 |
| [Log&Monitoring] 관측 가능성(Observability) (6) | 2025.08.08 |