Skip to content

Commit 2e22c78

Browse files
feat: make health check configurable (#122)
* Make health check configurable * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add test for ignoring staleness check * Add default to settings * Re-add poll frequency --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9771a6e commit 2e22c78

File tree

5 files changed

+102
-11
lines changed

5 files changed

+102
-11
lines changed

src/edge_proxy/health_check/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import typing
2+
from datetime import datetime
3+
from typing import Optional
4+
5+
from fastapi.responses import ORJSONResponse
6+
from starlette.background import BackgroundTask
7+
8+
9+
class HealthCheckResponse(ORJSONResponse):
10+
def __init__(
11+
self,
12+
status_code: int = 200,
13+
status: str = "ok",
14+
reason: Optional[str] = None,
15+
last_successful_update: Optional[datetime] = None,
16+
headers: typing.Mapping[str, str] | None = None,
17+
media_type: str | None = None,
18+
background: BackgroundTask | None = None,
19+
):
20+
content = {
21+
"status": status,
22+
"reason": reason,
23+
"last_successful_update": last_successful_update,
24+
}
25+
super().__init__(
26+
status_code=status_code,
27+
content=content,
28+
headers=headers,
29+
media_type=media_type,
30+
background=background,
31+
)

src/edge_proxy/server.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from contextlib import suppress
2-
from datetime import datetime
1+
from datetime import datetime, timedelta
32

43
import httpx
54
import structlog
@@ -8,6 +7,7 @@
87
from fastapi.middleware.gzip import GZipMiddleware
98
from fastapi.responses import ORJSONResponse
109

10+
from edge_proxy.health_check.responses import HealthCheckResponse
1111
from fastapi_utils.tasks import repeat_every
1212

1313
from edge_proxy.cache import LocalMemEnvironmentsCache
@@ -41,13 +41,31 @@ async def unknown_key_error(request, exc):
4141
@app.get("/health", response_class=ORJSONResponse, deprecated=True)
4242
@app.get("/proxy/health", response_class=ORJSONResponse)
4343
async def health_check():
44-
with suppress(TypeError):
45-
last_updated = datetime.now() - environment_service.last_updated_at
46-
buffer = 30 * len(settings.environment_key_pairs) # 30s per environment
47-
if last_updated.total_seconds() <= settings.api_poll_frequency_seconds + buffer:
48-
return ORJSONResponse(status_code=200, content={"status": "ok"})
44+
last_updated_at = environment_service.last_updated_at
45+
if not last_updated_at:
46+
return HealthCheckResponse(
47+
status_code=500,
48+
status="error",
49+
reason="environment document(s) not updated.",
50+
last_successful_update=None,
51+
)
4952

50-
return ORJSONResponse(status_code=500, content={"status": "error"})
53+
if settings.health_check.count_stale_documents_as_failing:
54+
buffer = settings.health_check.grace_period_seconds * len(
55+
settings.environment_key_pairs
56+
)
57+
threshold = datetime.now() - timedelta(
58+
seconds=settings.api_poll_frequency_seconds + buffer
59+
)
60+
if last_updated_at < threshold:
61+
return HealthCheckResponse(
62+
status_code=500,
63+
status="error",
64+
reason="environment document(s) stale.",
65+
last_successful_update=last_updated_at,
66+
)
67+
68+
return HealthCheckResponse(last_successful_update=last_updated_at)
5169

5270

5371
@app.get("/api/v1/flags/", response_class=ORJSONResponse)

src/edge_proxy/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ class ServerSettings(BaseModel):
100100
reload: bool = False
101101

102102

103+
class HealthCheckSettings(BaseModel):
104+
count_stale_documents_as_failing: bool = True
105+
grace_period_seconds: int = 30
106+
107+
103108
class AppSettings(BaseModel):
104109
environment_key_pairs: list[EnvironmentKeyPair] = Field(
105110
default_factory=lambda: [
@@ -128,6 +133,7 @@ class AppSettings(BaseModel):
128133
allow_origins: list[str] = Field(default_factory=lambda: ["*"])
129134
logging: LoggingSettings = LoggingSettings()
130135
server: ServerSettings = ServerSettings()
136+
health_check: HealthCheckSettings = HealthCheckSettings()
131137

132138

133139
class AppConfig(AppSettings, BaseSettings):

tests/test_server.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fastapi.testclient import TestClient
77
from pytest_mock import MockerFixture
88

9+
from edge_proxy.settings import AppSettings, HealthCheckSettings
910
from tests.fixtures.response_data import environment_1
1011

1112
if typing.TYPE_CHECKING:
@@ -30,18 +31,53 @@ def test_health_check_returns_500_if_cache_was_not_updated(
3031
) -> None:
3132
response = client.get("/proxy/health")
3233
assert response.status_code == 500
33-
assert response.json() == {"status": "error"}
34+
assert response.json() == {
35+
"status": "error",
36+
"reason": "environment document(s) not updated.",
37+
"last_successful_update": None,
38+
}
3439

3540

3641
def test_health_check_returns_500_if_cache_is_stale(
3742
mocker: MockerFixture,
3843
client: TestClient,
3944
) -> None:
45+
last_updated_at = datetime.now() - timedelta(days=10)
4046
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
41-
mocked_environment_service.last_updated_at = datetime.now() - timedelta(days=10)
47+
mocked_environment_service.last_updated_at = last_updated_at
4248
response = client.get("/proxy/health")
4349
assert response.status_code == 500
44-
assert response.json() == {"status": "error"}
50+
assert response.json() == {
51+
"status": "error",
52+
"reason": "environment document(s) stale.",
53+
"last_successful_update": last_updated_at.isoformat(),
54+
}
55+
56+
57+
def test_health_check_returns_200_if_cache_is_stale_and_health_check_configured_correctly(
58+
mocker: MockerFixture,
59+
client: TestClient,
60+
) -> None:
61+
# Given
62+
settings = AppSettings(
63+
health_check=HealthCheckSettings(count_stale_documents_as_failing=False)
64+
)
65+
mocker.patch("edge_proxy.server.settings", settings)
66+
67+
last_updated_at = datetime.now() - timedelta(days=10)
68+
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
69+
mocked_environment_service.last_updated_at = last_updated_at
70+
71+
# When
72+
response = client.get("/proxy/health")
73+
74+
# Then
75+
assert response.status_code == 200
76+
assert response.json() == {
77+
"status": "ok",
78+
"reason": None,
79+
"last_successful_update": last_updated_at.isoformat(),
80+
}
4581

4682

4783
def test_get_flags(

0 commit comments

Comments
 (0)