Skip to content

Commit 224b34a

Browse files
committed
feat: Add HTTP Basic auth on the metrics endpoint
1 parent b645ccb commit 224b34a

File tree

2 files changed

+38
-2
lines changed

2 files changed

+38
-2
lines changed

src/prometheus_fastapi_instrumentator/instrumentation.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import re
55
import warnings
66
from enum import Enum
7-
from typing import Any, Awaitable, Callable, List, Optional, Sequence, Union, cast
7+
from typing import Any, Awaitable, Callable, List, Optional, Sequence, Union, cast, Tuple
8+
from base64 import b64encode
89

9-
from fastapi import FastAPI
10+
from fastapi import FastAPI, HTTPException
1011
from prometheus_client import (
1112
CONTENT_TYPE_LATEST,
1213
REGISTRY,
@@ -227,6 +228,7 @@ def expose(
227228
endpoint: str = "/metrics",
228229
include_in_schema: bool = True,
229230
tags: Optional[List[Union[str, Enum]]] = None,
231+
basic_auth: Optional[Tuple[str, str]] = None,
230232
**kwargs: Any,
231233
) -> "PrometheusFastApiInstrumentator":
232234
"""Exposes endpoint for metrics.
@@ -246,6 +248,9 @@ def expose(
246248
247249
tags (List[str], optional): If you manage your routes with tags.
248250
Defaults to None.
251+
252+
basic_auth (Tuple[str, str], optional): username and password for
253+
HTTP basic authentication. Disabled if None.
249254
250255
kwargs: Will be passed to FastAPI route annotation.
251256
@@ -256,10 +261,23 @@ def expose(
256261
if self.should_respect_env_var and not self._should_instrumentate():
257262
return self
258263

264+
authorization_value = None
265+
if basic_auth is not None:
266+
username, password = basic_auth
267+
encoded_cred = b64encode(f'{username}:{password}'.encode('utf-8')).decode('ascii')
268+
authorization_value = f"Basic {encoded_cred}"
269+
259270
@app.get(endpoint, include_in_schema=include_in_schema, tags=tags, **kwargs)
260271
def metrics(request: Request) -> Response:
261272
"""Endpoint that serves Prometheus metrics."""
262273

274+
authorization_header = request.headers.get('authorization', None)
275+
if authorization_header != authorization_value:
276+
raise HTTPException(
277+
status_code=401,
278+
headers={'WWW-Authenticate': 'Basic realm="Access to metrics endpoint"'}
279+
)
280+
263281
ephemeral_registry = self.registry
264282
if "PROMETHEUS_MULTIPROC_DIR" in os.environ:
265283
ephemeral_registry = CollectorRegistry()

tests/test_instrumentator_expose.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import FastAPI
22
from prometheus_client import REGISTRY
33
from requests import Response as TestClientResponse
4+
from requests.auth import HTTPBasicAuth
45
from starlette.testclient import TestClient
56

67
from prometheus_fastapi_instrumentator import Instrumentator
@@ -76,3 +77,20 @@ def test_expose_custom_path():
7677
response = get_response(client, "/custom_metrics")
7778
assert response.status_code == 200
7879
assert b"http_request" in response.content
80+
81+
82+
def test_expose_basic_auth():
83+
username = 'hello'
84+
password = 'mom'
85+
app = create_app()
86+
Instrumentator().instrument(app).expose(app, basic_auth=(username, password))
87+
client = TestClient(app)
88+
89+
response = client.get("/metrics")
90+
assert response.status_code == 401
91+
assert b"http_request" not in response.content
92+
93+
auth = HTTPBasicAuth(username, password)
94+
response = client.get("/metrics", auth=auth)
95+
assert response.status_code == 200
96+
assert b"http_request" in response.content

0 commit comments

Comments
 (0)