카테고리 없음
FastAPI 서버 Prometheus, Grafana, Loki, Tempo로 모니터링하기
늘이
2024. 11. 15. 13:12
0. 진행 단계
- FastAPI 애플리케이션 설정
- Prometheus 설정:
- Prometheus는 메트릭 수집 도구로, FastAPI 애플리케이션에서 노출하는 메트릭을 수집합니다.
- FastAPI 애플리케이션에 prometheus_client 패키지를 사용하여 메트릭을 노출합니다.
- Prometheus 설정 파일을 구성하여 FastAPI 애플리케이션에서 메트릭을 스크랩합니다.
- Grafana 설정:
- Grafana는 시각화 도구로, Prometheus에서 수집한 메트릭을 시각화합니다.
- Grafana에 Prometheus 데이터를 소스로 추가하고, 대시보드를 구성합니다.
- Loki 설정:
- Loki는 로그 수집 및 저장 도구로, FastAPI 애플리케이션의 로그를 수집합니다.
- promtail을 사용하여 로그를 Loki로 보냅니다.
- Tempo 설정:
- Tempo는 분산 트레이싱 도구로, FastAPI 애플리케이션에서 트레이스를 수집합니다.
- OpenTelemetry를 사용하여 트레이스를 노출하고 Tempo로 보냅니다.
1. FastAPI 애플리케이션 설정
1) main.py 추가
import logging
import uvicorn
from fastapi import FastAPI
# 로깅 관련 코드 관리를 위해 따로 loggin_config.py를 생성하여 관리
from logging_config import ExceptionLoggingMiddleware, RequestResponseLoggingMiddleware
from utils import PrometheusMiddleware, metrics, setting_otlp
app = FastAPI()
# 환경 변수 설정
APP_NAME = os.environ.get("APP_NAME", "app")
EXPOSE_PORT = os.environ.get("EXPOSE_PORT", 58000)
OTLP_GRPC_ENDPOINT = os.environ.get("OTLP_GRPC_ENDPOINT", "http://tempo:4317")
# 로깅 미들웨어 추가
app.add_middleware(ExceptionLoggingMiddleware)
app.add_middleware(RequestResponseLoggingMiddleware)
# Prometheus 설정
app.add_middleware(PrometheusMiddleware, app_name=APP_NAME)
app.add_route("/metrics", metrics)
# OpenTelemetry exporter 설정
setting_otlp(app, APP_NAME, OTLP_GRPC_ENDPOINT)
# Uvicorn 엔드포인트 접근 로그 필터
class EndpointFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
return record.getMessage().find("GET /metrics") == -1
# Filter out /endpoint
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
@app.get("/")
async def read_root():
return {"Hello": "World"}
if __name__ == "__main__":
# update uvicorn access logger format
log_config = uvicorn.config.LOGGING_CONFIG
log_config["formatters"]["access"][
"fmt"
] = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s] - %(message)s"
uvicorn.run(app, host="0.0.0.0", port=EXPOSE_PORT, log_config=log_config)
2) logging_config.py
import os
import logging
from logging.handlers import TimedRotatingFileHandler
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from fastapi.responses import JSONResponse
# 로그 디렉토리 생성 함수
def create_log_directory(path):
if not os.path.exists(path):
os.makedirs(path)
# 로그 디렉토리 생성
create_log_directory("logs/error")
create_log_directory("logs/request")
# 로깅 포맷 설정
log_formatter = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] " \
"[trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s] - %(message)s")
# 에러 로그 핸들러 설정
error_log_handler = TimedRotatingFileHandler("logs/error/error.log", when="midnight", interval=1, backupCount=90, encoding='utf-8')
error_log_handler.setFormatter(log_formatter)
error_log_handler.suffix = "%Y-%m-%d"
error_log_handler.setLevel(logging.ERROR)
# 요청/응답 로그 핸들러 설정
request_log_handler = TimedRotatingFileHandler("logs/request/request.log", when="midnight", interval=1, backupCount=90, encoding='utf-8')
request_log_handler.setFormatter(log_formatter)
request_log_handler.suffix = "%Y-%m-%d"
request_log_handler.setLevel(logging.INFO)
# 스트림 핸들러 설정
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(log_formatter)
# 로깅 기본 설정
logging.basicConfig(level=logging.INFO, handlers=[error_log_handler, request_log_handler, stream_handler])
logger = logging.getLogger(__name__)
# 로깅 미들웨어 설정
class ExceptionLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
response = await call_next(request)
return response
except Exception as exc:
logger.error(f"Exception: {exc}", exc_info=True)
return JSONResponse(status_code=500, content={"message": "Internal Server Error"})
class RequestResponseLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
client_ip = request.client.host
pid = os.getpid()
logger.info(f"Request from {client_ip}: {request.method} {request.url} - PID:{pid}")
response = await call_next(request)
logger.info(f"Response to {client_ip}: {response.status_code} - PID:{pid}")
return response
3) Dockerfile
# 기본 이미지 설정
FROM python:3.10-slim
# apt init
ENV LANG=C.UTF-8
ENV TZ=Asia/Seoul
# 작업 디렉토리 설정
WORKDIR /app
# 의존성 파일 복사 및 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 소스 코드 복사
COPY . /app
EXPOSE 58000
# 애플리케이션 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "8"]
2. Prometheus 설정
1) Prometheus 설정 파일('promehteus.yml') 작성
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: [localhost:9090']
- job_name: 'fastapi'
static_configs:
- targets: [localhost:8000']
3. Loki 설정
1) Loki 설정 파일 작성(loki-config.yaml)- default 설정 파일 사용해도 됨! 로그 보관 기간 설정이 필요해서 구성함
auth_enabled: false
server:
http_listen_port: 3100
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
storage_config:
boltdb_shipper:
active_index_directory: /loki/index
cache_location: /loki/boltdb-cache
cache_ttl: 24h
shared_store: filesystem
filesystem:
directory: /loki/chunks
table_manager:
retention_deletes_enabled: true
retention_period: 2160h # 90 days
limits_config:
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 2160h # 90 days
Docker Loki 로깅 플러그인 설치
# Docker Loki 플러그인 설치
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
# Docker 서비스를 재시작하여 변경사항 적용
sudo systemctl restart docker
# Docker Loki 플러그인 활성화
docker plugin enable loki
# Loki 플러그인이 정상 설치되었는지 확인: 상태가 enabled 로 표시됨
docker plugin ls
4. Grafana 설정
1) Grafana 웹 인터페이스 접속
URL: http://localhost:3000
2) 데이터 소스 추가
Loki 데이터 소스 추가
데이터 소스를 저장해놔야 도커 재실행 시 데이터 소스가 유지되므로 dashboards.yaml 작성 필요
apiVersion: 1
providers:
- name: 'FastAPI Observability'
orgId: 1
folder: ''
type: 'file'
disableDeletion: true
editable: true
options:
path: '/etc/grafana/dashboards'
2) 대시보드 생성
Home > Dashboards > New dashboard > Save dashboard 아이콘 클릭
Title, Description 등 내용 입력 > Save 버튼 클릭
만들어진 Dashboard 확인
4) 메트릭 시각화 구성
4. Tempo 설정
1) OpenTelemetry 패키지 설치
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi opentelemetry-exporter-otlp
4) FastAPI 애플리케이션 코드에 트레이싱 추가
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
app = FastAPI()
# OpenTelemetry 트레이서 설정
trace.set_tracer_provider(TracerProvider())
span_processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://<Tempo 서버 주소>:3200"))
trace.get_tracer_provider().add_span_processor(span_processor)
# FastAPI 애플리케이션에 트레이서 적용
FastAPIInstrumentor.instrument_app(app)
@app.get("/")
def read_root():
return {"Hello": "World"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
docker-compose.yml 구성
Loki
https://grafana.com/docs/loki/latest/setup/install/docker/