Skip to content

Custom indicator api and health service#6

Closed
saadmoumou wants to merge 31 commits intomasterfrom
custom-indicator-api-and-health-service
Closed

Custom indicator api and health service#6
saadmoumou wants to merge 31 commits intomasterfrom
custom-indicator-api-and-health-service

Conversation

@saadmoumou
Copy link
Copy Markdown

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

@saadmoumou saadmoumou requested review from a team and Copilot April 2, 2026 11:31
@saadmoumou saadmoumou closed this Apr 2, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a reusable @ciscode/health-kit NestJS module that exposes liveness/readiness HTTP endpoints and a small indicator API (built-in indicators + custom indicator helpers) for composing health checks in consumer applications.

Changes:

  • Introduces HealthKitModule, HealthService, and a /live + /ready controller factory.
  • Adds built-in health indicators (Postgres, Redis, Mongo, HTTP) plus custom-indicator helpers (createIndicator, BaseHealthIndicator, @HealthIndicator decorator).
  • Updates tooling/config for the new package (path aliases, Jest mappers, ESLint config, lint-staged, Husky hook, build tsconfig, package rename).

Reviewed changes

Copilot reviewed 27 out of 30 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tsconfig.json Adds @interfaces/* and @indicators/* path aliases.
tsconfig.build.json Adjusts build output to CommonJS/Node module resolution for dist.
src/services/health.service.ts New orchestrator for running indicators and returning aggregate status.
src/services/health.service.spec.ts Unit tests for HealthService behavior and exception handling.
src/interfaces/health-indicator.interface.ts Defines indicator contracts and result types.
src/indicators/redis.indicator.ts Adds Redis indicator implementation.
src/indicators/redis.indicator.spec.ts Tests Redis indicator success/error/timeout paths.
src/indicators/postgres.indicator.ts Adds Postgres indicator implementation.
src/indicators/postgres.indicator.spec.ts Tests Postgres indicator success/error/timeout paths.
src/indicators/mongo.indicator.ts Adds Mongo indicator implementation.
src/indicators/mongo.indicator.spec.ts Tests Mongo indicator success/error/timeout paths.
src/indicators/http.indicator.ts Adds HTTP indicator using fetch + AbortController timeout.
src/indicators/http.indicator.spec.ts Tests HTTP indicator for 2xx/non-2xx/errors/timeout.
src/indicators/create-indicator.ts Adds functional factory for creating ad-hoc indicators.
src/indicators/create-indicator.spec.ts Tests createIndicator behavior including timeout/error cases.
src/indicators/base.indicator.ts Adds base class helper for DI-driven indicator implementations.
src/indicators/base.indicator.spec.ts Tests BaseHealthIndicator.result() helper behavior.
src/index.ts Updates public exports to expose health-kit module/services/indicators/decorator/types.
src/health-kit.module.ts Adds dynamic module wiring + auto-registration of DI indicators via metadata.
src/decorators/health-indicator.decorator.ts Adds @HealthIndicator scope metadata decorator.
src/decorators/health-indicator.decorator.spec.ts Tests decorator metadata behavior.
src/controllers/health.controller.ts Adds controller factory for configurable health routes.
src/controllers/health.controller.spec.ts Tests controller behavior (200 vs 503).
package.json Renames package and updates description/peers.
package-lock.json Updates lockfile for rename and dependency changes.
lint-staged.config.mjs Adds lint-staged configuration for formatting/linting staged files.
jest.config.ts Adds Jest moduleNameMapper entries for new path aliases.
eslint.config.mjs Reworks ESLint config (ignores, TS parser setup, import ordering rules).
eslint.config.js Removes old ESLint config file.
.husky/pre-commit Changes pre-commit hook to run lint-staged (but drops Husky header).

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged No newline at end of file
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Husky hook header was removed. Without the shebang and sourcing ./_/husky.sh, the hook can fail in some environments (e.g., PATH not set to include local node binaries). Restore the standard Husky pre-commit header like the pre-push hook uses.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +22
* Built-in health indicator for a Redis dependency.
*
* Sends a `PING` command and expects a `"PONG"` response.
* Returns `"down"` if the command fails or exceeds the configured timeout.
*
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc says the PING expects a "PONG" response, but the implementation treats any resolved ping() result as healthy. Either validate the returned value (e.g., equals "PONG") or adjust the documentation to match the behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +46
async check(): Promise<HealthIndicatorResult> {
try {
await Promise.race([this.client.ping(), this._timeout()]);
return { name: "redis", status: "up" };
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses Promise.race([ping(), _timeout()]) but the _timeout() sets a timer that is never cleared when ping() resolves quickly. This leaves extra timers queued (can keep the event loop alive and adds overhead under load). Consider creating the timeout timer in check() and clearTimeout it in a finally block once the race settles.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +45
async check(): Promise<HealthIndicatorResult> {
try {
await Promise.race([this.client.query("SELECT 1"), this._timeout()]);
return { name: "postgres", status: "up" };
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same timer issue as Redis: _timeout() schedules a setTimeout that isn't cleared when the query resolves first. Refactor to clear the timer in finally to avoid unnecessary timers keeping the event loop active.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +55
async check(): Promise<HealthIndicatorResult> {
try {
await Promise.race([this.db.command({ ping: 1 }), this._timeout()]);
return { name: "mongo", status: "up" };
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same timer issue as Redis/Postgres: _timeout() schedules a setTimeout that isn't cleared if the ping command resolves first. Clearing the timer once the race completes avoids leaking timers and reduces overhead.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +13
* The check function may return:
* - `false` → indicator reports `"down"`
* - `true` or `void` → indicator reports `"up"`
* - throws → indicator reports `"down"` with the error message
*
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says “throws → indicator reports "down" with the error message”, but this implementation lets rejections/timeouts propagate (the returned promise rejects) instead of returning a HealthIndicatorResult. Either update the docs (and potentially the IHealthIndicator contract) or catch errors/timeouts here and return { name, status: 'down', message } for consistency with the other indicators.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +44
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`${name} timed out after ${timeout}ms`)), timeout),
);

return Promise.race([run(), timeoutPromise]);
},
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout promise uses setTimeout but the timer is never cleared when run() completes first. While Promise.race attaches handlers, the pending timer still adds overhead and can keep the event loop alive. Consider storing the timeout handle and clearTimeout it in a finally block once the race settles.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +17
* Runs all registered indicators concurrently via `Promise.allSettled` so a
* single slow/failing dependency never blocks the others.
* Returns `status: "ok"` only when every indicator reports `"up"`.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims Promise.allSettled ensures a single slow dependency “never blocks the others”, but allSettled still waits for all indicators to settle before returning (so a slow indicator will delay the overall health response). Consider rewording to clarify it avoids short-circuiting on failure, not latency impact.

Suggested change
* Runs all registered indicators concurrently via `Promise.allSettled` so a
* single slow/failing dependency never blocks the others.
* Returns `status: "ok"` only when every indicator reports `"up"`.
* Runs all registered indicators concurrently via `Promise.allSettled`, so a
* failure in one indicator does not short‑circuit or prevent others from running.
* The overall check still waits for all indicators to settle, and returns
* `status: "ok"` only when every indicator reports `"up"`.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +7
class SomeIndicator {}
class AnotherIndicator {}
class UndecotratedIndicator {}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in class name UndecotratedIndicatorUndecoratedIndicator. This is user-facing in test names/examples and makes the intent harder to read.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +35
* import { HealthModule } from '@ciscode/health-kit';
* import { MongoHealthIndicator } from '@ciscode/health-kit';
*
* @Module({
* imports: [
* HealthModule.register({
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module JSDoc example uses HealthModule.register(...), but the exported class is HealthKitModule (and index.ts exports HealthKitModule). Update the example imports/usages to match the actual public API so consumers can copy/paste it successfully.

Suggested change
* import { HealthModule } from '@ciscode/health-kit';
* import { MongoHealthIndicator } from '@ciscode/health-kit';
*
* @Module({
* imports: [
* HealthModule.register({
* import { HealthKitModule, MongoHealthIndicator } from '@ciscode/health-kit';
*
* @Module({
* imports: [
* HealthKitModule.register({

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants