Skip to content

Commit

Permalink
feat: add proper health check
Browse files Browse the repository at this point in the history
  • Loading branch information
kazimanzurrashid committed Oct 1, 2022
1 parent 773fe3d commit 2ca2495
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 13 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ services:
ports:
- ${PORT}:${PORT}
healthcheck:
test: wget -q -O - --tries=1 http://localhost:${PORT}/
test: wget -q -O - --tries=1 http://localhost:${PORT}/health
interval: 15s
timeout: 1s
restart: unless-stopped
Expand Down
9 changes: 8 additions & 1 deletion k8s/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ spec:
value: production
- name: PORT
value: "3001"
livenessProbe:
httpGet:
port: 3001
path: /health
periodSeconds: 30
initialDelaySeconds: 5
timeoutSeconds: 1
readinessProbe:
httpGet:
port: 3001
path: /
path: /health/readiness
periodSeconds: 15
initialDelaySeconds: 5
timeoutSeconds: 1
Expand Down
12 changes: 5 additions & 7 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { join, resolve } from 'path';

import Pino from 'pino';
import express from 'express';
import { Pool } from 'pg';
import { Client, Pool } from 'pg';
import { container } from 'tsyringe';

import PostgreSQL from './infrastructure/postgre-sql';
import UsersController from './controllers/users-controller';
import EventsController from './controllers/events-controller';
import userRouter from './routes/users-router';
import eventsRouter from './routes/events-router';
import healthRouter from './routes/health-router';
import HealthController from './controllers/health-controller';

const logger = Pino();

Expand All @@ -23,6 +25,7 @@ const logger = Pino();
}

container.register('PGPool', { useValue: new Pool() });
container.register('PGClient', { useValue: new Client() });
container.register('Logger', { useValue: logger });
})();

