Blog
prometheusexporterspring-bootfastapimicrometerobservability

Prometheus Exporter 개발하기 — Spring Boot와 FastAPI로 /metrics 만들기

Prometheus가 HTTP로 scrape할 수 있는 exporter를 직접 만드는 방법을 정리합니다. Java는 Spring Boot(Micrometer + Actuator), Python은 FastAPI(prometheus-client)로 메트릭을 노출하고, Prometheus scrape_configs 설정과 동작 확인까지 코드와 함께 다룹니다.

Data Dynamics2026年6月12日10 min read
This post is not yet translated. The original Korean version is shown below.

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.createdorders_created_total(counter 라서 _total 접미사 자동 추가), order.processingorder_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.0

Python — FastAPI (prometheus-client)

FastAPI 에는 Actuator 같은 기본 장치가 없으므로, 공식 라이브러리 prometheus-client 로 메트릭을 만들고 /metrics 라우트를 직접 엽니다.

pip install fastapi uvicorn prometheus-client

1) 메트릭 정의 + /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.ymlscrape_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 라벨로 붙습니다.
  • targetshost:portinstance 라벨이 됩니다.
  • 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

동작 확인

  1. 각 앱이 메트릭을 내는지 직접 확인:
curl -s localhost:8080/actuator/prometheus | head
curl -s localhost:8000/metrics | head
  1. Prometheus UI 의 Status → Targets 에서 두 job 이 모두 UP 인지 확인합니다.

  2. 표현식 브라우저에서 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
  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 규칙으로 연결해, 수집한 메트릭이 실제 운영 신호가 되도록 묶는 것을 권합니다.