Skip to content

Commit fe0b626

Browse files
authored
Merge pull request #773 from DataIntegrationGroup/fix/health-db-check
fix(api): probe database in /health so 200 proves PostGIS reachable
2 parents 8e79756 + 8cc9c88 commit fe0b626

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

‎core/app.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@
2020
from contextlib import asynccontextmanager
2121
from typing import AsyncGenerator
2222

23-
from fastapi import FastAPI
23+
from fastapi import Depends, FastAPI, Response, status
2424
from fastapi import Request
2525
from fastapi.openapi.docs import (
2626
get_swagger_ui_html,
2727
get_swagger_ui_oauth2_redirect_html,
2828
)
2929
from fastapi.openapi.utils import get_openapi
30+
from sqlalchemy import text
31+
from sqlalchemy.orm import Session
32+
33+
from db.engine import get_db_session
3034

3135
from .settings import settings
3236

@@ -221,8 +225,25 @@ async def warmup():
221225

222226
@app.get("/health", tags=["meta"])
223227
@public_route
224-
async def health():
225-
return {"status": "ok", "version": settings.version}
228+
def health(response: Response, session: Session = Depends(get_db_session)):
229+
# Ping the database so a 200 actually proves PostGIS is reachable, not
230+
# just that the process is up. Uptime monitors / status pages assert on
231+
# the "db" field; on failure return 503 so they flag the outage.
232+
try:
233+
session.execute(text("SELECT 1"))
234+
db_ok = True
235+
except Exception:
236+
logger.exception("health check: database ping failed")
237+
db_ok = False
238+
239+
if not db_ok:
240+
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
241+
242+
return {
243+
"status": "ok" if db_ok else "degraded",
244+
"db": "ok" if db_ok else "error",
245+
"version": settings.version,
246+
}
226247

227248
return app
228249

‎tests/test_health.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# ===============================================================================
2+
# Copyright 2025 ross
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# ===============================================================================
16+
from db.engine import get_db_session
17+
from tests import client
18+
19+
20+
def test_health_ok():
21+
"""A healthy service pings the DB and reports db=ok with a 200."""
22+
response = client.get("/health")
23+
assert response.status_code == 200
24+
body = response.json()
25+
assert body["status"] == "ok"
26+
assert body["db"] == "ok"
27+
assert "version" in body
28+
29+
30+
def test_health_db_down_returns_503():
31+
"""When the DB ping fails, /health returns 503 and reports db=error."""
32+
33+
class _BadSession:
34+
def execute(self, *args, **kwargs):
35+
raise Exception("simulated database outage")
36+
37+
def _bad_session():
38+
yield _BadSession()
39+
40+
client.app.dependency_overrides[get_db_session] = _bad_session
41+
try:
42+
response = client.get("/health")
43+
assert response.status_code == 503
44+
body = response.json()
45+
assert body["status"] == "degraded"
46+
assert body["db"] == "error"
47+
finally:
48+
client.app.dependency_overrides.pop(get_db_session, None)

0 commit comments

Comments
 (0)