Skip to content

Commit 7faa78c

Browse files
Enhance health check endpoint to verify database connectivity
- Introduced PgStacApi class to extend StacApi with enhanced health checks. - Updated /_mgmt/ping endpoint to check database readiness. - Modified tests to validate new response format and database status.
1 parent 9ee1109 commit 7faa78c

File tree

6 files changed

+101
-10
lines changed

6 files changed

+101
-10
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ docker-shell:
3232
test:
3333
$(runtests) /bin/bash -c 'export && python -m pytest /app/tests/api/test_api.py --log-cli-level $(LOG_LEVEL)'
3434

35+
.PHONY: test-mgmt
36+
test-mgmt:
37+
$(runtests) /bin/bash -c 'export && python -m pytest /app/tests/resources/test_mgmt.py --log-cli-level $(LOG_LEVEL)'
38+
3539
.PHONY: run-database
3640
run-database:
3741
docker compose run --rm database

pr_description.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Enhance /_mgmt/ping endpoint to check if pgstac is ready
2+
3+
## Overview
4+
This PR enhances the `/_mgmt/ping` health check endpoint to actually verify database connectivity before returning a positive response. The current implementation always returns `{"message": "PONG"}` regardless of whether the database is actually accessible, which can lead to misleading health checks in production environments.
5+
6+
## Changes Made
7+
- Created a `PgStacApi` class in `stac_fastapi/pgstac/app.py` that extends the base `StacApi` class
8+
- Overrode the `add_health_check` method to implement database connectivity checks
9+
- Modified the ping endpoint to:
10+
- Test database connectivity by attempting to connect to the read pool
11+
- Verify pgstac is properly set up by querying the `pgstac.migrations` table
12+
- Return appropriate status codes and error messages when the database is unavailable
13+
- Updated the test in `tests/resources/test_mgmt.py` to verify the new response format
14+
15+
## Implementation Details
16+
The enhanced endpoint now:
17+
- Returns `{"message": "PONG", "database": "OK"}` with status 200 when the database is healthy
18+
- Returns a 503 Service Unavailable with descriptive error message when the database cannot be reached or pgstac is not properly set up
19+
20+
## Why This Is Important
21+
- Provides more accurate health/readiness checks in containerized environments
22+
- Better integration with container orchestration systems like Kubernetes
23+
- Faster detection of database connectivity issues
24+
- Helps operators quickly identify when database connectivity is the root cause of issues
25+
26+
## Testing
27+
The implementation can be tested by:
28+
29+
1. Running the API with a working database connection:
30+
```bash
31+
# Start with a working database
32+
docker-compose up -d
33+
# The endpoint should return status 200
34+
curl -v http://localhost:8080/_mgmt/ping
35+
```
36+
37+
2. Testing with a non-functioning database:
38+
```bash
39+
# Stop the database
40+
docker-compose stop pgstac
41+
# The endpoint should now return a 503 error
42+
curl -v http://localhost:8080/_mgmt/ping
43+
```
44+
45+
## Alternatives Considered
46+
I considered two alternative approaches:
47+
48+
1. **Using a simple connection check without querying any tables** - This would only verify the database is running but not that pgstac is properly set up.
49+
50+
2. **Implementing a more extensive set of health checks** - While more comprehensive, this would add complexity and potential performance overhead to the health check endpoint.
51+
52+
The current implementation provides a good balance between thoroughness and simplicity.

stac_fastapi/pgstac/app.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from contextlib import asynccontextmanager
1010

