Skip to content

Commit

Permalink
chore: Add health check aware of infrastructure dependencies and conf…
Browse files Browse the repository at this point in the history
…igure aws load-balancer accordingly
  • Loading branch information
alepefe committed Dec 10, 2024
1 parent 43f1156 commit d189592
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 13 deletions.
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/terminus": "^10.2.3",
"@nestjs/typeorm": "^10.0.2",
"@ts-rest/nest": "^3.51.0",
"@types/multer": "1.4.12",
Expand Down
26 changes: 21 additions & 5 deletions api/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import {
HealthCheck,
HealthCheckService,
TypeOrmHealthIndicator,
} from '@nestjs/terminus';
import { ControllerResponse } from '@api/types/controller-response.type';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
constructor(
private readonly health: HealthCheckService,
private readonly db: TypeOrmHealthIndicator,
) {}

@Get()
getHello(): string {
return this.appService.getHello();
@Get('/')
public root(): ControllerResponse {
return null;
}

@Get('/health')
@HealthCheck({ noCache: true })
public checkHealth(): ControllerResponse {
return this.health.check([
async () => this.db.pingCheck('database', { timeout: 1500 }),
]);
}
}
4 changes: 4 additions & 0 deletions api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ import * as path from 'path';
import { CountriesModule } from '@api/modules/countries/countries.module';
import { ProjectsModule } from '@api/modules/projects/projects.module';
import { CustomProjectsModule } from '@api/modules/custom-projects/custom-projects.module';
import { TerminusModule } from '@nestjs/terminus';

const NODE_ENV = process.env.NODE_ENV;

@Module({
imports: [
TerminusModule.forRoot({ logger: NODE_ENV === 'test' ? false : true }),
TsRestModule.register({
validateRequestQuery: true,
validateRequestBody: true,
Expand Down
46 changes: 46 additions & 0 deletions api/test/integration/health/health.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TestManager } from 'api/test/utils/test-manager';

describe('Health', () => {
let testManager: TestManager;

beforeAll(async () => {
testManager = await TestManager.createTestManager({ logger: false });
});

beforeEach(async () => {
await testManager.clearDatabase();
});

afterAll(async () => {
await testManager.close();
});

it("Should return the app's health status: OK", async () => {
// Given
// The app is running

// When
const { statusCode, body } = await testManager.request().get('/health');

// Then
expect(statusCode).toBe(200);
expect(body).toStrictEqual({
status: 'ok',
info: { database: { status: 'up' } },
error: {},
details: { database: { status: 'up' } },
});
});

it("Should return the app's health status: Unavailable", async () => {
// Given
// The app is running and the database becomes unavailable
await testManager.dataSource.destroy();

// When
const { statusCode } = await testManager.request().get('/health');

// Then
expect(statusCode).toBe(503);
});
});
8 changes: 6 additions & 2 deletions api/test/utils/test-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppModule } from '@api/app.module';
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { INestApplication, Logger } from '@nestjs/common';
import { DataSource } from 'typeorm';

import { logUserIn } from './user.auth';
Expand Down Expand Up @@ -50,7 +50,7 @@ export class TestManager {
this.moduleFixture = moduleFixture;
}

static async createTestManager() {
static async createTestManager(options: { logger?: Logger | false } = {}) {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
Expand All @@ -59,6 +59,10 @@ export class TestManager {
.compile();
const dataSource = moduleFixture.get<DataSource>(getDataSourceToken());
const testApp = moduleFixture.createNestApplication();
if (options.logger !== undefined) {
// Has to be called before init. Otherwise it has no effect.
testApp.useLogger(options.logger);
}
// TODO: Add global validation or App level Zod when decided what to use
//testApp.useGlobalPipes(new ValidationPipe());
await testApp.init();
Expand Down
23 changes: 23 additions & 0 deletions client/src/app/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export async function GET() {
try {
const res = await fetch(process.env.NEXT_PUBLIC_API_URL + "/health", {
cache: "no-cache",
});
if (res.status === 200) {
return new Response("OK", {
status: 200,
headers: { "Cache-Control": "max-age=5, must-revalidate" },
});
} else {
return new Response("KO", {
status: 503,
headers: { "Cache-Control": "max-age=5, must-revalidate" },
});
}
} catch (error) {
return new Response("KO", {
status: 503,
headers: { "Cache-Control": "max-age=5, must-revalidate" },
});
}
}
5 changes: 5 additions & 0 deletions infrastructure/modules/beanstalk/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ locals {
name = "SSLCertificateArns"
value = var.acm_certificate.arn
},
{
namespace = "aws:elasticbeanstalk:environment:process:default"
name = "HealthCheckPath"
value = "/health"
}
]
}

Expand Down
Loading

0 comments on commit d189592

Please sign in to comment.