Skip to content

Commit 7c48310

Browse files
OpenTelemetry monitoring (#63)
* chore: update requirements * perf: add telemetry * perf: add telemetry * perf: add loki logging to start
1 parent 494dc3c commit 7c48310

File tree

5 files changed

+254
-19
lines changed

5 files changed

+254
-19
lines changed

main.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
from fastapi import FastAPI, Response, status, responses
2+
import os
3+
import logging
4+
import telemetry
25

3-
app = FastAPI()
6+
7+
OTLP_GRPC_ENDPOINT = os.getenv("OTLP_GRPC_ENDPOINT", "tempo:4317")
8+
9+
app_name = "refinery-authorizer"
10+
app = FastAPI(title=app_name)
11+
12+
if telemetry.ENABLE_TELEMETRY:
13+
print("WARNING: Running telemetry.", flush=True)
14+
telemetry.setting_otlp(app, app_name=app_name, endpoint=OTLP_GRPC_ENDPOINT)
15+
app.add_middleware(telemetry.PrometheusMiddleware, app_name=app_name)
16+
app.add_route("/metrics", telemetry.metrics)
17+
18+
# Filter out /metrics
19+
logging.getLogger("uvicorn.access").addFilter(
20+
lambda record: "GET /metrics" not in record.getMessage()
21+
)
422

523

624
@app.get("/health")

requirements.txt

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,130 @@
66
#
77
annotated-types==0.7.0
88
# via
9-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
9+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
1010
# pydantic
1111
anyio==4.9.0
1212
# via
13-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
13+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
1414
# starlette
15+
asgiref==3.9.2
16+
# via opentelemetry-instrumentation-asgi
1517
certifi==2025.7.14
1618
# via
17-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
19+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
1820
# requests
1921
charset-normalizer==3.4.2
2022
# via
21-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
23+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
2224
# requests
2325
click==8.2.1
2426
# via
25-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
27+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
2628
# uvicorn
2729
fastapi==0.116.1
28-
# via -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
30+
# via -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
31+
googleapis-common-protos==1.70.0
32+
# via opentelemetry-exporter-otlp-proto-grpc
33+
grpcio==1.75.0
34+
# via opentelemetry-exporter-otlp-proto-grpc
2935
h11==0.16.0
3036
# via
31-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
37+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
3238
# uvicorn
3339
idna==3.10
3440
# via
35-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
41+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
3642
# anyio
3743
# requests
44+
importlib-metadata==8.7.0
45+
# via opentelemetry-api
46+
opentelemetry-api==1.37.0
47+
# via
48+
# opentelemetry-exporter-otlp-proto-grpc
49+
# opentelemetry-instrumentation
50+
# opentelemetry-instrumentation-asgi
51+
# opentelemetry-instrumentation-fastapi
52+
# opentelemetry-instrumentation-logging
53+
# opentelemetry-sdk
54+
# opentelemetry-semantic-conventions
55+
opentelemetry-exporter-otlp-proto-common==1.37.0
56+
# via opentelemetry-exporter-otlp-proto-grpc
57+
opentelemetry-exporter-otlp-proto-grpc==1.37.0
58+
# via -r requirements/requirements.in
59+
opentelemetry-instrumentation==0.58b0
60+
# via
61+
# opentelemetry-instrumentation-asgi
62+
# opentelemetry-instrumentation-fastapi
63+
# opentelemetry-instrumentation-logging
64+
opentelemetry-instrumentation-asgi==0.58b0
65+
# via opentelemetry-instrumentation-fastapi
66+
opentelemetry-instrumentation-fastapi==0.58b0
67+
# via -r requirements/requirements.in
68+
opentelemetry-instrumentation-logging==0.58b0
69+
# via -r requirements/requirements.in
70+
opentelemetry-proto==1.37.0
71+
# via
72+
# opentelemetry-exporter-otlp-proto-common
73+
# opentelemetry-exporter-otlp-proto-grpc
74+
opentelemetry-sdk==1.37.0
75+
# via opentelemetry-exporter-otlp-proto-grpc
76+
opentelemetry-semantic-conventions==0.58b0
77+
# via
78+
# opentelemetry-instrumentation
79+
# opentelemetry-instrumentation-asgi
80+
# opentelemetry-instrumentation-fastapi
81+
# opentelemetry-sdk
82+
opentelemetry-util-http==0.58b0
83+
# via
84+
# opentelemetry-instrumentation-asgi
85+
# opentelemetry-instrumentation-fastapi
86+
packaging==25.0
87+
# via opentelemetry-instrumentation
88+
prometheus-client==0.23.1
89+
# via -r requirements/requirements.in
90+
protobuf==6.32.1
91+
# via
92+
# googleapis-common-protos
93+
# opentelemetry-proto
3894
pydantic==2.7.4
3995
# via
40-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
96+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
4197
# -r requirements/requirements.in
4298
# fastapi
4399
pydantic-core==2.18.4
44100
# via
45-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
101+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
46102
# pydantic
47103
requests==2.32.4
48-
# via -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
104+
# via -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
49105
sniffio==1.3.1
50106
# via
51-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
107+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
52108
# anyio
53109
starlette==0.47.2
54110
# via
55-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
111+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
56112
# fastapi
57113
typing-extensions==4.14.1
58114
# via
59-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
115+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
60116
# anyio
61117
# fastapi
118+
# grpcio
119+
# opentelemetry-api
120+
# opentelemetry-exporter-otlp-proto-grpc
121+
# opentelemetry-sdk
122+
# opentelemetry-semantic-conventions
62123
# pydantic
63124
# pydantic-core
64125
# starlette
65126
urllib3==2.5.0
66127
# via
67-
# -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
128+
# -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
68129
# requests
69130
uvicorn==0.35.0
70-
# via -r /home/runner/work/refinery-submodule-parent-images/refinery-submodule-parent-images/refinery-authorizer/requirements/mini-requirements.txt
131+
# via -r /Users/andhrelja/Projects/refinery-authorizer/requirements/mini-requirements.txt
132+
wrapt==1.17.3
133+
# via opentelemetry-instrumentation
134+
zipp==3.23.0
135+
# via importlib-metadata

requirements/requirements.in

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
-r mini-requirements.txt
2-
pydantic==2.7.4
2+
pydantic==2.7.4
3+
opentelemetry-exporter-otlp-proto-grpc==1.37.0
4+
opentelemetry-instrumentation-fastapi==0.58b0
5+
opentelemetry-instrumentation-logging==0.58b0
6+
prometheus-client==0.23.1

start

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
DEBUG_MODE=false
44
DEBUG_PORT=15672
5+
ENABLE_TELEMETRY=false
56

6-
while getopts d flag
7+
while getopts dg flag
78
do
89
case "${flag}" in
910
d) DEBUG_MODE=true;;
11+
g) ENABLE_TELEMETRY=true;;
1012
esac
1113
done
1214

