개발자라면 한 번쯤 이런 질문을 받아본 적 있을 겁니다. "실시간 매출을 그라파나 대시보드에 띄울 수 있어요?" 처음엔 당황스럽죠. 프로메테우스는 CPU, 메모리, 요청 수 같은 시스템 메트릭을 수집하는 도구인데, 비즈니스 데이터는 데이터베이스에 있거든요. 하지만 커스텀 메트릭을 사용하면 애플리케이션 코드에서 직접 비즈니스 이벤트를 메트릭으로 전송할 수 있습니다.
문제는 여기서 시작됩니다. 개발자들이 좋은 의도로 커스텀 메트릭을 추가하다가 월말에 Datadog이나 New Relic에서 수천 달러 청구서를 받는 경우가 비일비재합니다. Reddit의 r/sre 커뮤니티에서는 "Datadog 비용을 관리하는 가장 빠른 방법은 메트릭의 카디널리티를 줄이는 것"이라는 조언이 가장 많이 추천받았습니다.
카디널리티란 메트릭과 태그 조합의 고유한 개수를 의미합니다. 예를 들어 주문 메트릭에 user_id 태그를 붙이면 사용자 수만큼 고유 시계열이 생깁니다. 사용자 100만 명이면 100만 개. Datadog은 커스텀 메트릭 하나당 월 5달러를 청구하니까 월 500만 달러가 나올 수도 있는 거죠. 실제로 한 스타트업은 잘못된 태그 설계로 월 2만 달러 청구서를 받고 패닉에 빠진 적이 있습니다.
이 글에서는 비즈니스 메트릭을 안전하게 수집하고, 비용 폭탄을 피하며, 경영진이 좋아하는 대시보드를 만드는 실전 노하우를 코드와 함께 제공합니다.
우리 회사의 매출을 그라파나에서 볼 수 없을까
시스템 메트릭과 비즈니스 메트릭은 완전히 다른 세계입니다. Prometheus가 기본으로 수집하는 건 HTTP 요청 수, 응답시간, 에러율 같은 기술 지표죠. 하지만 CEO나 사업부장이 원하는 건 "지금 이 순간 주문이 몇 건 들어왔고, 매출이 얼마인가"입니다.
전통적인 방법은 데이터베이스를 주기적으로 쿼리해서 집계하는 겁니다. 크론 잡으로 1분마다 ORDER 테이블을 SELECT COUNT하고 결과를 어딘가에 저장하는 식이죠. 하지만 이 방식에는 문제가 많습니다. 첫째로 데이터베이스에 부하를 줍니다. 특히 대용량 테이블에서 집계 쿼리는 비용이 비싸죠.
둘째는 실시간성이 떨어집니다. 1분 주기로 집계하면 최대 1분의 지연이 생깁니다. 주문이 급증하는 상황에서 1분은 영원처럼 느껴지죠. 셋째는 히스토리 관리가 어렵습니다. 집계 결과를 어디에 저장할 건가요. 별도 테이블을 만들어야 하고, 그것도 또 관리 포인트가 됩니다.
커스텀 메트릭 방식은 이 모든 문제를 해결합니다. 애플리케이션 코드에서 주문이 발생하는 순간 즉시 메트릭을 전송하는 겁니다. 데이터베이스 쿼리 없이, 실시간으로, 메트릭 수집 시스템이 알아서 히스토리를 관리해주죠. Prometheus, Datadog, New Relic 같은 모니터링 플랫폼은 메트릭을 시계열로 저장하고, 쿼리하고, 시각화하는 기능을 완벽하게 제공합니다.
# 기존 방식: DB 주기적 집계 (권장하지 않음)
# cron_job.py
import psycopg2
from datetime import datetime, timedelta
def collect_order_metrics():
conn = psycopg2.connect("dbname=myapp user=postgres")
cursor = conn.cursor()
# 1분 전부터 지금까지 주문 수 집계
query = """
SELECT COUNT(*) as order_count,
SUM(total_amount) as revenue
FROM orders
WHERE created_at >= NOW() - INTERVAL '1 minute'
"""
cursor.execute(query)
result = cursor.fetchone()
# 결과를 어딘가에 저장... 하지만 어디에?
# 별도 테이블? 파일? Redis?
# 히스토리 관리는?
conn.close()
# 문제점:
# 1. DB 부하 (특히 대용량 테이블)
# 2. 1분 지연 발생
# 3. 집계 결과 저장소 별도 관리 필요
# 4. 시각화 도구와 통합 어려움# 커스텀 메트릭 방식: 이벤트 발생 즉시 전송
from datadog import statsd
from datetime import datetime
class OrderService:
def create_order(self, user_id, items, total_amount):
try:
# 실제 주문 생성 로직
order = self.db.create_order(
user_id=user_id,
items=items,
total_amount=total_amount
)
# 주문 발생 즉시 메트릭 전송
statsd.increment('business.orders.created',
tags=[
f'payment_method:{order.payment_method}',
f'amount_range:{self.get_amount_range(total_amount)}',
'status:success'
])
# 매출액도 함께 기록
statsd.gauge('business.revenue.realtime',
total_amount,
tags=['currency:KRW'])
# 주문 처리 시간 측정
statsd.timing('business.orders.processing_time',
order.processing_duration)
return order
except PaymentFailedException as e:
# 실패도 추적
statsd.increment('business.orders.failed',
tags=[
f'failure_reason:{e.reason}',
'status:failure'
])
raise
def get_amount_range(self, amount):
"""비용 최적화를 위해 금액을 구간으로 그룹화"""
if amount < 10000:
return 'under_10k'
elif amount < 50000:
return '10k_to_50k'
elif amount < 100000:
return '50k_to_100k'
else:
return 'over_100k'
# 주의: 정확한 금액을 태그로 넣으면 안 됨!
# 금액이 모두 다르면 카디널리티 폭발| 비교 항목 | DB 주기적 집계 방식 | 커스텀 메트릭 방식 | 권장 사항 |
|---|---|---|---|
| 실시간성 | 집계 주기만큼 지연 1~5분 | 이벤트 발생 즉시 수초 이내 | 커스텀 메트릭 압승 |
| DB 부하 | 매 집계마다 쿼리 실행 높은 부하 | DB 부하 제로 애플리케이션에서 직접 전송 | 커스텀 메트릭 압승 |
| 히스토리 관리 | 별도 저장소 필요 수동 관리 | 메트릭 플랫폼이 자동 관리 | 커스텀 메트릭 압승 |
| 시각화 난이도 | 별도 대시보드 개발 필요 | Grafana 등으로 즉시 시각화 | 커스텀 메트릭 압승 |
| 초기 구축 난이도 | 쉬움 SQL만 작성 | 중간 메트릭 전송 코드 필요 | DB 집계 승 |
| 운영 비용 | 서버 리소스만 | 메트릭 플랫폼 과금 | DB 집계 승 |
| 세밀한 제어 | 복잡한 집계 쿼리 가능 | 태그 설계에 제약 | DB 집계 승 |
| 확장성 | DB가 병목 될 수 있음 | 메트릭 플랫폼이 확장 처리 | 커스텀 메트릭 승 |
개발자 입장에서 커스텀 메트릭의 가장 큰 장점은 기존 모니터링 인프라와 통합된다는 겁니다. 이미 Prometheus와 Grafana로 시스템 메트릭을 보고 있다면, 같은 대시보드에 비즈니스 메트릭을 추가하는 건 설정 몇 줄이면 끝입니다. 별도 시스템을 구축할 필요가 없죠.
또한 알림 설정도 동일한 방식으로 가능합니다. "주문 건수가 전주 평균 대비 30% 이상 감소하면 Slack 알림" 같은 룰을 Prometheus AlertManager나 Datadog 알림으로 쉽게 만들 수 있어요. 시스템 알림과 비즈니스 알림을 하나의 채널로 통합할 수 있다는 의미입니다.
커스텀 메트릭 생성의 두 가지 방법 로그 기반 vs 코드 인스트루먼테이션
커스텀 메트릭을 만드는 방법은 크게 두 가지입니다. 첫 번째는 로그에서 메트릭을 추출하는 방식이고, 두 번째는 코드에서 직접 메트릭을 전송하는 방식입니다.
로그 기반 방식은 애플리케이션이 이미 로그를 남기고 있다면 그 로그를 파싱해서 메트릭으로 변환하는 겁니다. Datadog, Splunk, CloudWatch 같은 플랫폼은 로그 기반 메트릭 생성 기능을 제공합니다. 예를 들어 "Order created: order_id=12345, amount=50000" 같은 로그 라인이 있다면, 정규식으로 amount를 추출해서 메트릭으로 만들 수 있죠.
장점은 기존 코드를 수정하지 않아도 된다는 겁니다. 로그만 있으면 메트릭 플랫폼 설정으로 끝나니까 개발 부담이 없어요. 또 과거 로그도 소급 적용할 수 있습니다. 단점은 로그 파싱 비용이 발생한다는 거죠. 로그 데이터는 메트릭보다 훨씬 비싸고, 모든 로그를 파싱하면 성능 오버헤드가 생깁니다.
코드 인스트루먼테이션 방식은 애플리케이션 코드에서 직접 메트릭 라이브러리를 호출해서 데이터를 전송하는 겁니다. Python이면 statsd.increment(), Java면 MeterRegistry.counter().increment() 같은 API를 사용하죠. 이벤트가 발생하는 바로 그 순간에 메트릭을 보내니까 정확하고 실시간입니다.
장점은 정밀한 제어가 가능하다는 겁니다. 어떤 데이터를 어떤 태그와 함께 보낼지 코드 레벨에서 완벽하게 컨트롤할 수 있어요. 로그 파싱 오버헤드도 없고요. 단점은 코드 수정이 필요하다는 겁니다. 메트릭을 추가하려면 배포를 해야 하고, 테스트도 해야 하죠.
# 방법 1: 로그 기반 메트릭 생성
import logging
logger = logging.getLogger(__name__)
class OrderService:
def create_order(self, user_id, items, total_amount):
order = self.db.create_order(user_id, items, total_amount)
# 구조화된 로그 출력 (JSON 형식 권장)
logger.info(
"Order created",
extra={
'order_id': order.id,
'user_id': user_id,
'amount': total_amount,
'payment_method': order.payment_method,
'event_type': 'order_created'
}
)
return order
# Datadog에서 로그 기반 메트릭 설정
# 1. Logs > Generate Metrics 메뉴
# 2. 쿼리: event_type:order_created
# 3. Measure: @amount (합계 또는 평균)
# 4. Group by: @payment_method
# 5. 메트릭 이름: business.orders.from_logs
# 장점:
# - 코드 수정 최소화
# - 과거 로그도 소급 적용 가능
# - 배포 없이 메트릭 추가/수정
# 단점:
# - 로그 수집 비용 발생 (보통 메트릭보다 10배 비쌈)
# - 로그 파싱 오버헤드
# - 로그 포맷이 바뀌면 메트릭도 영향받음
# - 실시간성이 약간 떨어질 수 있음# 방법 2: 코드에서 직접 메트릭 전송 (권장)
from datadog import initialize, statsd
from prometheus_client import Counter, Histogram, Gauge
import time
# Datadog 초기화
initialize(
statsd_host='localhost',
statsd_port=8125
)
# Prometheus 메트릭 정의
orders_total = Counter(
'business_orders_total',
'Total number of orders',
['payment_method', 'status']
)
order_amount = Histogram(
'business_order_amount_krw',
'Order amount in KRW',
['payment_method'],
buckets=[10000, 50000, 100000, 500000, 1000000]
)
class OrderService:
def create_order(self, user_id, items, total_amount, payment_method):
start_time = time.time()
try:
order = self.db.create_order(user_id, items, total_amount)
# Datadog 메트릭 전송
statsd.increment('business.orders.created',
tags=[
f'payment_method:{payment_method}',
'status:success'
])
statsd.histogram('business.order.amount',
total_amount,
tags=[f'payment_method:{payment_method}'])
# Prometheus 메트릭 업데이트
orders_total.labels(
payment_method=payment_method,
status='success'
).inc()
order_amount.labels(
payment_method=payment_method
).observe(total_amount)
# 처리 시간 측정
duration = (time.time() - start_time) * 1000
statsd.timing('business.order.processing_time', duration)
return order
except Exception as e:
# 실패 메트릭
statsd.increment('business.orders.failed',
tags=[
f'payment_method:{payment_method}',
f'error_type:{type(e).__name__}',
'status:failure'
])
orders_total.labels(
payment_method=payment_method,
status='failure'
).inc()
raise
# 장점:
# - 정확한 실시간 데이터
# - 로그 수집 비용 없음
# - 메트릭 전송만의 최적화된 프로토콜 사용
# - 세밀한 제어 (샘플링, 조건부 전송 등)
# 단점:
# - 코드 수정 필요
# - 배포 필요
# - 메트릭 라이브러리 의존성 추가| 비교 요소 | 로그 기반 메트릭 | 코드 인스트루먼테이션 | 실전 권장 |
|---|---|---|---|
| 코드 수정 필요성 | 불필요 로그만 있으면 됨 | 필요 메트릭 전송 코드 추가 | 로그 방식이 유리한 경우 레거시 시스템 빠른 프로토타이핑 |
| 정확도 | 로그 파싱 오류 가능성 | 정확함 코드에서 직접 제어 | 코드 방식 압승 |
| 실시간성 | 로그 수집 지연 존재 | 즉시 전송 수 밀리초 | 코드 방식 압승 |
| 비용 | 로그 저장 비용 높음 GB당 $0.5~2 | 메트릭 전송만 저렴 | 코드 방식 압승 비용 10배 차이 |
| 과거 데이터 | 소급 적용 가능 | 불가능 배포 이후부터 | 로그 방식 승 |
| 유지보수 | 로그 포맷 변경 시 영향 | 독립적 안정적 | 코드 방식 승 |
| 성능 영향 | 로그 파싱 오버헤드 | 메트릭 전송만 경량 | 코드 방식 승 |
| 유연성 | 로그에 없는 데이터는 불가 | 모든 데이터 가능 | 코드 방식 승 |
| 학습 곡선 | 낮음 설정만 하면 됨 | 중간 라이브러리 API 학습 | 로그 방식 승 |
실전에서는 하이브리드 접근을 추천합니다. 레거시 시스템이나 수정이 어려운 외부 라이브러리는 로그 기반으로 시작하고, 새로 개발하는 핵심 비즈니스 로직에는 코드 인스트루먼테이션을 적용하는 거죠. 특히 주문, 결제, 회원가입 같은 중요한 이벤트는 반드시 코드에서 직접 메트릭을 전송해야 정확도를 보장할 수 있습니다.
Datadog 문서에 따르면 로그 기반 메트릭은 설정만으로 쉽게 시작할 수 있지만, 프로덕션 환경에서는 코드 인스트루먼테이션이 더 안정적이고 비용 효율적이라고 명시하고 있습니다. 로그는 디버깅용으로, 메트릭은 모니터링용으로 명확히 분리하는 게 베스트 프랙티스죠.
Python과 Java 코드로 주문 건수 메트릭 전송하기 따라하기
실제로 코드를 작성해봅시다. Python과 Java 두 가지 버전으로 준비했습니다.
# Python: Datadog StatsD 방식
# 1. 라이브러리 설치
# pip install datadog
from datadog import initialize, statsd
from datetime import datetime
import logging
# Datadog 초기화
initialize(
statsd_host='localhost', # DogStatsD 에이전트 주소
statsd_port=8125,
statsd_constant_tags=['env:production', 'service:order-api']
)
logger = logging.getLogger(__name__)
class OrderMetrics:
"""주문 관련 메트릭을 전송하는 헬퍼 클래스"""
@staticmethod
def track_order_created(order):
"""주문 생성 메트릭"""
try:
# 카운터: 주문 건수
statsd.increment(
'business.orders.count',
tags=[
f'payment_method:{order.payment_method}',
f'order_type:{order.order_type}',
f'amount_range:{OrderMetrics._get_amount_range(order.amount)}',
'status:created'
]
)
# 게이지: 현재 주문 금액 (실시간 합계용)
statsd.gauge(
'business.orders.amount',
order.amount,
tags=['currency:KRW']
)
# 히스토그램: 주문 금액 분포
statsd.histogram(
'business.orders.amount_distribution',
order.amount,
tags=[f'payment_method:{order.payment_method}']
)
# 타이밍: 주문 처리 시간
if hasattr(order, 'processing_duration_ms'):
statsd.timing(
'business.orders.processing_time',
order.processing_duration_ms
)
logger.info(f"Order metrics sent: order_id={order.id}")
except Exception as e:
logger.error(f"Failed to send order metrics: {e}")
# 메트릭 전송 실패가 비즈니스 로직을 방해하면 안 됨
@staticmethod
def track_order_cancelled(order, reason):
"""주문 취소 메트릭"""
statsd.increment(
'business.orders.cancelled',
tags=[
f'cancellation_reason:{reason}',
f'order_type:{order.order_type}',
'status:cancelled'
]
)
@staticmethod
def track_payment_failed(order, error_code):
"""결제 실패 메트릭"""
statsd.increment(
'business.payment.failed',
tags=[
f'payment_method:{order.payment_method}',
f'error_code:{error_code}',
'status:failed'
]
)
@staticmethod
def _get_amount_range(amount):
"""금액을 구간으로 변환 (카디널리티 최적화)"""
if amount < 10000:
return 'under_10k'
elif amount < 50000:
return '10k_50k'
elif amount < 100000:
return '50k_100k'
elif amount < 500000:
return '100k_500k'
else:
return 'over_500k'
# 실제 사용 예제
class OrderService:
def create_order(self, user_id, items, payment_method):
start_time = datetime.now()
try:
# 비즈니스 로직
order = Order.create(
user_id=user_id,
items=items,
payment_method=payment_method
)
order.processing_duration_ms = \
(datetime.now() - start_time).total_seconds() * 1000
# 메트릭 전송
OrderMetrics.track_order_created(order)
return order
except PaymentException as e:
OrderMetrics.track_payment_failed(order, e.error_code)
raise
def cancel_order(self, order_id, reason):
order = Order.get(order_id)
order.cancel()
OrderMetrics.track_order_cancelled(order, reason)
return order# Python: Prometheus Client 방식
# pip install prometheus-client
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# 메트릭 정의 (전역으로 한 번만 선언)
orders_total = Counter(
'business_orders_total',
'Total number of orders created',
['payment_method', 'order_type', 'status']
)
orders_amount_krw = Histogram(
'business_orders_amount_krw',
'Order amount distribution in KRW',
['payment_method'],
buckets=[5000, 10000, 30000, 50000, 100000, 300000, 500000, 1000000]
)
orders_processing_seconds = Histogram(
'business_orders_processing_seconds',
'Order processing time in seconds',
buckets=[0.1, 0.5, 1.0, 2.0, 5.0, 10.0]
)
orders_current_amount = Gauge(
'business_orders_current_amount_krw',
'Current order amount being processed'
)
payment_failures = Counter(
'business_payment_failures_total',
'Total payment failures',
['payment_method', 'error_code']
)
class OrderService:
def create_order(self, user_id, items, payment_method, order_type='online'):
# 처리 시간 측정 시작
start_time = time.time()
try:
# 비즈니스 로직
order = self._process_order(user_id, items, payment_method)
# 성공 메트릭
orders_total.labels(
payment_method=payment_method,
order_type=order_type,
status='success'
).inc()
orders_amount_krw.labels(
payment_method=payment_method
).observe(order.amount)
orders_current_amount.set(order.amount)
# 처리 시간 기록
duration = time.time() - start_time
orders_processing_seconds.observe(duration)
return order
except PaymentException as e:
# 실패 메트릭
orders_total.labels(
payment_method=payment_method,
order_type=order_type,
status='failure'
).inc()
payment_failures.labels(
payment_method=payment_method,
error_code=e.error_code
).inc()
raise
finally:
# 처리 시간은 성공/실패 관계없이 기록
duration = time.time() - start_time
orders_processing_seconds.observe(duration)
# Prometheus 메트릭 엔드포인트 시작
# http://localhost:8000/metrics 에서 확인 가능
if __name__ == '__main__':
start_http_server(8000)
print("Prometheus metrics available at http://localhost:8000/metrics")// Java: Micrometer + Spring Boot 방식
// build.gradle
// implementation 'io.micrometer:micrometer-registry-prometheus'
// implementation 'io.micrometer:micrometer-core'
package com.example.order.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.DistributionSummary;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final MeterRegistry meterRegistry;
private final OrderRepository orderRepository;
public Order createOrder(Long userId, List<OrderItem> items,
String paymentMethod) {
// 타이머로 처리 시간 자동 측정
return Timer.builder("business.orders.processing.time")
.tag("payment_method", paymentMethod)
.description("Order processing duration")
.register(meterRegistry)
.record(() -> {
try {
Order order = processOrder(userId, items, paymentMethod);
// 주문 성공 카운터
Counter.builder("business.orders.created")
.tag("payment_method", paymentMethod)
.tag("order_type", order.getOrderType())
.tag("status", "success")
.description("Total orders created")
.register(meterRegistry)
.increment();
// 주문 금액 분포
DistributionSummary.builder("business.orders.amount")
.tag("payment_method", paymentMethod)
.tag("currency", "KRW")
.description("Order amount distribution")
.baseUnit("krw")
.register(meterRegistry)
.record(order.getTotalAmount());
log.info("Order created: id={}, amount={}",
order.getId(), order.getTotalAmount());
return order;
} catch (PaymentException e) {
// 결제 실패 카운터
Counter.builder("business.payment.failed")
.tag("payment_method", paymentMethod)
.tag("error_code", e.getErrorCode())
.tag("status", "failure")
.register(meterRegistry)
.increment();
throw e;
}
});
}
public void cancelOrder(Long orderId, String reason) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.cancel();
orderRepository.save(order);
// 취소 메트릭
Counter.builder("business.orders.cancelled")
.tag("cancellation_reason", reason)
.tag("order_type", order.getOrderType())
.register(meterRegistry)
.increment();
}
private Order processOrder(Long userId, List<OrderItem> items,
String paymentMethod) {
// 실제 주문 처리 로직
Order order = Order.builder()
.userId(userId)
.items(items)
.paymentMethod(paymentMethod)
.build();
return orderRepository.save(order);
}
}
// application.yml 설정
// management:
// endpoints:
// web:
// exposure:
// include: prometheus
// metrics:
// tags:
// application: order-service
// environment: production| 언어/프레임워크 | 라이브러리 | 설치 방법 | 메트릭 노출 방식 | 학습 난이도 |
|---|---|---|---|---|
| Python + Datadog | datadog | pip install datadog | DogStatsD 에이전트로 전송 | 쉬움 |
| Python + Prometheus | prometheus-client | pip install prometheus-client | HTTP /metrics 엔드포인트 | 쉬움 |
| Java + Spring Boot | Micrometer | Gradle/Maven 의존성 | /actuator/prometheus | 중간 |
| Node.js + Datadog | hot-shots | npm install hot-shots | DogStatsD 에이전트로 전송 | 쉬움 |
| Go + Prometheus | prometheus/client_golang | go get | HTTP /metrics 엔드포인트 | 중간 |
| .NET + Prometheus | prometheus-net | NuGet 패키지 | HTTP /metrics 엔드포인트 | 중간 |
요금 폭탄 주의 High Cardinality 문제 해결과 태그 설계 팁
카디널리티는 커스텀 메트릭의 가장 큰 함정입니다. Reddit r/devops 커뮤니티에서는 "Datadog 청구서가 갑자기 수천 달러 나왔다"는 글이 정기적으로 올라옵니다. 대부분의 원인이 카디널리티 폭발이죠.
카디널리티란 메트릭 이름과 태그 조합의 고유한 개수입니다. 예를 들어 business.orders.count 메트릭에 payment_method 태그(카드, 계좌이체, 간편결제 3가지)와 status 태그(성공, 실패 2가지)를 붙이면 카디널리티는 1 * 3 * 2 = 6입니다. 이 정도는 전혀 문제없어요.
문제는 고유값이 많은 태그를 붙일 때 발생합니다. user_id 태그를 붙이면 어떻게 될까요. 사용자가 100만 명이면 카디널리티가 600만이 됩니다. Datadog은 커스텀 메트릭 개당 월 $0.05를 청구하니까, 600만 * $0.05 = 월 $300,000입니다. 한 달에 3억 원 넘게 나오는 거죠.
실제 사례를 보겠습니다. 한 스타트업이 주문 메트릭에 order_id 태그를 붙였습니다. 하루 주문이 1만 건이니까 한 달이면 30만 개의 고유 order_id가 생깁니다. 메트릭 종류가 10개면 카디널리티는 300만. 월 청구액이 $150,000, 약 2억 원이 나왔죠. 회사는 패닉 상태가 됐고, 긴급하게 태그를 제거한 후에야 비용이 정상화됐습니다.
# 나쁜 예: 카디널리티 폭발
def track_order_bad(order):
statsd.increment('business.orders.count',
tags=[
f'user_id:{order.user_id}', # 🚨 절대 안 됨!
f'order_id:{order.id}', # 🚨 절대 안 됨!
f'exact_amount:{order.amount}', # 🚨 절대 안 됨!
f'timestamp:{datetime.now().isoformat()}', # 🚨 절대 안 됨!
f'user_email:{order.user_email}', # 🚨 절대 안 됨!
])
# 문제:
# - user_id: 사용자 100만 명 = 100만 배
# - order_id: 매번 고유 = 무한 증가
# - exact_amount: 금액이 모두 다름 = 무한 조합
# - timestamp: 매 초마다 다름 = 무한 증가
# - user_email: 사용자만큼 = 100만 배
# 결과: 카디널리티 수백만 → 월 수천만 원 청구
# 좋은 예: 카디널리티 제어
def track_order_good(order):
statsd.increment('business.orders.count',
tags=[
f'payment_method:{order.payment_method}', # ✅ 3~5가지
f'order_type:{order.order_type}', # ✅ online/offline 2가지
f'amount_range:{get_amount_range(order.amount)}', # ✅ 5개 구간
f'user_tier:{get_user_tier(order.user_id)}', # ✅ VIP/일반 2~3가지
'status:success' # ✅ 고정값
])
# 카디널리티: 5 * 2 * 5 * 3 * 1 = 150
# 월 비용: 150 * $0.05 = $7.50 (1만 원)
def get_amount_range(amount):
"""금액을 5개 구간으로 그룹화"""
if amount < 10000: return 'under_10k'
elif amount < 50000: return '10k_50k'
elif amount < 100000: return '50k_100k'
elif amount < 500000: return '100k_500k'
else: return 'over_500k'
def get_user_tier(user_id):
"""사용자를 몇 개 티어로 그룹화"""
user = User.get(user_id)
if user.total_orders > 100:
return 'vip'
elif user.total_orders > 10:
return 'regular'
else:
return 'new'| 태그 종류 | 카디널리티 | 월 비용 예상 | 사용 가능 여부 | 대안 |
|---|---|---|---|---|
| payment_method | 3~5 | $0.15~0.25 | ✅ 안전 | 그대로 사용 |
| status | 2~3 | $0.10~0.15 | ✅ 안전 | 그대로 사용 |
| order_type | 2~3 | $0.10~0.15 | ✅ 안전 | 그대로 사용 |
| amount_range | 5~10 | $0.25~0.50 | ✅ 안전 | 구간으로 그룹화 |
| user_tier | 3~5 | $0.15~0.25 | ✅ 안전 | 등급으로 그룹화 |
| region | 10~20 | $0.50~1.00 | ✅ 안전 | 그대로 사용 |
| user_id | 100만+ | $50,000+ | 🚨 절대 금지 | user_tier로 대체 |
| order_id | 무한 | $무한 | 🚨 절대 금지 | 로그에만 기록 |
| exact_amount | 무한 | $무한 | 🚨 절대 금지 | amount_range로 대체 |
| timestamp | 무한 | $무한 | 🚨 절대 금지 | 메트릭 자체가 시간 정보 포함 |
| 100만+ | $50,000+ | 🚨 절대 금지 | user_tier로 대체 | |
| IP 주소 | 수십만+ | $10,000+ | 🚨 절대 금지 | region으로 대체 |
카디널리티를 제어하는 핵심 원칙은 "태그 값의 고유 개수를 10개 이하로 유지"하는 겁니다. 만약 더 세밀한 데이터가 필요하다면 메트릭이 아니라 로그나 트레이스에 기록해야 합니다. 예를 들어 특정 주문의 상세 정보는 로그에 남기고, 전체 주문 트렌드만 메트릭으로 집계하는 식이죠.
New Relic 문서에 따르면 높은 카디널리티로 인해 데이터 제한에 도달하면 메트릭 수집이 중단될 수 있습니다. 특정 메트릭의 일일 카디널리티가 기본값인 50,000을 초과하면 해당 메트릭에 대한 새로운 시계열 생성이 거부됩니다. 이는 갑작스러운 모니터링 블라인드 존을 만들 수 있어 매우 위험하죠.
# 카디널리티 모니터링 코드
from datadog import api, initialize
initialize()
def check_metric_cardinality(metric_name):
"""메트릭의 현재 카디널리티 확인"""
try:
# Datadog API로 카디널리티 조회
result = api.Metric.get_cardinality(metric=metric_name)
print(f"Metric: {metric_name}")
print(f"Cardinality: {result['cardinality']}")
print(f"Top tags by cardinality:")
for tag in result['tag_cardinalities']:
print(f" {tag['tag']}: {tag['cardinality']}")
# 경고 임계값
if result['cardinality'] > 1000:
print("⚠️ WARNING: High cardinality detected!")
print(" Consider reducing unique tag values.")
if result['cardinality'] > 10000:
print("🚨 CRITICAL: Very high cardinality!")
print(" Immediate action required to reduce cost.")
return result
except Exception as e:
print(f"Error checking cardinality: {e}")
# 주기적으로 확인
check_metric_cardinality('business.orders.count')비즈니스팀이 좋아하는 대시보드 시각화 노하우
메트릭을 수집했으면 이제 시각화해야 합니다. 하지만 개발자가 만드는 대시보드와 비즈니스팀이 원하는 대시보드는 많이 다릅니다.
개발자는 세밀한 기술 지표를 선호합니다. "지난 1시간 동안의 p95 응답시간", "메모리 사용률 추이", "초당 요청 수" 같은 거죠. 하지만 CEO나 사업부장은 이런 걸 이해하기 어렵습니다. 그들이 원하는 건 "지금 매출이 얼마인가", "전주 대비 몇 퍼센트 증가했는가", "어떤 결제 수단이 가장 많이 쓰이는가" 같은 비즈니스 인사이트입니다.
좋은 비즈니스 대시보드는 세 가지 레이어로 구성됩니다. 첫 번째는 KPI 요약 레이어입니다. 가장 위에 큰 숫자로 핵심 지표를 보여줍니다. "오늘 주문 1,234건", "매출 5,670만 원", "전주 대비 +15%". 경영진은 이것만 보고도 상황을 파악할 수 있어야 하죠.
두 번째는 트렌드 레이어입니다. 시간에 따른 변화를 그래프로 보여줍니다. 선 그래프로 주문 추이, 막대 그래프로 결제 수단별 비율, 히트맵으로 시간대별 집중도 같은 거죠. 여기서 패턴을 발견할 수 있습니다.
세 번째는 드릴다운 레이어입니다. 문제가 발견되면 클릭해서 상세 데이터를 볼 수 있어야 합니다. "전주 대비 -20%"를 클릭하면 어떤 결제 수단에서 떨어졌는지, 어느 시간대에 문제가 있었는지 상세 분석 화면으로 이동하는 식이죠.
// Grafana 대시보드 JSON 예시
{
"dashboard": {
"title": "비즈니스 실시간 모니터링",
"panels": [
{
"id": 1,
"title": "오늘 주문 건수",
"type": "stat",
"targets": [{
"expr": "sum(increase(business_orders_total{status=\"success\"}[24h]))",
"legendFormat": "주문"
}],
"fieldConfig": {
"defaults": {
"unit": "none",
"thresholds": {
"mode": "absolute",
"steps": [
{"value": 0, "color": "red"},
{"value": 100, "color": "yellow"},
{"value": 500, "color": "green"}
]
}
}
},
"options": {
"textMode": "value_and_name",
"graphMode": "area",
"colorMode": "background"
}
},
{
"id": 2,
"title": "시간별 주문 추이",
"type": "graph",
"targets": [{
"expr": "rate(business_orders_total{status=\"success\"}[5m]) * 300",
"legendFormat": "{{payment_method}}"
}],
"yaxes": [{
"label": "주문 수 (5분당)",
"format": "short"
}]
},
{
"id": 3,
"title": "결제 수단별 비율",
"type": "piechart",
"targets": [{
"expr": "sum by(payment_method) (increase(business_orders_total{status=\"success\"}[24h]))",
"legendFormat": "{{payment_method}}"
}]
},
{
"id": 4,
"title": "매출 실시간",
"type": "timeseries",
"targets": [{
"expr": "sum(business_orders_amount_krw_sum) / 1000000",
"legendFormat": "매출 (백만원)"
}],
"fieldConfig": {
"defaults": {
"unit": "currencyKRW"
}
}
},
{
"id": 5,
"title": "전주 대비 증감률",
"type": "stat",
"targets": [{
"expr": "(sum(increase(business_orders_total{status=\"success\"}[24h])) - sum(increase(business_orders_total{status=\"success\"}[24h] offset 7d))) / sum(increase(business_orders_total{status=\"success\"}[24h] offset 7d)) * 100",
"legendFormat": "증감률"
}],
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 1,
"thresholds": {
"steps": [
{"value": -20, "color": "red"},
{"value": -5, "color": "yellow"},
{"value": 0, "color": "green"},
{"value": 10, "color": "blue"}
]
}
}
}
},
{
"id": 6,
"title": "평균 주문 금액",
"type": "gauge",
"targets": [{
"expr": "avg(business_orders_amount_krw)",
"legendFormat": "평균"
}],
"fieldConfig": {
"defaults": {
"unit": "currencyKRW",
"min": 0,
"max": 100000,
"thresholds": {
"steps": [
{"value": 0, "color": "red"},
{"value": 30000, "color": "yellow"},
{"value": 50000, "color": "green"}
]
}
}
}
},
{
"id": 7,
"title": "결제 실패율",
"type": "stat",
"targets": [{
"expr": "sum(business_payment_failures_total) / (sum(business_orders_total) + sum(business_payment_failures_total)) * 100",
"legendFormat": "실패율"
}],
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 2,
"thresholds": {
"steps": [
{"value": 0, "color": "green"},
{"value": 2, "color": "yellow"},
{"value": 5, "color": "red"}
]
}
}
}
},
{
"id": 8,
"title": "주문 처리 시간 분포",
"type": "heatmap",
"targets": [{
"expr": "rate(business_orders_processing_seconds_bucket[5m])",
"format": "heatmap"
}]
}
],
"refresh": "30s",
"time": {
"from": "now-24h",
"to": "now"
},
"timezone": "Asia/Seoul"
}
}| 대시보드 요소 | 개발자 선호 | 비즈니스팀 선호 | 권장 방식 |
|---|---|---|---|
| 핵심 지표 표시 | 작은 숫자 여러 개 | 큰 숫자 3~5개 | 비즈니스팀 방식 채택 KPI 요약 상단 배치 |
| 시간 범위 | 1시간 15분 단위 | 24시간 또는 7일 1시간 단위 | 기본은 24시간 드릴다운으로 세밀 조회 |
| 그래프 종류 | 선 그래프 히스토그램 | 막대 원 그래프 | 비율은 원 그래프 추이는 선 그래프 혼합 |
| 색상 | 기본 색상 | 빨강=나쁨 녹색=좋음 명확한 의미 | 비즈니스팀 방식 임계값 색상 코딩 |
| 단위 표기 | 원본 값 | K M B 약어 | 백만 원은 "5.6M" 억 원은 "12B" |
| 레이아웃 | 1열 세로 나열 | 그리드 2~3열 | 중요도 순 좌→우 위→아래 |
| 새로고침 | 수동 또는 5분 | 30초 자동 | 30초 권장 실시간감 제공 |
| 알림 표시 | 별도 화면 | 대시보드에 통합 | Alert 패널 추가 이상 상황 즉시 확인 |
실전 팁을 몇 가지 더 드리겠습니다. 첫째, 숫자에는 반드시 컨텍스트를 붙이세요. "주문 1,234건"보다는 "주문 1,234건 (전주 대비 +15%)"가 훨씬 유용합니다. 숫자만 보면 좋은 건지 나쁜 건지 모르거든요.
둘째, 임계값을 색상으로 표현하세요. 주문 건수가 목표치의 80% 미만이면 빨간색, 80~100%면 노란색, 100% 이상이면 녹색. 한눈에 상태를 파악할 수 있습니다.
셋째, 드릴다운 링크를 제공하세요. "결제 실패율 5%"를 클릭하면 어떤 결제 수단에서 실패가 많은지, 에러 코드별 분포는 어떤지 상세 화면으로 이동할 수 있게 만드는 거죠. Grafana는 변수와 링크 기능으로 이를 쉽게 구현할 수 있습니다.
넷째, 모바일 최적화를 고려하세요. CEO는 출장 중에 휴대폰으로 대시보드를 봅니다. 작은 화면에서도 핵심 KPI가 명확히 보여야 하죠. Grafana의 반응형 레이아웃과 모바일 뷰 설정을 활용하세요.
# Grafana 대시보드 프로그래밍 방식 생성 (Python)
# pip install grafana-api
from grafana_api.grafana_face import GrafanaFace
grafana = GrafanaFace(
auth=('admin', 'admin'),
host='localhost',
port=3000
)
# 대시보드 정의
dashboard = {
"dashboard": {
"title": "실시간 비즈니스 모니터링",
"tags": ["business", "orders"],
"timezone": "browser",
"panels": [
{
"id": 1,
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 0},
"type": "stat",
"title": "오늘 주문 건수",
"targets": [{
"expr": "sum(increase(business_orders_total[24h]))",
"refId": "A"
}],
"options": {
"textMode": "value_and_name",
"colorMode": "background"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{"value": 0, "color": "red"},
{"value": 500, "color": "yellow"},
{"value": 1000, "color": "green"}
]
}
}
}
},
# 더 많은 패널 추가...
],
"refresh": "30s"
}
}
# 대시보드 생성
grafana.dashboard.update_dashboard(dashboard)
print("Dashboard created successfully!")Prometheus와 오픈소스 스택으로 비용 제로 구축하기
Datadog이나 New Relic은 강력하지만 비용이 만만치 않습니다. 월 수백만 원씩 나올 수 있죠. 예산이 제한적인 스타트업이나 중소기업은 오픈소스 스택을 고려해볼 만합니다.
Prometheus + Grafana 조합은 완전히 무료이고, 기능도 상용 솔루션 못지않습니다. Prometheus는 메트릭 수집과 저장을, Grafana는 시각화를 담당하죠. AlertManager로 알림도 설정할 수 있고요.
# docker-compose.yml: Prometheus + Grafana 전체 스택
version: '3.8'
services:
# Prometheus 서버
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
networks:
- monitoring
# Grafana 대시보드
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana-data:/var/lib/grafana
- ./grafana-dashboards:/etc/grafana/provisioning/dashboards
- ./grafana-datasources:/etc/grafana/provisioning/datasources
networks:
- monitoring
depends_on:
- prometheus
# AlertManager (알림)
alertmanager:
image: prom/alertmanager:latest
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
networks:
- monitoring
# 애플리케이션 (메트릭 노출)
app:
build: .
ports:
- "8000:8000"
environment:
- PROMETHEUS_MULTIPROC_DIR=/tmp
networks:
- monitoring
volumes:
prometheus-data:
grafana-data:
networks:
monitoring:# prometheus.yml: Prometheus 설정
global:
scrape_interval: 15s
evaluation_interval: 15s
# 알림 규칙
rule_files:
- "alerts.yml"
# AlertManager 설정
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# 메트릭 수집 대상
scrape_configs:
# Prometheus 자체 모니터링
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# 애플리케이션 메트릭
- job_name: 'order-service'
static_configs:
- targets: ['app:8000']
metrics_path: '/metrics'
scrape_interval: 10s
# alerts.yml: 알림 규칙
groups:
- name: business_alerts
interval: 1m
rules:
# 주문 급락 알림
- alert: OrderCountDrop
expr: |
(
sum(rate(business_orders_total{status="success"}[5m]))
/
sum(rate(business_orders_total{status="success"}[5m] offset 1w))
) < 0.7
for: 5m
labels:
severity: critical
annotations:
summary: "주문 건수 30% 이상 급락"
description: "지난 5분간 주문이 전주 대비 30% 이상 감소했습니다. 현재: {{ $value | humanizePercentage }}"
# 결제 실패율 증가 알림
- alert: HighPaymentFailureRate
expr: |
(
sum(rate(business_payment_failures_total[5m]))
/
(sum(rate(business_orders_total[5m])) + sum(rate(business_payment_failures_total[5m])))
) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "결제 실패율 5% 초과"
description: "현재 결제 실패율: {{ $value | humanizePercentage }}"# alertmanager.yml: 알림 채널 설정
global:
resolve_timeout: 5m
# 알림 전송 경로
route:
group_by: ['alertname', 'severity']
group_wait: 10s
group_interval: 10s
repeat_interval: 12h
receiver: 'slack-critical'
routes:
- match:
severity: critical
receiver: 'slack-critical'
continue: true
- match:
severity: warning
receiver: 'slack-warning'
# 알림 수신자
receivers:
- name: 'slack-critical'
slack_configs:
- api_url: 'YOUR_SLACK_WEBHOOK_URL'
channel: '#alerts-critical'
title: '🚨 긴급 알림'
text: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ .Annotations.description }}\n{{ end }}'
- name: 'slack-warning'
slack_configs:
- api_url: 'YOUR_SLACK_WEBHOOK_URL'
channel: '#alerts-warning'
title: '⚠️ 경고 알림'
text: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}'
# 알림 억제 (동일 알림 반복 방지)
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname']| 비교 항목 | Datadog/New Relic | Prometheus + Grafana | 실전 권장 |
|---|---|---|---|
| 초기 비용 | 무료 트라이얼 | 완전 무료 | 소규모는 오픈소스 대규모는 상용 |
| 월 운영 비용 | $300~3,000+ | 서버 비용만 $50~200 | 오픈소스가 10배 저렴 |
| 설치 난이도 | 쉬움 에이전트만 설치 | 중간 직접 구축 필요 | 상용이 쉬움 |
| 학습 곡선 | 낮음 UI 직관적 | 중간 PromQL 학습 필요 | 상용이 쉬움 |
| 커뮤니티 | 공식 지원 강력 | 오픈소스 커뮤니티 방대 | 둘 다 우수 |
| 확장성 | 플랫폼이 처리 | 직접 구축 필요 | 상용이 편함 |
| 데이터 소유권 | 외부 SaaS | 자체 서버 완전 통제 | 규제 산업은 오픈소스 |
| 고급 기능 | AI 이상 탐지 등 | 기본 기능만 | 상용이 풍부 |
| 장애 대응 | 24/7 지원 | 자체 해결 | 상용이 안전 |
오픈소스 스택의 가장 큰 장점은 비용입니다. Datadog에서 월 $2,000 나오던 회사가 Prometheus로 전환해서 서버 비용 $100만 내게 됐다는 사례가 많습니다. 연간 $22,800 절감이죠.
하지만 단점도 분명합니다. 직접 구축하고 운영해야 하니까 DevOps 인력이 필요합니다. Prometheus 서버가 죽으면 직접 고쳐야 하고요. 상용 솔루션의 AI 기반 이상 탐지, 자동 근본 원인 분석 같은 고급 기능도 없습니다.
실전 권장은 이렇습니다. 스타트업 초기나 예산이 제한적이면 Prometheus로 시작하세요. 제품이 성장하고 모니터링이 중요해지면 Datadog 같은 상용 솔루션으로 전환하는 겁니다. 또는 하이브리드 방식도 가능합니다. 시스템 메트릭은 Prometheus로, 비즈니스 메트릭만 Datadog으로 보내서 비용을 최적화하는 거죠.
알림 설정과 On-Call 체계 구축하기
메트릭을 수집하고 대시보드를 만들었으면 이제 알림이 필요합니다. 주문이 급락하거나 결제 실패율이 치솟으면 즉시 알아야 하죠.
하지만 알림 설계는 신중해야 합니다. 너무 민감하게 설정하면 하루에 수십 번 울려서 결국 무시하게 되고, 너무 둔하게 설정하면 심각한 문제를 놓칠 수 있습니다.
# Python: Datadog 알림 프로그래밍 방식 생성
from datadog import initialize, api
initialize(
api_key='YOUR_API_KEY',
app_key='YOUR_APP_KEY'
)
# 주문 급락 알림
order_drop_monitor = api.Monitor.create(
type="metric alert",
query="avg(last_5m):rate(sum:business.orders.count{status:success}) / rate(sum:business.orders.count{status:success}.rollup(sum, 3600) offset_by_1w) < 0.7",
name="주문 건수 30% 이상 급락",
message="""
🚨 긴급: 주문이 급락했습니다!
현재 주문 건수가 전주 같은 시간 대비 30% 이상 감소했습니다.
**현재 상황:**
- 현재 주문 비율: {{value}}%
- 감소폭: {{comparator}} 30%
**확인 사항:**
1. 결제 시스템 상태 확인
2. 웹사이트 접속 가능 여부
3. 마케팅 캠페인 변경 사항
@slack-channel-critical
@pagerduty
""",
tags=["team:business", "priority:critical"],
options={
"notify_no_data": True,
"no_data_timeframe": 10,
"notify_audit": True,
"require_full_window": False,
"new_host_delay": 300,
"include_tags": True,
"escalation_message": "⚠️ 알림 미확인 - 에스컬레이션",
"timeout_h": 0,
"renotify_interval": 0,
"thresholds": {
"critical": 0.7,
"warning": 0.8
}
}
)
# 결제 실패율 증가 알림
payment_failure_monitor = api.Monitor.create(
type="metric alert",
query="avg(last_5m):(sum:business.payment.failed{*}.as_count() / (sum:business.orders.count{*}.as_count() + sum:business.payment.failed{*}.as_count())) * 100 > 5",
name="결제 실패율 5% 초과",
message="""
⚠️ 경고: 결제 실패율 상승
현재 결제 실패율: {{value}}%
**결제 수단별 현황 확인:**
[대시보드 바로가기](https://app.datadoghq.com/dashboard/abc-123)
@slack-channel-warning
""",
tags=["team:payments", "priority:high"],
options={
"thresholds": {
"critical": 5,
"warning": 3
},
"notify_no_data": False,
"renotify_interval": 60
}
)
print(f"Monitor created: {order_drop_monitor['id']}")| 알림 설계 요소 | 나쁜 예 | 좋은 예 | 설명 |
|---|---|---|---|
| 임계값 설정 | 절대값 주문 < 100건 | 상대값 전주 대비 -30% | 절대값은 시간대별 차이 고려 못함 |
| 지속 시간 | 즉시 알림 | 5분 지속 시 알림 | 일시적 튀김 무시 |
| 알림 메시지 | "주문 감소" | "주문 30% 감소 현재 70건 대시보드 링크" | 컨텍스트와 액션 아이템 필수 |
| 알림 채널 | 모든 알림 같은 채널 | Critical은 전화 Warning은 Slack | 중요도별 분리 |
| 재알림 간격 | 매번 알림 | 1시간마다 재알림 | 알림 피로 방지 |
| 복구 알림 | 없음 | "주문 정상화됨" 알림 | 복구 확인 중요 |
| 담당자 지정 | 전체 채널 태그 | @oncall-team 지정 | 명확한 책임자 |
| 데이터 없음 처리 | 무시 | 10분 데이터 없으면 알림 | 수집 중단 감지 |
# PagerDuty와 통합한 On-Call 로테이션
from pdpyras import APISession
from datetime import datetime, timedelta
# PagerDuty API 초기화
session = APISession('YOUR_API_TOKEN')
# On-Call 스케줄 생성
schedule = session.post(
'/schedules',
json={
"schedule": {
"type": "schedule",
"name": "비즈니스 메트릭 On-Call",
"time_zone": "Asia/Seoul",
"description": "주문/결제 관련 긴급 알림 대응",
"schedule_layers": [
{
"name": "평일 주간 (09:00-18:00)",
"start": datetime.now().isoformat(),
"rotation_virtual_start": datetime.now().isoformat(),
"rotation_turn_length_seconds": 86400, # 1일
"users": [
{"user": {"id": "USER_ID_1", "type": "user_reference"}},
{"user": {"id": "USER_ID_2", "type": "user_reference"}}
],
"restrictions": [
{
"type": "weekly_restriction",
"start_time_of_day": "09:00:00",
"duration_seconds": 32400, # 9시간
"start_day_of_week": 1 # 월요일
}
]
},
{
"name": "야간 및 주말",
"start": datetime.now().isoformat(),
"rotation_virtual_start": datetime.now().isoformat(),
"rotation_turn_length_seconds": 604800, # 1주
"users": [
{"user": {"id": "USER_ID_3", "type": "user_reference"}},
{"user": {"id": "USER_ID_4", "type": "user_reference"}}
]
}
]
}
}
)
# 에스컬레이션 정책
escalation_policy = session.post(
'/escalation_policies',
json={
"escalation_policy": {
"name": "비즈니스 알림 에스컬레이션",
"escalation_rules": [
{
"escalation_delay_in_minutes": 5,
"targets": [
{
"type": "schedule_reference",
"id": schedule['schedule']['id']
}
]
},
{
"escalation_delay_in_minutes": 15,
"targets": [
{
"type": "user_reference",
"id": "MANAGER_USER_ID" # 15분 미응답 시 매니저에게
}
]
},
{
"escalation_delay_in_minutes": 30,
"targets": [
{
"type": "user_reference",
"id": "CTO_USER_ID" # 30분 미응답 시 CTO에게
}
]
}
]
}
}
)
print(f"On-Call schedule created: {schedule['schedule']['id']}")
print(f"Escalation policy created: {escalation_policy['escalation_policy']['id']}")| On-Call 체계 요소 | 소규모 팀 (<10명) | 중규모 팀 (10~50명) | 대규모 팀 (50명+) |
|---|---|---|---|
| 로테이션 주기 | 1주일 교대 | 평일/야간 분리 3~4명 | 24시간 3교대 전문팀 |
| 에스컬레이션 단계 | 1차: 당직자 2차: 팀장 | 1차: 당직 2차: 시니어 3차: 매니저 | 1차: L1 2차: L2 3차: L3 매니저 |
| 응답 시간 SLA | 15분 이내 | 5분 이내 | 3분 이내 (Critical) |
| 보상 체계 | 대체 휴가 | 당직 수당 + 대체 휴가 | 당직 수당 + 인센티브 |
| 도구 | Slack 전화 | PagerDuty Opsgenie | PagerDuty + 자동화 |
| 문서화 | 간단한 Runbook | 상세 Runbook + FAQ | 자동화된 Runbook Incident 리포트 |
비용 최적화 실전 체크리스트와 모니터링
커스텀 메트릭 비용을 효과적으로 관리하려면 정기적인 모니터링이 필수입니다.
# 커스텀 메트릭 비용 모니터링 스크립트
from datadog import initialize, api
import pandas as pd
from datetime import datetime, timedelta
initialize(api_key='YOUR_API_KEY', app_key='YOUR_APP_KEY')
def analyze_metric_costs():
"""메트릭별 비용 분석"""
# 지난 30일 메트릭 사용량 조회
end_date = datetime.now()
start_date = end_date - timedelta(days=30)
metrics_list = api.Metric.list(from_date=int(start_date.timestamp()))
cost_analysis = []
for metric in metrics_list['metrics']:
metric_name = metric
# 카디널리티 조회
try:
cardinality = api.Metric.get_cardinality(metric=metric_name)
cardinality_count = cardinality.get('cardinality', 0)
estimated_cost = cardinality_count * 0.05 # $0.05 per metric
cost_analysis.append({
'metric_name': metric_name,
'cardinality': cardinality_count,
'estimated_monthly_cost_usd': estimated_cost,
'estimated_monthly_cost_krw': estimated_cost * 1300
})
except Exception as e:
print(f"Error checking {metric_name}: {e}")
# DataFrame으로 변환 및 정렬
df = pd.DataFrame(cost_analysis)
df = df.sort_values('estimated_monthly_cost_usd', ascending=False)
# 보고서 출력
print("\n" + "="*80)
print("커스텀 메트릭 비용 분석 보고서")
print("="*80)
print(f"\n총 메트릭 수: {len(df)}")
print(f"총 카디널리티: {df['cardinality'].sum():,}")
print(f"예상 월 비용: ${df['estimated_monthly_cost_usd'].sum():,.2f}")
print(f"예상 월 비용 (원): ₩{df['estimated_monthly_cost_krw'].sum():,.0f}")
print("\n\n상위 10개 고비용 메트릭:")
print(df.head(10).to_string(index=False))
# 경고: 카디널리티가 높은 메트릭
high_cardinality = df[df['cardinality'] > 1000]
if not high_cardinality.empty:
print("\n\n⚠️ 경고: 카디널리티가 높은 메트릭 (1,000 초과)")
print(high_cardinality[['metric_name', 'cardinality', 'estimated_monthly_cost_krw']].to_string(index=False))
print("\n권장 조치:")
print("1. 태그 개수 줄이기")
print("2. 고유값이 많은 태그 제거 (user_id, order_id 등)")
print("3. 연속형 값은 구간으로 그룹화")
# CSV로 저장
output_file = f"metric_cost_analysis_{datetime.now().strftime('%Y%m%d')}.csv"
df.to_csv(output_file, index=False, encoding='utf-8-sig')
print(f"\n상세 보고서 저장: {output_file}")
return df
def find_unused_metrics():
"""사용하지 않는 메트릭 찾기"""
print("\n" + "="*80)
print("미사용 메트릭 분석")
print("="*80)
# 지난 7일간 쿼리되지 않은 메트릭 찾기
# (실제로는 Datadog API로 usage 데이터 조회)
unused_metrics = []
# 예시: 실제로는 API 호출로 구현
print("\n지난 7일간 쿼리되지 않은 메트릭:")
print("(대시보드, 알림, API에서 사용하지 않는 메트릭)")
# 권장 조치
print("\n권장 조치:")
print("1. 정말 필요한 메트릭인지 팀과 논의")
print("2. 불필요하면 메트릭 전송 코드 제거")
print("3. 30일 후 재확인하여 삭제")
def optimize_tags():
"""태그 최적화 제안"""
print("\n" + "="*80)
print("태그 최적화 제안")
print("="*80)
problematic_tags = [
"user_id", "order_id", "session_id",
"transaction_id", "request_id", "timestamp",
"email", "ip_address", "user_agent"
]
print("\n❌ 절대 사용하지 말아야 할 태그:")
for tag in problematic_tags:
print(f" - {tag}: 고유값이 너무 많음 → 카디널리티 폭발")
print("\n✅ 권장 태그 설계:")
print(" - payment_method: 3~5가지 (카드/계좌/간편결제)")
print(" - order_type: 2~3가지 (온라인/오프라인/모바일)")
print(" - amount_range: 5개 구간 (under_10k, 10k_50k...)")
print(" - user_tier: 3가지 (신규/일반/VIP)")
print(" - status: 2~3가지 (성공/실패/대기)")
print("\n💡 최적화 팁:")
print(" 1. 각 태그의 고유값을 10개 이하로 제한")
print(" 2. 연속형 값은 반드시 구간으로 변환")
print(" 3. 세밀한 분석은 로그에서 수행")
print(" 4. 태그 조합 개수 = 태그1 * 태그2 * 태그3...")
# 실행
if __name__ == '__main__':
df = analyze_metric_costs()
find_unused_metrics()
optimize_tags()| 비용 최적화 전략 | 구현 방법 | 예상 절감 효과 | 난이도 |
|---|---|---|---|
| 고카디널리티 태그 제거 | user_id order_id 같은 태그 삭제 | 50~90% 비용 절감 | 쉬움 |
| 태그 값 그룹화 | 연속형 값을 5~10개 구간으로 변환 | 30~50% 비용 절감 | 쉬움 |
| 미사용 메트릭 정리 | 7일간 쿼리 안 된 메트릭 삭제 | 10~20% 비용 절감 | 중간 |
| 샘플링 적용 | 트래픽 많은 메트릭 10% 샘플링 | 20~40% 비용 절감 | 중간 |
| 보관 기간 단축 | 13개월 → 3개월로 단축 | 30~50% 저장소 비용 절감 | 쉬움 |
| 집계 메트릭 활용 | 초 단위 → 분 단위 집계로 전환 | 20~30% 데이터 포인트 절감 | 중간 |
| 조건부 전송 | 중요 이벤트만 메트릭 전송 | 10~30% 비용 절감 | 어려움 |
| 오픈소스 전환 | Prometheus로 이전 | 80~95% 비용 절감 | 어려움 |
프로덕션 체크리스트와 트러블슈팅
실제 프로덕션 환경에 배포하기 전 반드시 확인해야 할 체크리스트입니다.
# 프로덕션 배포 전 체크리스트 자동 검증
class MetricHealthChecker:
"""커스텀 메트릭 건강성 체크"""
def __init__(self, metric_name):
self.metric_name = metric_name
self.issues = []
self.warnings = []
def check_cardinality(self, max_cardinality=1000):
"""카디널리티 체크"""
try:
cardinality_info = api.Metric.get_cardinality(metric=self.metric_name)
cardinality = cardinality_info.get('cardinality', 0)
if cardinality > max_cardinality:
self.issues.append(
f"❌ 카디널리티 너무 높음: {cardinality:,} "
f"(임계값: {max_cardinality:,})"
)
elif cardinality > max_cardinality * 0.7:
self.warnings.append(
f"⚠️ 카디널리티 주의: {cardinality:,} "
f"(임계값의 70% 초과)"
)
else:
print(f"✅ 카디널리티 정상: {cardinality:,}")
# 태그별 카디널리티 분석
for tag in cardinality_info.get('tag_cardinalities', []):
tag_name = tag['tag']
tag_cardinality = tag['cardinality']
if tag_cardinality > 100:
self.warnings.append(
f"⚠️ 태그 '{tag_name}' 카디널리티 높음: {tag_cardinality}"
)
except Exception as e:
self.issues.append(f"❌ 카디널리티 체크 실패: {e}")
def check_naming_convention(self):
"""네이밍 컨벤션 체크"""
# 권장: business.domain.metric_type 형식
parts = self.metric_name.split('.')
if len(parts) < 3:
self.warnings.append(
f"⚠️ 메트릭 이름이 너무 짧음: {self.metric_name}\n"
f" 권장: business.orders.count 형식"
)
if not self.metric_name.startswith('business.'):
self.warnings.append(
f"⚠️ 비즈니스 메트릭은 'business.'로 시작 권장"
)
if any(char.isupper() for char in self.metric_name):
self.issues.append(
f"❌ 메트릭 이름에 대문자 사용 금지: {self.metric_name}\n"
f" 소문자와 언더스코어만 사용"
)
def check_data_freshness(self):
"""데이터 신선도 체크 (최근 데이터가 들어오는지)"""
try:
# 지난 5분간 데이터 포인트 확인
query = f"avg:last_5m:{self.metric_name}{{*}}"
result = api.Metric.query(
start=int((datetime.now() - timedelta(minutes=5)).timestamp()),
end=int(datetime.now().timestamp()),
query=query
)
if not result.get('series'):
self.issues.append(
f"❌ 지난 5분간 데이터 없음\n"
f" 메트릭이 전송되지 않고 있을 수 있음"
)
else:
print(f"✅ 데이터 정상 수집 중")
except Exception as e:
self.issues.append(f"❌ 데이터 신선도 체크 실패: {e}")
def check_cost_estimate(self):
"""예상 비용 체크"""
try:
cardinality_info = api.Metric.get_cardinality(metric=self.metric_name)
cardinality = cardinality_info.get('cardinality', 0)
monthly_cost_usd = cardinality * 0.05
monthly_cost_krw = monthly_cost_usd * 1300
print(f"\n💰 예상 월 비용:")
print(f" ${monthly_cost_usd:.2f} (약 ₩{monthly_cost_krw:,.0f})")
if monthly_cost_usd > 100:
self.warnings.append(
f"⚠️ 월 비용이 $100 초과: ${monthly_cost_usd:.2f}\n"
f" 비용 최적화 검토 필요"
)
if monthly_cost_usd > 500:
self.issues.append(
f"❌ 월 비용이 $500 초과: ${monthly_cost_usd:.2f}\n"
f" 즉시 최적화 필요"
)
except Exception as e:
self.warnings.append(f"⚠️ 비용 추정 실패: {e}")
def generate_report(self):
"""최종 보고서 생성"""
print("\n" + "="*80)
print(f"커스텀 메트릭 건강성 체크 보고서: {self.metric_name}")
print("="*80 + "\n")
# 체크 실행
self.check_naming_convention()
self.check_cardinality()
self.check_data_freshness()
self.check_cost_estimate()
# 결과 출력
if self.issues:
print("\n🚨 치명적 문제 (즉시 수정 필요):")
for issue in self.issues:
print(f"\n{issue}")
if self.warnings:
print("\n⚠️ 경고 (검토 권장):")
for warning in self.warnings:
print(f"\n{warning}")
if not self.issues and not self.warnings:
print("\n✅ 모든 체크 통과! 프로덕션 배포 가능")
return True
elif self.issues:
print("\n❌ 치명적 문제 발견. 수정 후 재배포 필요")
return False
else:
print("\n⚠️ 경고 사항 검토 후 배포 결정")
return True
# 사용 예시
if __name__ == '__main__':
checker = MetricHealthChecker('business.orders.count')
is_healthy = checker.generate_report()
if not is_healthy:
print("\n배포 중단!")
exit(1)| 체크 항목 | 체크 내용 | 통과 기준 | 실패 시 조치 |
|---|---|---|---|
| 네이밍 컨벤션 | 메트릭 이름 형식 | business.domain.type | 이름 변경 후 재배포 |
| 카디널리티 | 고유 시계열 개수 | 1,000개 이하 | 태그 최적화 필수 |
| 태그 설계 | 각 태그 고유값 | 10개 이하 | 태그 그룹화 |
| 데이터 신선도 | 최근 5분 데이터 | 데이터 존재 | 전송 코드 확인 |
| 예상 비용 | 월 비용 추정 | $100 이하 | 비용 최적화 |
| 문서화 | 메트릭 설명 | README 작성 | 문서 추가 |
| 알림 설정 | 임계값 설정 | 알림 룰 존재 | 알림 추가 |
| 대시보드 | 시각화 여부 | 대시보드에 표시 | 대시보드 생성 |
실전 트러블슈팅 가이드
마지막으로 자주 발생하는 문제와 해결 방법을 정리했습니다.
# 트러블슈팅 헬퍼 함수들
def troubleshoot_no_data():
"""메트릭 데이터가 안 보일 때"""
print("🔍 메트릭 데이터가 보이지 않는 문제 해결\n")
checklist = [
{
"문제": "메트릭이 아예 안 보임",
"원인": [
"메트릭 전송 코드가 실행되지 않음",
"네트워크 연결 문제",
"잘못된 API 키"
],
"해결": [
"1. 로그에서 메트릭 전송 확인",
"2. statsd.increment() 호출 여부 확인",
"3. 네트워크 방화벽 체크",
"4. API 키 유효성 확인"
]
},
{
"문제": "처음엔 보이다가 갑자기 사라짐",
"원인": [
"카디널리티 한도 초과",
"데이터 샘플링 적용됨",
"비용 한도 도달"
],
"해결": [
"1. 카디널리티 체크",
"2. Datadog 계정 한도 확인",
"3. 청구 상태 확인",
"4. 메트릭 전송 에러 로그 확인"
]
},
{
"문제": "일부 태그만 안 보임",
"원인": [
"태그 이름에 특수문자 사용",
"태그 값이 null 또는 빈 문자열",
"태그 개수 한도 초과"
],
"해결": [
"1. 태그 이름을 영문 소문자와 언더스코어만 사용",
"2. None 값 필터링 추가",
"3. 태그 개수 10개 이하로 제한"
]
}
]
for item in checklist:
print(f"❓ {item['문제']}")
print("\n가능한 원인:")
for cause in item['원인']:
print(f" • {cause}")
print("\n해결 방법:")
for solution in item['해결']:
print(f" {solution}")
print("\n" + "-"*60 + "\n")
def troubleshoot_high_cost():
"""비용이 갑자기 증가했을 때"""
print("💸 메트릭 비용 급증 문제 해결\n")
print("즉시 확인 사항:")
print("1. 새로 추가된 메트릭 확인")
print("2. 카디널리티가 높은 메트릭 찾기")
print("3. 최근 코드 변경 사항 리뷰")
print("4. 태그에 고유값(user_id 등) 사용 여부")
print("\n긴급 조치:")
print("1. 고카디널리티 메트릭 전송 즉시 중단")
print("2. 문제 태그 제거 후 재배포")
print("3. Datadog 지원팀에 비용 조정 요청")
print("\n예방 대책:")
print("1. 메트릭 추가 시 카디널리티 리뷰 프로세스")
print("2. 주간 비용 모니터링 자동화")
print("3. 비용 알림 설정 (예산의 80% 도달 시)")
def troubleshoot_alert_fatigue():
"""알림이 너무 많을 때"""
print("🔔 알림 피로 문제 해결\n")
print("현상 분석:")
print("• 하루 알림 횟수: ___회")
print("• 실제 액션이 필요했던 알림: ___회")
print("• 알림 무시 비율: ___%")
print("\n해결 전략:")
print("1. 임계값 조정: 너무 민감한 임계값 완화")
print("2. 지속 시간 추가: 5분 지속 시에만 알림")
print("3. 알림 통합: 여러 알림을 하나로 그룹화")
print("4. 시간대 필터: 업무 시간에만 알림")
print("5. 심각도 분리: Critical만 전화, Warning은 Slack")
print("\n예시 - 개선 전:")
print(" 주문 < 100건 → 즉시 알림")
print(" 결과: 하루 50회 알림, 대부분 일시적 감소")
print("\n예시 - 개선 후:")
print(" 주문 < 전주 평균의 70% && 5분 지속 → 알림")
print(" 결과: 하루 2~3회 알림, 모두 실제 문제")
# 실행
if __name__ == '__main__':
troubleshoot_no_data()
print("\n" + "="*80 + "\n")
troubleshoot_high_cost()
print("\n" + "="*80 + "\n")
troubleshoot_alert_fatigue()커스텀 메트릭은 비즈니스 상태를 실시간으로 파악하는 강력한 도구입니다. 주문 건수, 매출액, 결제 성공률 같은 핵심 KPI를 기술 메트릭과 함께 모니터링하면 IT팀과 비즈니스팀이 같은 데이터를 보며 빠르게 의사결정할 수 있습니다.
하지만 카디널리티 관리를 소홀히 하면 월 수천 달러 청구서를 받을 수 있으니 주의가 필요합니다. user_id, order_id 같은 고유값을 태그로 쓰지 말고, 연속형 값은 반드시 5~10개 구간으로 그룹화하세요. 태그는 각각 10개 이하의 고유값을 유지하는 게 핵심입니다.
Python은 datadog 라이브러리나 prometheus-client로, Java는 Micrometer로 쉽게 구현할 수 있습니다. 프로덕션 배포 전에는 카디널리티, 네이밍 컨벤션, 예상 비용을 반드시 체크하고, 정기적으로 미사용 메트릭을 정리하며, 알림 임계값을 지속적으로 튜닝해야 합니다. 제대로 설계하면 월 수십만 원으로 수억 원의 매출 손실을 막을 수 있는 최고의 투자가 됩니다.
공식 참고 링크 안내
Datadog Metrics Without Limits
Grafana Dashboard Best Practices
.jpg)
0 댓글