Prometheus Exporter 개발하기 — Spring Boot와 FastAPI로 /metrics 만들기
Prometheus가 HTTP로 scrape할 수 있는 exporter를 직접 만드는 방법을 정리합니다. Java는 Spring Boot(Micrometer + Actuator), Python은 FastAPI(prometheus-client)로 메트릭을 노출하고, Prometheus scrape_configs 설정과 동작 확인까지 코드와 함께 다룹니다.
Prometheus 는 pull 모델입니다. 애플리케이션이 메트릭을 어딘가로 밀어 보내는 게 아니라, Prometheus 가 주기적으로 각 타깃의 HTTP 엔드포인트(/metrics)에 GET 요청을 보내 텍스트를 긁어(scrape) 갑니다. 따라서 "exporter 를 만든다"는 것은 결국 메트릭을 exposition format 으로 돌려주는 HTTP 엔드포인트 하나를 여는 일입니다.
이 글은 그 엔드포인트를 Java(Spring Boot)와 Python(FastAPI)으로 각각 만들고, Prometheus 가 긁어가도록 설정하는 방법을 정리합니다. 메트릭 타입·이름 규칙 자체가 헷갈린다면 앞선 글 《Prometheus 메트릭 제대로 이해하기》를 먼저 읽으면 좋습니다.
동작 원리
┌────────────┐ GET /metrics (15초마다) ┌──────────────────┐
│ Prometheus │ ──────────────────────────▶ │ 내 앱(exporter) │
│ (scraper) │ ◀────────────────────────── │ /metrics 엔드포인트│
└────────────┘ 200 OK + 메트릭 텍스트 └──────────────────┘응답 본문은 사람이 읽을 수 있는 평문입니다.
# HELP app_requests_total 처리한 요청 수
# TYPE app_requests_total counter
app_requests_total{method="GET",path="/health",status="200"} 42이 텍스트만 규격대로 돌려주면 어떤 언어·프레임워크든 exporter 가 됩니다. 직접 문자열을 조립할 필요는 없고, 각 언어의 클라이언트 라이브러리가 메트릭 객체와 노출을 대신 처리해 줍니다.
Java — Spring Boot (Micrometer + Actuator)
Spring Boot 에서는 보통 Micrometer 를 씁니다. Micrometer 는 메트릭 파사드(facade)이고, micrometer-registry-prometheus 를 추가하면 Actuator 가 /actuator/prometheus 엔드포인트를 자동으로 열어 줍니다. 즉 엔드포인트를 직접 구현할 필요가 없습니다.
1) 의존성 추가
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>Gradle:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'2) 엔드포인트 노출 설정
application.yml 에서 prometheus 엔드포인트를 외부에 노출합니다.
management:
endpoints:
web:
exposure:
include: health,info,prometheus # /actuator/prometheus 활성화
metrics:
tags:
application: order-service # 모든 메트릭에 application 라벨 부여이것만으로 http://host:8080/actuator/prometheus 가 열리고, JVM·HTTP 서버 요청(http_server_requests_seconds) 등 기본 메트릭이 이미 노출됩니다.
3) 커스텀 메트릭 만들기
MeterRegistry 를 주입받아 Counter·Gauge·Timer 를 등록합니다.
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class OrderMetrics {
private final Counter ordersCreated; // 누적 → counter
private final Timer processingTimer; // 처리 시간 분포
private final AtomicInteger queueSize = new AtomicInteger(0); // 상태값 → gauge
public OrderMetrics(MeterRegistry registry) {
this.ordersCreated = Counter.builder("orders.created") // 점(.) 표기 권장
.description("생성된 주문 수")
.tag("channel", "web") // 라벨
.register(registry);
this.processingTimer = Timer.builder("order.processing")
.description("주문 처리 시간")
.register(registry);
Gauge.builder("orders.queue.size", queueSize, AtomicInteger::get)
.description("처리 대기 중인 주문 수")
.register(registry);
}
public void onOrderCreated() {
ordersCreated.increment(); // counter 는 증가만
}
public void process(Runnable work) {
processingTimer.record(work); // 실행 시간을 자동 기록
}
public void setQueueSize(int n) {
queueSize.set(n); // gauge 는 set/오르내림 가능
}
}Micrometer 이름 규칙 주의 — Micrometer 에서는 메트릭 이름을 점(
.)으로 짓고, Prometheus 레지스트리가 출력 시 자동으로 변환합니다.orders.created→orders_created_total(counter 라서_total접미사 자동 추가),order.processing→order_processing_seconds_count/_sum(Timer 는 초 단위). 그래서 이름에 직접_total을 붙이면 안 됩니다(_total_total이 됩니다).
4) 사용과 출력
서비스 코드에서 호출만 하면 됩니다.
@Service
public class OrderService {
private final OrderMetrics metrics;
public OrderService(OrderMetrics metrics) { this.metrics = metrics; }
public void createOrder(Order order) {
metrics.process(() -> {
// ... 실제 주문 처리 ...
metrics.onOrderCreated();
});
}
}/actuator/prometheus 응답에는 이렇게 찍힙니다.
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web"} 128.0
# TYPE orders_queue_size gauge
orders_queue_size{application="order-service"} 3.0Python — FastAPI (prometheus-client)
FastAPI 에는 Actuator 같은 기본 장치가 없으므로, 공식 라이브러리 prometheus-client 로 메트릭을 만들고 /metrics 라우트를 직접 엽니다.
pip install fastapi uvicorn prometheus-client1) 메트릭 정의 + /metrics 라우트
import time
from fastapi import FastAPI, Request, Response
from prometheus_client import (
Counter, Gauge, Histogram, generate_latest, CONTENT_TYPE_LATEST,
)
app = FastAPI()
REQUESTS = Counter(
"app_requests_total", "처리한 요청 수", ["method", "path", "status"]
)
INPROGRESS = Gauge("app_inprogress_requests", "처리 중인 요청 수")
LATENCY = Histogram(
"app_request_duration_seconds", "요청 처리 시간",
["method", "path"],
buckets=[0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
)
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
INPROGRESS.inc()
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
INPROGRESS.dec()
# 라우트 템플릿을 라벨로 사용 (카디널리티 방지)
route = request.scope.get("route")
path = getattr(route, "path", request.url.path)
LATENCY.labels(request.method, path).observe(elapsed)
REQUESTS.labels(request.method, path, str(response.status_code)).inc()
return response
@app.get("/metrics")
def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
@app.get("/orders/{order_id}")
def get_order(order_id: int):
return {"order_id": order_id}여기서 핵심은 라벨에 원본 URL 경로가 아니라 라우트 템플릿(/orders/{order_id})을 쓰는 것입니다. /orders/123, /orders/124 … 를 그대로 라벨로 넣으면 시계열이 무한히 늘어나는 카디널리티 폭발이 일어납니다.
generate_latest() 가 등록된 모든 메트릭을 exposition format 문자열로 만들어 주고, CONTENT_TYPE_LATEST 가 올바른 Content-Type 헤더를 채웁니다.
2) 더 간단한 방법 두 가지
/metrics 를 미들웨어로 처리하지 않고 ASGI 앱으로 마운트할 수도 있습니다.
from prometheus_client import make_asgi_app
app.mount("/metrics", make_asgi_app())요청 메트릭 자동 계측까지 한 번에 끝내려면 서드파티 라이브러리가 편합니다.
# pip install prometheus-fastapi-instrumentator
from prometheus_fastapi_instrumentator import Instrumentator
Instrumentator().instrument(app).expose(app) # /metrics 자동 노출 + 요청 계측3) 실행
uvicorn main:app --host 0.0.0.0 --port 8000
curl -s localhost:8000/metrics | grep app_멀티 워커 주의 — gunicorn/uvicorn 을 워커 여러 개로 띄우면 각 워커가 별도 카운터를 가지므로 값이 워커마다 달라집니다. 이때는
prometheus-client의 멀티프로세스 모드(PROMETHEUS_MULTIPROC_DIR환경변수 +MultiProcessCollector)를 설정해야 합니다.
Prometheus 설정
이제 Prometheus 가 두 앱을 긁어가도록 prometheus.yml 의 scrape_configs 에 타깃을 등록합니다. 두 앱의 엔드포인트 경로가 다르다는 점(metrics_path)이 포인트입니다.
global:
scrape_interval: 15s # 기본 수집 주기
scrape_configs:
# Spring Boot — 경로가 /actuator/prometheus 이므로 명시해야 함
- job_name: "order-service"
metrics_path: /actuator/prometheus
static_configs:
- targets: ["10.0.0.1:8080"]
# FastAPI — 기본 경로가 /metrics 라서 metrics_path 생략 가능
- job_name: "fastapi-app"
static_configs:
- targets: ["10.0.0.2:8000"]
labels:
team: "platform" # 이 타깃에 공통 라벨을 추가로 부여job_name→ 모든 메트릭에job라벨로 붙습니다.targets의host:port→instance라벨이 됩니다.metrics_path→ 기본값이/metrics이므로 FastAPI 는 생략, Spring Boot 는/actuator/prometheus로 지정.- HTTPS·인증이 필요하면
scheme: https,basic_auth,authorization등을 같은 블록에 추가합니다.
설정을 바꾼 뒤에는 Prometheus 를 리로드합니다.
# --web.enable-lifecycle 옵션으로 떠 있을 때
curl -X POST http://localhost:9090/-/reload동작 확인
- 각 앱이 메트릭을 내는지 직접 확인:
curl -s localhost:8080/actuator/prometheus | head
curl -s localhost:8000/metrics | head-
Prometheus UI 의 Status → Targets 에서 두 job 이 모두
UP인지 확인합니다. -
표현식 브라우저에서
up을 질의하면 타깃별 상태가 보입니다(성공1, 실패0).
up{job="order-service", instance="10.0.0.1:8080"} 1
up{job="fastapi-app", instance="10.0.0.2:8000"} 1- 커스텀 메트릭도 바로 질의해 봅니다.
rate(orders_created_total[5m])
sum by (path) (rate(app_requests_total[5m]))마무리
exporter 의 본질은 단순합니다 — /metrics 에서 exposition format 을 돌려주는 HTTP 엔드포인트 하나. Spring Boot 는 Micrometer + Actuator 가 그 엔드포인트와 기본 메트릭까지 만들어 주므로 의존성·설정 두 줄이면 시작되고, FastAPI 는 prometheus-client 로 라우트 하나를 직접 열면 됩니다. 남은 일은 (1) 의미 있는 커스텀 메트릭을 적절한 타입으로 정의하고, (2) 라벨 카디널리티를 통제하며, (3) Prometheus scrape_configs 에 경로(metrics_path)를 맞춰 등록하는 것뿐입니다.
다음 단계로는 여기서 만든 up·요청율·지연 분위수를 Grafana 대시보드와 Alertmanager 규칙으로 연결해, 수집한 메트릭이 실제 운영 신호가 되도록 묶는 것을 권합니다.