feat(api): add a /health endpoint for monitoring - #8
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mongo_dart 0.7.4 implements `count` over the legacy OP_QUERY opcode, which MongoDB removed in 5.1, so `/health` reported the database unreachable on every 5.1-or-newer server. Only the 5.0.6 half of the CI matrix was green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new public, network-facing endpoint that probes the database and must avoid leaking connection secrets, so it warrants final human sign-off.
Pull request overview
This PR adds a public GET /health endpoint to the in_pub Dart pub server for monitoring. The handler runs a real round-trip probe against the metadata store (bounded by a configurable timeout) and returns 200 with {"status":"ok",...} when the store answers, or 503 with a type-only error when it does not. A new MetaStore.checkHealth() extension point is introduced with a concrete default (queryPackages), overridden in MongoStore with a lightweight findOne projection to avoid the removed OP_QUERY count path on MongoDB 5.1+. The response is never cached and never reveals repository contents, keeping the endpoint safe to leave public.
Changes:
- New
/healthhandler inAppplus ahealthProbeTimeoutfield, generated route wiring inapp.g.dart, and aMetaStore.checkHealth()contract with aMongoStore.findOneoverride. - Extensive tests (
health_test.dart, plus a live-Mongohealth checkgroup inunpub_test.dart) covering healthy/failing/hanging stores, secret non-leakage, response shape, caching header, and auth-on/off parity. - Documentation updates in
README.md,CHANGELOG.md, andCLAUDE.md.
File summaries
| File | Description |
|---|---|
| unpub/lib/src/app.dart | Adds healthProbeTimeout field and the /health handler with status/body/error handling. |
| unpub/lib/src/app.g.dart | Generated router registration for GET /health. |
| unpub/lib/src/meta_store.dart | Adds concrete checkHealth() default so third-party stores keep compiling. |
| unpub/lib/src/mongo_store.dart | Overrides checkHealth() with a findOne _id projection to avoid OP_QUERY. |
| unpub/test/health_test.dart | New end-to-end tests for the endpoint's behavior and safety. |
| unpub/test/unpub_test.dart | Live-Mongo probe tests for open vs closed connections. |
| unpub/README.md | Documents the health check endpoint and its rationale. |
| unpub/CHANGELOG.md | Records the new endpoint under an Unreleased section. |
| CLAUDE.md | Adds "Before handing work over" verification guidance. |
Review details
Files not reviewed (1)
- unpub/lib/src/app.g.dart: Generated file
- Files reviewed: 8/9 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // the database unreachable on exactly the versions this repository is | ||
| // headed for. `findOne` picks the modern OP_MSG path where the server has | ||
| // one and falls back on the legacy path where it does not. | ||
| await db.collection(packageCollection).findOne(where.fields(['_id'])); |
Что изменилось
Появился
GET /health— endpoint для мониторинга. 200, пока сервер может обратиться к хранилищу метаданных, 503, когда не может, так что проверка, читающая один только статус-код, уже корректна. Ответ не кэшируется.{ "status": "ok", "checks": { "database": { "status": "ok", "latencyMs": 4 } } }{ "status": "error", "checks": { "database": { "status": "error", "error": "TimeoutException" } } }Как устроено
db.isConnected: база, переставшая отвечать, оставляет сокет, который по-прежнему считает себя открытым, поэтому только вернувшийся ответ доказывает, что хранилище пригодно.findOneс проекцией одного_id, а неcount, который читал бы ноль документов и был бы очевидным выбором: mongo_dart 0.7.4 реализуетcountчерез legacy-опкод OP_QUERY, убранный в MongoDB 5.1, так что на 5.1+ проба всегда отвечала бы «база недоступна» (первый прогон CI это и показал на Mongo 7).findOneвыбирает современный OP_MSG-путь, где сервер его поддерживает, и откатывается на legacy, где нет.healthProbeTimeout, 5 с по умолчанию). Без него подвисшая Mongo даёт не упавшую проверку, а повисший запрос, который мониторинг прочитает как сетевую проблему, а не как проблему базы.MetaStore.checkHealth()добавлен с рабочей реализацией по умолчанию (queryPackages(size: 1)), а не абстрактным членом:MetaStoreдокументирован в README как точка расширения, и сторонняя реализация не должна перестать компилироваться из-за пробы, которой не просила.MongoStoreпереопределяет её своей пробой (см. пункт ниже), результат в обоих случаях выбрасывается.RouteKind.publicвclassifyRoute— теперь на нём есть хендлер.Про статистику
В ответе намеренно нет ни числа пакетов, ни версии сервера, ни uptime — обсуждали и решили не отдавать. Маршрут публичный, гейт для публичных маршрутов не резолвит credential, так что хендлер в принципе не знает, кто спрашивает, и ответ один для всех. Счётчик пакетов на таком endpoint — это число, которое любой может опрашивать раз в несколько секунд, а счётчик, снимаемый по расписанию, — это лента публикаций, а не статус.
Проверено
unpub:dart test— 641 тест зелёный на обеих версиях матрицы CI, MongoDB 5.0.6 и 7.0.40;dart analyzeиdart format --set-exit-if-changedчисто.unpub/test/health_test.dart(10): 200 при отвечающем хранилище, 503 при падении и при зависании, ошибка только типом (с проверкой, что пароль из connection string не попал в тело),Cache-Control: no-store, точный набор ключей ответа на всех уровнях, ответ без сессии и тот же самый ответ при включённой авторизации. Плюс группаhealth checkвunpub_test.dartпротив живой Mongo: открытое соединение пробу проходит, закрытое — бросает.