FastAPI 모니터링
FastAPI 모니터링
서드파티 api 호출 시간을 시각적으로 정리해서 보고 싶었다. 내가 원했던 건 하나의 요청에 모든 호출이 묶이기를 바랬는데, jaeger나 zipkin은 내 능력 부족으로 뭐가 잘 안되더라… span id 하나 넣고 싹 다 묶이기를 바랬는데 그게 생각처럼 잘 되지를 않았다.
그래서 ‘이길 수 없으면 합류하라’는 말처럼 그냥 누가 그라파나 템플릿 만들어 놓은거 썼다. 역시 검증된 템플릿이 좋긴 좋다. 어쨌든 해당 템플릿에 맞춰서 내 코드를 수정했던 과정이라도 써놓으려고 한다.
목표
FastAPI_Observability 라는 그라파나 템플릿을 프로젝트에 녹여내는 것이 목표다.
사전 세팅이 좀 많긴 하다. 이 글에서는 코드 위주의 설명이니 도커와 해당 소스코드의 github 로 접속해서 코드는 받아놓도록 하자. 이거 만든 사람이 스프링 부트도 유사한 템플릿을 만들었다고 하는데, 시간이 되면 개인 프로젝트에도 적용해서 글을 써보려 한다.
코드
먼저 관련 라이브러리를 설치해야 한다. 가장 중심이 되는 라이브러리가 Opentelemetry인데, 단일 오픈소스 표준을 제공하며 클라우드 네이티브 애플리케이션 및 인프라에서 측정항목, trace, 로그를 캡처하고 내보내는 기술 집합을 제공하는 라이브러리라고 한다.
그런데 생각보다 이것저것 설치할게 많아서 검색해서 import 안되는거 설치하는걸 추천한다. 한 가지 노하우가 있다면 점(.)으로 구별된 라이브러리 이름을 -으로 치환해서 pip install 치면 대충 되는 것 같다.
예를 들면 아래와 같은 라이브러리는
1
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
이런식으로 설치하면 보통 다 진행된다.
1
pip install opentelemetry-instrumentation-fastapi
어쨌든 메인 실행 파일을 보자면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from starlette.middleware.cors import CORSMiddleware
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from fastapi import FastAPI
import uvicorn
import logging
from utils import PrometheusMiddleware, metrics, setting_otlp
app = FastAPI()
APP_NAME = "main-project"
origins = [
"http://localhost:8080"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Setting metrics middleware
app.add_middleware(PrometheusMiddleware, app_name=APP_NAME)
app.add_route("/metrics", metrics)
# Setting OpenTelemetry exporter
setting_otlp(app, APP_NAME, "http://tempo:4317")
RequestsInstrumentor().instrument()
class EndpointFilter(logging.Filter):
# Uvicorn endpoint access log filter
def filter(self, record: logging.LogRecord) -> bool:
return record.getMessage().find("GET /metrics") == -1
# Filter out /endpoint
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
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=8000, log_config=log_config)
이런식으로 코드를 추려봤다. utils 클래스는 해당 라이브러리 레포에서 긁어온 후 모니터링이나 로깅 관련 웹 주소들만 살짝 변경해줬다.
1
RequestsInstrumentor().instrument()
이 코드는 외부 요청을 하는 requests 모듈에 span_id같은 컨텍스트를 자동으로 전파해준다고 해서 넣었다. 그런데 jaeger랑 zipkin 설정할때도 헤더에 inject하고 별 쇼를 다했는데 잘 설정 안되서 저 코드의 유효성은 잘 모르겠다. 그래도 넣어보자.
설정 파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
x-logging: &default-logging
driver: loki
options:
loki-url: 'http://localhost:3100/api/prom/push'
loki-pipeline-stages: |
- multiline:
firstline: '^\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2}'
max_wait_time: 3s
- regex:
expression: '^(?P<time>\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2},\d{3}) (?P<message>(?s:.*))$$'
version: "3.4"
services:
loki:
image: grafana/loki:3.0.0
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
app-a:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
depends_on:
- loki
- mongo
environment:
APP_NAME: "app-a"
logging: *default-logging
prometheus:
image: prom/prometheus:v2.51.2
ports:
- "9090:9090"
volumes:
- ./etc/prometheus:/workspace
command:
- --config.file=/workspace/prometheus.yml
- --enable-feature=exemplar-storage
depends_on:
- loki
logging: *default-logging
tempo:
image: grafana/tempo:2.4.1
command: [ "--target=all", "--storage.trace.backend=local", "--storage.trace.local.path=/var/tempo", "--auth.enabled=false" ]
ports:
- "4317:4317"
- "4318:4318"
depends_on:
- loki
logging: *default-logging
grafana:
image: grafana/grafana:10.4.2
ports:
- "3000:3000"
volumes:
- ./etc/grafana/:/etc/grafana/provisioning/datasources
- ./etc/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml
- ./etc/dashboards:/etc/grafana/dashboards
depends_on:
- loki
- prometheus
logging: *default-logging
mongo:
image: mongo
ports:
- "27017:27017"
volumes:
- ~/data:/data/db
app-a의 세부 설정만 조금 건드렸고, mongo는 내가 써야해서 임시로 추가했다. 참고로 app-* 이런 이름 형식으로 로깅이나 메트릭을 수집하는것 같다. 관련 설정을 전부 바꿔주거나 해당 포맷을 따르는걸 추천한다. 난 귀찮아서 app-a 그대로 썼다.
그리고 프로젝트의 구조에 맞게 etc 폴더를 만든 후, 원본 레포의 yml 파일들을 적절히 넣어준다. loki의
1
command: -config.file=/etc/loki/local-config.yaml
같은건 도커 내부 이미지 주소이지만, 그라파나 같은곳의
1
2
3
4
volumes:
- ./etc/grafana/:/etc/grafana/provisioning/datasources
- ./etc/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml
- ./etc/dashboards:/etc/grafana/dashboards
과 같은 코드는 프로젝트 구조와 싱크되기 때문에 잘 넣어주도록 하자.
이제 설정이 끝났다면 아래 커맨드를 날려주자.
1
docker-compose up -d
확인
localhost:3000으로 들어갔을 때 로그인 후 요런식으로 뜬다면 성공이다. 참고로 초기 비번 아이디는 admin admin 이라고 한다.
PR 99 Requests Duration 탭에 가서 점을 찍어보면 Query with tempo 버튼이 뜬다. 눌러보자.
그러면 내 api가 어디서 이렇게 느려터졌는지 한 사이클로 볼 수 있다. 이만 지연시간 줄이러 가야해서 글을 마무리한다…
참고
- https://opentelemetry.io/
- https://grafana.com/grafana/dashboards/16110-fastapi-observability/
- https://github.com/Blueswen/fastapi-observability