@@ -29,7 +31,10 @@ echo -ne 'starting...'
2931
docker run -d --rm \
3032
--name refinery-authorizer \
3133
-p $DEBUG_PORT:$DEBUG_PORT \
34+
-e ENABLE_TELEMETRY=$ENABLE_TELEMETRY \
3235
--network dev-setup_default \
36+
--log-driver=loki \
37+
--log-opt loki-url="http://$HOST_IP:3100/loki/api/v1/push" \
3338
refinery-authorizer-dev $CMD > /dev/null 2>&1
3439
echo -ne '\t\t\t [done]\n'
3540

telemetry.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from typing import Tuple
2+
3+
import time
4+
import os
5+
6+
from opentelemetry import trace
7+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
8+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
9+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
10+
from opentelemetry.sdk.resources import Resource
11+
from opentelemetry.sdk.trace import TracerProvider
12+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
13+
from prometheus_client import REGISTRY, Counter, Gauge, Histogram
14+
from prometheus_client.openmetrics.exposition import (
15+
CONTENT_TYPE_LATEST,
16+
generate_latest,
17+
)
18+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
19+
from starlette.requests import Request
20+
from starlette.responses import Response
21+
from starlette.routing import Match
22+
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
23+
from starlette.types import ASGIApp
24+
25+
ENABLE_TELEMETRY = os.getenv("ENABLE_TELEMETRY", "false") == "true"
26+
27+
INFO = Gauge("fastapi_app_info", "FastAPI application information.", ["app_name"])
28+
REQUESTS = Counter(
29+
"fastapi_requests_total",
30+
"Total count of requests by method and path.",
31+
["method", "path", "app_name"],
32+
)
33+
RESPONSES = Counter(
34+
"fastapi_responses_total",
35+
"Total count of responses by method, path and status codes.",
36+
["method", "path", "status_code", "app_name"],
37+
)
38+
REQUESTS_PROCESSING_TIME = Histogram(
39+
"fastapi_requests_duration_seconds",
40+
"Histogram of requests processing time by path (in seconds)",
41+
["method", "path", "app_name"],
42+
)
43+
EXCEPTIONS = Counter(
44+
"fastapi_exceptions_total",
45+
"Total count of exceptions raised by path and exception type",
46+
["method", "path", "exception_type", "app_name"],
47+
)
48+
REQUESTS_IN_PROGRESS = Gauge(
49+
"fastapi_requests_in_progress",
50+
"Gauge of requests by method and path currently being processed",
51+
["method", "path", "app_name"],
52+
)
53+
54+
55+
class PrometheusMiddleware(BaseHTTPMiddleware):
56+
def __init__(self, app: ASGIApp, app_name: str = "fastapi-app") -> None:
57+
super().__init__(app)
58+
self.app_name = app_name
59+
INFO.labels(app_name=self.app_name).inc()
60+
61+
async def dispatch(
62+
self, request: Request, call_next: RequestResponseEndpoint
63+
) -> Response:
64+
method = request.method
65+
path, is_handled_path = self.get_path(request)
66+
67+
if not is_handled_path:
68+
return await call_next(request)
69+
70+
REQUESTS_IN_PROGRESS.labels(
71+
method=method, path=path, app_name=self.app_name
72+
).inc()
73+
REQUESTS.labels(method=method, path=path, app_name=self.app_name).inc()
74+
before_time = time.perf_counter()
75+
try:
76+
response = await call_next(request)
77+
except BaseException as e:
78+
status_code = HTTP_500_INTERNAL_SERVER_ERROR
79+
EXCEPTIONS.labels(
80+
method=method,
81+
path=path,
82+
exception_type=type(e).__name__,
83+
app_name=self.app_name,
84+
).inc()
85+
raise e from None
86+
else:
87+
status_code = response.status_code
88+
after_time = time.perf_counter()
89+
# retrieve trace id for exemplar
90+
span = trace.get_current_span()
91+
trace_id = trace.format_trace_id(span.get_span_context().trace_id)
92+
93+
REQUESTS_PROCESSING_TIME.labels(
94+
method=method, path=path, app_name=self.app_name
95+
).observe(after_time - before_time, exemplar={"TraceID": trace_id})
96+
finally:
97+
RESPONSES.labels(
98+
method=method,
99+
path=path,
100+
status_code=status_code,
101+
app_name=self.app_name,
102+
).inc()
103+
REQUESTS_IN_PROGRESS.labels(
104+
method=method, path=path, app_name=self.app_name
105+
).dec()
106+
107+
return response
108+
109+
@staticmethod
110+
def get_path(request: Request) -> Tuple[str, bool]:
111+
for route in request.app.routes:
112+
match, child_scope = route.matches(request.scope)
113+
if match == Match.FULL:
114+
return route.path, True
115+
116+
return request.url.path, False
117+
118+
119+
def metrics(request: Request) -> Response:
120+
return Response(
121+
generate_latest(REGISTRY), headers={"Content-Type": CONTENT_TYPE_LATEST}
122+
)
123+
124+
125+
def setting_otlp(
126+
app: ASGIApp, app_name: str, endpoint: str, log_correlation: bool = True
127+
) -> None:
128+
# Setting OpenTelemetry
129+
# set the service name to show in traces
130+
resource = Resource.create(attributes={"service.name": app_name})
131+
132+
# set the tracer provider
133+
tracer = TracerProvider(resource=resource)
134+
trace.set_tracer_provider(tracer)
135+
136+
tracer.add_span_processor(
137+
BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, insecure=True))
138+
)
139+
140+
if log_correlation:
141+
LoggingInstrumentor().instrument(set_logging_format=True)
142+
143+
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer)

0 commit comments

Comments
 (0)