See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

install and configure @adonisjs/health with SQLite + ClickHouse checks

Adds canonical AdonisJS health-check endpoints using @adonisjs/core/health
(which re-exports @adonisjs/health, already a transitive dependency):

- start/health.ts: registers DbCheck(sqlite) + custom ClickHouseCheck
(uses store.client.ping() from @clickhouse/client)
- app/controllers/health_checks_controller.ts: live + ready actions
- GET /health/live — liveness probe, always open
- GET /health/ready — readiness probe; protected by x-monitoring-secret
header when HEALTH_CHECK_TOKEN env var is set
- 8 new functional tests (169 total, was 161)
- .env.example: documents optional HEALTH_CHECK_TOKEN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+233 -1
+1
apps/web/.adonisjs/server/controllers.ts
··· 4 4 */ 5 5 6 6 export const controllers = { 7 + HealthChecks: () => import('#controllers/health_checks_controller'), 7 8 Landing: () => import('#controllers/landing_controller'), 8 9 Profile: () => import('#controllers/profile_controller'), 9 10 Search: () => import('#controllers/search_controller'),
+6 -1
apps/web/.env.example
··· 28 28 JETSTREAM_URL=wss://jetstream2.us-east.bsky.network/subscribe 29 29 30 30 # Backfill 31 - BACKFILL_MAX_POSTS=10000 31 + BACKFILL_MAX_POSTS=10000 32 + 33 + # Health checks 34 + # Optional: when set, GET /health/ready requires the header 35 + # x-monitoring-secret: <value> — leave unset to keep the endpoint open. 36 + # HEALTH_CHECK_TOKEN=
+28
apps/web/app/controllers/health_checks_controller.ts
··· 1 + import { healthChecks } from '#start/health' 2 + import type { HttpContext } from '@adonisjs/core/http' 3 + 4 + export default class HealthChecksController { 5 + /** 6 + * Liveness probe: returns 200 if the HTTP process is alive. 7 + * Does not check external dependencies — suitable for container 8 + * restart decisions (e.g. Kubernetes liveness probe). 9 + */ 10 + async live({ response }: HttpContext) { 11 + return response.ok({ status: 'ok' }) 12 + } 13 + 14 + /** 15 + * Readiness probe: runs all registered health checks and returns 16 + * the detailed JSON report. Returns 200 when healthy, 503 otherwise. 17 + * Protected by the x-monitoring-secret header in production. 18 + */ 19 + async ready({ response }: HttpContext) { 20 + const report = await healthChecks.run() 21 + 22 + if (report.isHealthy) { 23 + return response.ok(report) 24 + } 25 + 26 + return response.serviceUnavailable(report) 27 + } 28 + }
+45
apps/web/start/health.ts
··· 1 + /* 2 + |-------------------------------------------------------------------------- 3 + | Health checks 4 + |-------------------------------------------------------------------------- 5 + | 6 + | Defines the health checks that are executed on the /health/ready endpoint. 7 + | The liveness probe (/health/live) does not run these — it only verifies 8 + | the HTTP process is alive. 9 + | 10 + */ 11 + 12 + import { HealthChecks } from '@adonisjs/core/health' 13 + import db from '@adonisjs/lucid/services/db' 14 + import { DbCheck } from '@adonisjs/lucid/database' 15 + import { Result, BaseCheck } from '@adonisjs/core/health' 16 + import type { HealthCheckResult } from '@adonisjs/core/types/health' 17 + import { ClickHouseStore } from '@skystar/clickhouse' 18 + import app from '@adonisjs/core/services/app' 19 + 20 + /** 21 + * Custom health check that pings the ClickHouse server via the client's 22 + * built-in /ping endpoint. Only runs when ClickHouseStore is registered 23 + * (CLICKHOUSE_URL is configured). 24 + */ 25 + export class ClickHouseCheck extends BaseCheck { 26 + name = 'ClickHouse connection' 27 + 28 + async run(): Promise<HealthCheckResult> { 29 + try { 30 + const store = await app.container.make(ClickHouseStore) 31 + const result = await store.client.ping() 32 + if (result.success) { 33 + return Result.ok('Successfully connected to ClickHouse server') 34 + } 35 + return Result.failed(`ClickHouse ping failed: ${result.error.message}`) 36 + } catch (error) { 37 + return Result.failed(`ClickHouse check threw: ${(error as Error).message}`) 38 + } 39 + } 40 + } 41 + 42 + export const healthChecks = new HealthChecks().register([ 43 + new DbCheck(db.connection()), 44 + new ClickHouseCheck(), 45 + ])
+31
apps/web/start/routes.ts
··· 12 12 const LandingController = () => import('#controllers/landing_controller') 13 13 const SearchController = () => import('#controllers/search_controller') 14 14 const ProfileController = () => import('#controllers/profile_controller') 15 + const HealthChecksController = () => import('#controllers/health_checks_controller') 15 16 16 17 // --------------------------------------------------------------------------- 17 18 // Landing ··· 44 45 router.get('/profile/:handle/likes', [ProfileController, 'showLikes']).as('profile.likes') 45 46 46 47 router.get('/profile/:handle/reposts', [ProfileController, 'showReposts']).as('profile.reposts') 48 + 49 + // --------------------------------------------------------------------------- 50 + // Health checks 51 + // --------------------------------------------------------------------------- 52 + 53 + /** 54 + * Liveness probe: always returns 200 if the HTTP process is running. 55 + * No auth required — this is intentionally lightweight and public. 56 + */ 57 + router.get('/health/live', [HealthChecksController, 'live']).as('health.live') 58 + 59 + /** 60 + * Readiness probe: runs all health checks (SQLite + ClickHouse). 61 + * Protected by x-monitoring-secret header when HEALTH_CHECK_TOKEN is set. 62 + * Returns 200 when healthy, 503 when degraded. 63 + */ 64 + router 65 + .get('/health/ready', [HealthChecksController, 'ready']) 66 + .use(async ({ request, response }, next) => { 67 + const token = process.env.HEALTH_CHECK_TOKEN 68 + if (!token) { 69 + // No token configured — endpoint is open (suitable for local / private networks) 70 + return next() 71 + } 72 + if (request.header('x-monitoring-secret') === token) { 73 + return next() 74 + } 75 + return response.unauthorized({ message: 'Unauthorized' }) 76 + }) 77 + .as('health.ready')
+122
apps/web/tests/functional/health.spec.ts
··· 1 + /** 2 + * Functional tests for the health check endpoints. 3 + * 4 + * /health/live — liveness probe, always open, no dependencies 5 + * /health/ready — readiness probe, runs DbCheck + ClickHouseCheck, 6 + * protected by x-monitoring-secret when HEALTH_CHECK_TOKEN is set 7 + */ 8 + import { test } from '@japa/runner' 9 + 10 + test.group('Health checks', () => { 11 + // No withGlobalTransaction — health checks run their own DB queries and don't 12 + // need transaction isolation. The migration-created schema is read-only here. 13 + 14 + // ------------------------------------------------------------------------- 15 + // Liveness probe 16 + // ------------------------------------------------------------------------- 17 + 18 + test('GET /health/live returns 200', async ({ client }) => { 19 + const response = await client.get('/health/live') 20 + response.assertStatus(200) 21 + }) 22 + 23 + test('GET /health/live returns JSON with status ok', async ({ client, assert }) => { 24 + const response = await client.get('/health/live') 25 + response.assertStatus(200) 26 + assert.equal(response.body().status, 'ok') 27 + }) 28 + 29 + // ------------------------------------------------------------------------- 30 + // Readiness probe (no token configured in test env) 31 + // ------------------------------------------------------------------------- 32 + 33 + test('GET /health/ready returns 200 when no HEALTH_CHECK_TOKEN is set', async ({ 34 + client, 35 + assert, 36 + }) => { 37 + // In the test environment HEALTH_CHECK_TOKEN is not set, so the endpoint 38 + // is open and should return 200 (SQLite is available; ClickHouse may not be). 39 + delete process.env.HEALTH_CHECK_TOKEN 40 + const response = await client.get('/health/ready') 41 + // The HTTP server is healthy (SQLite works); ClickHouse may warn/fail. 42 + // Accept both 200 (fully healthy) and 503 (degraded but responding). 43 + assert.include([200, 503], response.status()) 44 + const body = response.body() 45 + assert.property(body, 'isHealthy') 46 + assert.property(body, 'checks') 47 + assert.isArray(body.checks) 48 + }) 49 + 50 + test('GET /health/ready includes the SQLite database check', async ({ client, assert }) => { 51 + delete process.env.HEALTH_CHECK_TOKEN 52 + const response = await client.get('/health/ready') 53 + const checks: Array<{ name: string; status: string }> = response.body().checks 54 + const dbCheck = checks.find((c) => c.name === 'Database health check (sqlite)') 55 + assert.isDefined( 56 + dbCheck, 57 + 'expected a check named "Database health check (sqlite)" in the response' 58 + ) 59 + assert.equal(dbCheck!.status, 'ok', 'SQLite connection check should pass') 60 + }) 61 + 62 + test('GET /health/ready includes a check named "ClickHouse connection"', async ({ 63 + client, 64 + assert, 65 + }) => { 66 + delete process.env.HEALTH_CHECK_TOKEN 67 + const response = await client.get('/health/ready') 68 + const checks: Array<{ name: string }> = response.body().checks 69 + const chCheck = checks.find((c) => c.name === 'ClickHouse connection') 70 + assert.isDefined(chCheck, 'expected a check named "ClickHouse connection" in the response') 71 + }) 72 + 73 + // ------------------------------------------------------------------------- 74 + // Auth: HEALTH_CHECK_TOKEN enforcement 75 + // ------------------------------------------------------------------------- 76 + 77 + test('GET /health/ready returns 401 when token required but header missing', async ({ 78 + client, 79 + assert, 80 + }) => { 81 + process.env.HEALTH_CHECK_TOKEN = 'test-secret-token' 82 + try { 83 + const response = await client.get('/health/ready') 84 + response.assertStatus(401) 85 + assert.equal(response.body().message, 'Unauthorized') 86 + } finally { 87 + delete process.env.HEALTH_CHECK_TOKEN 88 + } 89 + }) 90 + 91 + test('GET /health/ready returns 401 when token required and wrong header sent', async ({ 92 + client, 93 + assert, 94 + }) => { 95 + process.env.HEALTH_CHECK_TOKEN = 'test-secret-token' 96 + try { 97 + const response = await client 98 + .get('/health/ready') 99 + .header('x-monitoring-secret', 'wrong-token') 100 + response.assertStatus(401) 101 + assert.equal(response.body().message, 'Unauthorized') 102 + } finally { 103 + delete process.env.HEALTH_CHECK_TOKEN 104 + } 105 + }) 106 + 107 + test('GET /health/ready returns 200 or 503 when correct token is sent', async ({ 108 + client, 109 + assert, 110 + }) => { 111 + process.env.HEALTH_CHECK_TOKEN = 'test-secret-token' 112 + try { 113 + const response = await client 114 + .get('/health/ready') 115 + .header('x-monitoring-secret', 'test-secret-token') 116 + assert.include([200, 503], response.status()) 117 + assert.property(response.body(), 'isHealthy') 118 + } finally { 119 + delete process.env.HEALTH_CHECK_TOKEN 120 + } 121 + }) 122 + })