카테고리 없음

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) 데이터 소스 추가

Prometheus 데이터 소스 추가

 

[Springboot] 그라파나(grafana)-프로메테우스 연동

그라파나는 프로메테우스를 통해서 데이터를 조회하고 보여주는 역할을 함 프로메테우스를 데이터 소스로 사용해서 데이터 읽어옴 1. 데이터 소스 추가 1) 그라파나 접속 후 admin으로 로그인 http

k-sky.tistory.com

 

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/

 

Install Loki with Docker or Docker Compose | Grafana Loki documentation

Open source Install Loki with Docker or Docker Compose You can install Loki and Promtail with Docker or Docker Compose if you are evaluating, testing, or developing Loki. For production, Grafana recommends installing with Helm or Tanka. The configuration f

grafana.com