mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-04 12:06:11 +01:00
Add health check endpoint and resolve locale detection bug (#387)
* Add health check API endpoint with database connectivity * Update locale handling to fallback to default language on invalid input * Add health check endpoints for application readiness and liveness - Introduced `/api/health/readiness` endpoint to check if the application can serve requests, including database connectivity. - Introduced `/api/health/liveness` endpoint to verify if the application is running independently of external dependencies. - Updated the health check logic to streamline database connectivity checks and response handling. * Refactor health check logic --------- Co-authored-by: Julen Dixneuf <julen.d@padoa-group.com>
This commit is contained in:
@@ -71,6 +71,13 @@ Here is the current state of translation:
|
|||||||
You could also pull it from the container registry:
|
You could also pull it from the container registry:
|
||||||
```docker pull ghcr.io/spliit-app/spliit:latest```
|
```docker pull ghcr.io/spliit-app/spliit:latest```
|
||||||
|
|
||||||
|
## Health check
|
||||||
|
|
||||||
|
The application has a health check endpoint that can be used to check if the application is running and if the database is accessible.
|
||||||
|
|
||||||
|
- `GET /api/health/readiness` or `GET /api/health` - Check if the application is ready to serve requests, including database connectivity.
|
||||||
|
- `GET /api/health/liveness` - Check if the application is running, but not necessarily ready to serve requests.
|
||||||
|
|
||||||
## Opt-in features
|
## Opt-in features
|
||||||
|
|
||||||
### Expense documents
|
### Expense documents
|
||||||
|
|||||||
7
src/app/api/health/liveness/route.ts
Normal file
7
src/app/api/health/liveness/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { checkLiveness } from '@/lib/health'
|
||||||
|
|
||||||
|
// Liveness: Is the app itself healthy? (no external dependencies)
|
||||||
|
// If this fails, Kubernetes should restart the pod
|
||||||
|
export async function GET() {
|
||||||
|
return checkLiveness()
|
||||||
|
}
|
||||||
7
src/app/api/health/readiness/route.ts
Normal file
7
src/app/api/health/readiness/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { checkReadiness } from '@/lib/health'
|
||||||
|
|
||||||
|
// Readiness: Can the app serve requests? (includes all external dependencies)
|
||||||
|
// If this fails, Kubernetes should stop sending traffic but not restart
|
||||||
|
export async function GET() {
|
||||||
|
return checkReadiness()
|
||||||
|
}
|
||||||
7
src/app/api/health/route.ts
Normal file
7
src/app/api/health/route.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { checkReadiness } from '@/lib/health'
|
||||||
|
|
||||||
|
// Default health check - same as readiness (includes database check)
|
||||||
|
// This is readiness-focused for monitoring tools like uptime-kuma
|
||||||
|
export async function GET() {
|
||||||
|
return checkReadiness()
|
||||||
|
}
|
||||||
88
src/lib/health.ts
Normal file
88
src/lib/health.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
|
export interface HealthCheckStatus {
|
||||||
|
status: 'healthy' | 'unhealthy'
|
||||||
|
services?: {
|
||||||
|
database?: {
|
||||||
|
status: 'healthy' | 'unhealthy'
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDatabase(): Promise<{ status: 'healthy' | 'unhealthy'; error?: string }> {
|
||||||
|
try {
|
||||||
|
// Simple query to test database connectivity
|
||||||
|
await prisma.$queryRaw`SELECT 1`
|
||||||
|
return {
|
||||||
|
status: 'healthy'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: 'unhealthy',
|
||||||
|
error: error instanceof Error ? error.message : 'Database connection failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHealthResponse(data: HealthCheckStatus, isHealthy: boolean): Response {
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status: isHealthy ? 200 : 503,
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkReadiness(): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const databaseStatus = await checkDatabase()
|
||||||
|
|
||||||
|
const services: HealthCheckStatus['services'] = {
|
||||||
|
database: databaseStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// For readiness: healthy only if all services are healthy
|
||||||
|
const isHealthy = databaseStatus.status === 'healthy'
|
||||||
|
|
||||||
|
const healthStatus: HealthCheckStatus = {
|
||||||
|
status: isHealthy ? 'healthy' : 'unhealthy',
|
||||||
|
services
|
||||||
|
}
|
||||||
|
|
||||||
|
return createHealthResponse(healthStatus, isHealthy)
|
||||||
|
} catch (error) {
|
||||||
|
const errorStatus: HealthCheckStatus = {
|
||||||
|
status: 'unhealthy',
|
||||||
|
services: {
|
||||||
|
database: {
|
||||||
|
status: 'unhealthy',
|
||||||
|
error: error instanceof Error ? error.message : 'Readiness check failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createHealthResponse(errorStatus, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkLiveness(): Promise<Response> {
|
||||||
|
try {
|
||||||
|
// Liveness: Only check if the app process is alive
|
||||||
|
// No database or external service checks - restarting won't fix those
|
||||||
|
const healthStatus: HealthCheckStatus = {
|
||||||
|
status: 'healthy'
|
||||||
|
// No services reported - we don't check them for liveness
|
||||||
|
}
|
||||||
|
|
||||||
|
return createHealthResponse(healthStatus, true) // Always 200 for liveness
|
||||||
|
} catch (error) {
|
||||||
|
// This should rarely happen, but if it does, the app needs restart
|
||||||
|
const errorStatus: HealthCheckStatus = {
|
||||||
|
status: 'unhealthy'
|
||||||
|
}
|
||||||
|
|
||||||
|
return createHealthResponse(errorStatus, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ function getAcceptLanguageLocale(requestHeaders: Headers, locales: Locales) {
|
|||||||
try {
|
try {
|
||||||
locale = match(languages, locales, defaultLocale)
|
locale = match(languages, locales, defaultLocale)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// invalid language
|
// invalid language - fallback to default
|
||||||
|
locale = defaultLocale
|
||||||
}
|
}
|
||||||
return locale
|
return locale
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user