1111
from brotli_asgi import BrotliMiddleware
12-
from fastapi import FastAPI
13-
from fastapi.responses import ORJSONResponse
12+
from fastapi import FastAPI, Request, APIRouter
13+
from fastapi.responses import ORJSONResponse, JSONResponse
1414
from stac_fastapi.api.app import StacApi
1515
from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware
1616
from stac_fastapi.api.models import (
@@ -160,7 +160,39 @@ async def lifespan(app: FastAPI):
160160
await close_db_connection(app)
161161

162162

163-
api = StacApi(
163+
class PgStacApi(StacApi):
164+
"""PgStac API with enhanced health checks."""
165+
166+
def add_health_check(self):
167+
"""Add a health check with pgstac database readiness check."""
168+
mgmt_router = APIRouter(prefix=self.app.state.router_prefix)
169+
170+
@mgmt_router.get("/_mgmt/ping")
171+
async def ping(request: Request):
172+
"""Liveliness/readiness probe that checks database connection."""
173+
try:
174+
# Test read connection
175+
async with request.app.state.get_connection(request, "r") as conn:
176+
# Execute a simple query to verify pgstac is ready
177+
# Check if we can query the migrations table which should exist in pgstac
178+
result = await conn.fetchval("SELECT 1 FROM pgstac.migrations LIMIT 1")
179+
if result is not None:
180+
return {"message": "PONG", "database": "OK"}
181+
else:
182+
return JSONResponse(
183+
status_code=503,
184+
content={"message": "Database tables not found", "database": "ERROR"}
185+
)
186+
except Exception as e:
187+
return JSONResponse(
188+
status_code=503,
189+
content={"message": f"Database connection failed: {str(e)}", "database": "ERROR"}
190+
)
191+
192+
self.app.include_router(mgmt_router, tags=["Liveliness/Readiness"])
193+
194+
195+
api = PgStacApi(
164196
app=FastAPI(
165197
openapi_url=settings.openapi_url,
166198
docs_url=settings.docs_url,

tests/api/test_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pypgstac.db import PgstacDB
1111
from pypgstac.load import Loader
1212
from pystac import Collection, Extent, Item, SpatialExtent, TemporalExtent
13-
from stac_fastapi.api.app import StacApi
13+
from stac_fastapi.pgstac.app import PgStacApi
1414
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
1515
from stac_fastapi.extensions.core import (
1616
CollectionSearchExtension,
@@ -748,7 +748,7 @@ async def get_collection(
748748
]
749749
)
750750

751-
api = StacApi(
751+
api = PgStacApi(
752752
client=Client(pgstac_search_model=post_request_model),
753753
settings=settings,
754754
extensions=extensions,
@@ -806,7 +806,7 @@ async def test_no_extension(
806806
)
807807
extensions = []
808808
post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
809-
api = StacApi(
809+
api = PgStacApi(
810810
client=CoreCrudClient(pgstac_search_model=post_request_model),
811811
settings=settings,
812812
extensions=extensions,

tests/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pypgstac.db import PgstacDB
1616
from pypgstac.migrate import Migrate
1717
from pytest_postgresql.janitor import DatabaseJanitor
18-
from stac_fastapi.api.app import StacApi
18+
from stac_fastapi.pgstac.app import PgStacApi
1919
from stac_fastapi.api.models import (
2020
ItemCollectionUri,
2121
create_get_request_model,
@@ -181,7 +181,7 @@ def api_client(request):
181181
search_extensions, base_model=PgstacSearch
182182
)
183183

184-
api = StacApi(
184+
api = PgStacApi(
185185
settings=api_settings,
186186
extensions=application_extensions,
187187
client=CoreCrudClient(pgstac_search_model=search_post_request_model),
@@ -296,7 +296,7 @@ def api_client_no_ext():
296296
api_settings = Settings(
297297
testing=True,
298298
)
299-
return StacApi(
299+
return PgStacApi(
300300
settings=api_settings,
301301
extensions=[
302302
TransactionExtension(client=TransactionsClient(), settings=api_settings)

tests/resources/test_mgmt.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ async def test_ping_no_param(app_client):
66
"""
77
res = await app_client.get("/_mgmt/ping")
88
assert res.status_code == 200
9-
assert res.json() == {"message": "PONG"}
9+
response_json = res.json()
10+
assert response_json["message"] == "PONG"
11+
assert "database" in response_json
12+
assert response_json["database"] == "OK"

0 commit comments

Comments
 (0)