Expand All @@ -38,12 +41,7 @@ const app = express()
.use(express.json())
.use('/users', userRouter(container.resolve(UsersController)))
.use('/events', eventsRouter(container.resolve(EventsController)))
.get('/', (_, res) => {
res.json({
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});
.use('/health', healthRouter(container.resolve(HealthController)));

// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
Expand Down
114 changes: 114 additions & 0 deletions src/controllers/health-controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import 'reflect-metadata';

import type { Request, Response } from 'express';
import HealthReadinessRequest from '../features/health-readiness/health-readiness-request';

import type Mediator from '../infrastructure/mediator';
import HealthController from './health-controller';

describe('HealthController', () => {
describe('liveness', () => {
describe('success', () => {
let mockedResponseStatus: jest.Mock;

beforeAll(() => {
const controller = new HealthController({} as unknown as Mediator);

const req = {};

mockedResponseStatus = jest.fn(() => ({ json: jest.fn() }));

const res = {
status: mockedResponseStatus
};

controller.liveness(
req as unknown as Request,
res as unknown as Response
);
});

it('returns http status code ok', () => {
expect(mockedResponseStatus).toHaveBeenCalledWith(200);
});
});
});

describe('readiness', () => {
describe('success', () => {
let mockedResponseStatus: jest.Mock;
let readinessRequest: HealthReadinessRequest;

beforeAll(async () => {
const mockedMediatorSend: jest.Mock = jest.fn(async () =>
Promise.resolve(true)
);

const mediator = {
send: mockedMediatorSend
};

const controller = new HealthController(
mediator as unknown as Mediator
);

const req = {};

mockedResponseStatus = jest.fn(() => ({ json: jest.fn() }));

const res = {
status: mockedResponseStatus
};

await controller.readiness(req as Request, res as unknown as Response);
readinessRequest = mockedMediatorSend.mock.calls[0][0];
});

it('uses mediator', () => {
expect(readinessRequest).toBeInstanceOf(HealthReadinessRequest);
});

it('returns http status code ok', () => {
expect(mockedResponseStatus).toHaveBeenCalledWith(200);
});
});

describe('fail', () => {
let mockedResponseStatus: jest.Mock;
let readinessRequest: HealthReadinessRequest;

beforeAll(async () => {
const mockedMediatorSend: jest.Mock = jest.fn(async () =>
Promise.resolve(false)
);

const mediator = {
send: mockedMediatorSend
};

const controller = new HealthController(
mediator as unknown as Mediator
);

const req = {};

mockedResponseStatus = jest.fn(() => ({ json: jest.fn() }));

const res = {
status: mockedResponseStatus
};

await controller.readiness(req as Request, res as unknown as Response);
readinessRequest = mockedMediatorSend.mock.calls[0][0];
});

it('uses mediator', () => {
expect(readinessRequest).toBeInstanceOf(HealthReadinessRequest);
});

it('returns http status code service unavailable', () => {
expect(mockedResponseStatus).toHaveBeenCalledWith(503);
});
});
});
});
43 changes: 43 additions & 0 deletions src/controllers/health-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { injectable } from 'tsyringe';

import type { Request, Response } from 'express';

import Mediator from '../infrastructure/mediator';
import HealthReadinessRequest from '../features/health-readiness/health-readiness-request';

@injectable()
export default class HealthController {
constructor(private readonly _mediator: Mediator) {}

liveness(_: Request, res: Response): void {
res.status(200).json({
healthy: true,
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
}

async readiness(_: Request, res: Response): Promise<void> {
const request = new HealthReadinessRequest({
ts: new Date()
});

const result = await this._mediator.send<boolean, HealthReadinessRequest>(
request
);

if (!result) {
res.status(503).json({
healthy: false,
timestamp: new Date().toISOString()
});
return;
}

res.status(200).json({
healthy: true,
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
}
}
37 changes: 37 additions & 0 deletions src/features/health-readiness/health-readiness-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { inject, injectable } from 'tsyringe';
import { Client } from 'pg';
import { Logger } from 'pino';

import handles from '../../infrastructure/handles';
import Handler from '../../infrastructure/handler';
import HealthReadinessRequest from './health-readiness-request';

@injectable()
@handles(HealthReadinessRequest)
export default class HealthReadinessHandler extends Handler<
HealthReadinessRequest,
boolean
> {
constructor(
@inject('PGClient') private readonly _pg: Client,
@inject('Logger') private readonly _logger: Logger
) {
super();
}

async handle(request: HealthReadinessRequest): Promise<boolean> {
try {
await this._pg.connect();
await this._pg.end();

return true;
} catch (e) {
this._logger.error(
{ error: e, timestamp: request.ts },
// eslint-disable-next-line i18n-text/no-en
'Failed to connect to database'
);
return false;
}
}
}
5 changes: 5 additions & 0 deletions src/features/health-readiness/health-readiness-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Request from '../../infrastructure/request';

export default class HealthReadinessRequest extends Request {
ts: Date;
}
4 changes: 3 additions & 1 deletion src/infrastructure/mediator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import UserCreateHandler from '../features/user-create/user-create-handler';
import UserDeleteHandler from '../features/user-delete/user-delete-handler';
import UserGetHandler from '../features/user-get/user-get-handler';
import EventCreateHandler from '../features/event-create/event-create-handler';
import HealthReadinessHandler from '../features/health-readiness/health-readiness-handler';

const registrations = [
UserCreateHandler,
UserDeleteHandler,
UserGetHandler,
EventCreateHandler
EventCreateHandler,
HealthReadinessHandler
].map((c) => ({
token: 'Handler',
useClass: c
Expand Down
6 changes: 3 additions & 3 deletions src/infrastructure/postgre-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export interface IPostgreSQLBase {

@injectable()
export default class PostgreSQL implements PostgreSQL {
constructor(@inject('PGPool') private readonly _pool: Pool) {}
constructor(@inject('PGPool') private readonly _pg: Pool) {}

async query(sql: string, values?: unknown[]): Promise<QueryResult> {
return this._pool.query(sql, values);
return this._pg.query(sql, values);
}

async unit<T>(func: (client: IPostgreSQLBase) => Promise<T>): Promise<T> {
const client = await this._pool.connect();
const client = await this._pg.connect();

try {
return await func(client);
Expand Down
18 changes: 18 additions & 0 deletions src/routes/health-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Request, Response, Router } from 'express';
import express from 'express';

import type HealthController from '../controllers/health-controller';

export default function healthRouter(controller: HealthController): Router {
const router = express.Router();

router.get('/', (req: Request, res: Response) =>
controller.liveness(req, res)
);

router.get('/readiness', async (req: Request, res: Response) =>
controller.readiness(req, res)
);

return router;
}

0 comments on commit 2ca2495

Please sign in to comment.