Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Healthz to api and frontend #34

Merged
merged 9 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ To understand what is planned you can read and ask questions here:
To install correct node version, we recommend that you use [nvm](https://github.com/nvm-sh/nvm). If you have `nvm` installed you can run `nvm install && nvm use` to automatically use the correct node version. The version is detected from the [.nvmrc](./.nvmrc).

If you do not have a code editor setup, we recommend that you use [Visual Studio Code](https://code.visualstudio.com/) to get started. It is very beginner friendly and you can move on to something else down the road if you want to.

## Health endpoints

Both api and frontend have health endpoints that can be used to check if the service is up and running.

See the respective descriptions in the [api](./api/README.md#health-endpoint) and [frontend](./frontend/README.md#health-endpoint) READMEs.
4 changes: 4 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ npx prisma studio
### Logging configuration

The configuration is received from the `logger.config.ts` file at the root of the project. Adjust the file parameters to control the logger behaviour.

## Health endpoint

Endpoint available at `/healthz` path. Provides response if api is currently running and prisma (orm) is able to execute queries.
126 changes: 98 additions & 28 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"gql-query-builder": "^3.8.0",
"graphql-request": "^5.2.0",
"prisma": "^4.11.0",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"vitest-mock-extended": "^1.1.3"
}
}
11 changes: 11 additions & 0 deletions api/src/__mocks__/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client';
import { beforeEach } from 'vitest';
import { mockDeep, mockReset } from 'vitest-mock-extended';

const prisma = mockDeep<PrismaClient>();

beforeEach(() => {
mockReset(prisma);
});

export default prisma;
35 changes: 29 additions & 6 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,42 @@ import type { Express } from 'express';
import express from 'express';
import expressPlayground from 'graphql-playground-middleware-express';
import { getChildLogger } from './logger';
import prisma from './database';

const logger = getChildLogger({ msgPrefix: 'APP' });
const startupTime = new Date();

export const createApp = (): Express => {
logger.debug('Creating app');
const app = express();

app.get('/', expressPlayground({
endpoint: '/api/graphql',
settings: {
'editor.theme': 'light',
},
}));
app.get('/healthz', async (_req, res) => {
try {
// TODO: after migration to postgres, do SELECT 1
await prisma.user.findFirst();
} catch (error) {
return res.status(500).json({
status: 'DB failed initialization check',
time: new Date(),
startupTime,
});
}
return res.json({
status: 'healthy',
time: new Date(),
startupTime,
});
});

app.get(
'/',
expressPlayground({
endpoint: '/api/graphql',
settings: {
'editor.theme': 'light',
},
}),
);

return app;
};
28 changes: 27 additions & 1 deletion api/tests/auxilary.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { test, expect } from 'vitest';
import {
test, expect, vi, describe,
} from 'vitest';
import { getJwtSecret, getJwtExpirationPeriod } from '../src/env/getters';
import { restoreEnvAfterEach } from './helpers/env';
import { ctx } from './helpers/server';
import prisma from '../src/__mocks__/database';

restoreEnvAfterEach();

Expand Down Expand Up @@ -34,3 +38,25 @@ test('Env: jwt expiration in seconds format', async () => {
process.env.JWT_EXPIRATION_PERIOD = '3600';
expect(getJwtExpirationPeriod()).toBe('1h');
});

describe('Healthz', () => {
vi.mock('../src/database');

test('healthz: returns 200', async () => {
prisma.user.findFirst.mockResolvedValueOnce({ id: '1', username: 'asdf', password: 'asdf' });
const url = `${ctx.baseUrl}/healthz`;
const res = await fetch(url);
expect(res.status).toBe(200);
const json: any = await res.json();
expect(json.status).toBe('healthy');
});

test('healthz: returns 500', async () => {
prisma.user.findFirst.mockRejectedValueOnce(new Error('test'));
const url = `${ctx.baseUrl}/healthz`;
const res = await fetch(url);
expect(res.status).toBe(500);
const json: any = await res.json();
expect(json.status).toBe('DB failed initialization check');
});
});
9 changes: 7 additions & 2 deletions api/tests/helpers/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { createApp } from '../../src/app';

interface TestContext {
client: GraphQLClient;
baseUrl: string;
}

function getGraphqlTestContext() {
let serverInstance: Server | null = null;
let baseUrl: string | null = null;
return {
async before() {
const app = createApp();
Expand All @@ -19,10 +21,12 @@ function getGraphqlTestContext() {
throw new Error('Unexpected server address format');
}
const { port } = serverAddress;
return new GraphQLClient(`http://0.0.0.0:${port}/graphql`);
baseUrl = `http://0.0.0.0:${port}`;
return { client: new GraphQLClient(`${baseUrl}/graphql`), baseUrl };
},
async after() {
serverInstance?.close();
baseUrl = null;
},
};
}
Expand All @@ -31,8 +35,9 @@ function createTestContext(): TestContext {
const context = {} as TestContext;
const graphqlTestContext = getGraphqlTestContext();
beforeEach(async () => {
const client = await graphqlTestContext.before();
const { client, baseUrl } = await graphqlTestContext.before();
context.client = client;
context.baseUrl = baseUrl;
});
afterEach(async () => {
await graphqlTestContext.after();
Expand Down
4 changes: 4 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ Please refer to the root readme to learn more about general development setup.
## Production

You can build application for production using `npm run build` and then locally preview production build via `npm run preview`.

## Health endpoint

Endpoint available at `/healthz` path. Provides response if frontend is currently running.
9 changes: 9 additions & 0 deletions frontend/server/api/healthz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const startupTime = new Date()

export default eventHandler(() => {
return {
status: 'healthy',
time: new Date(),
startupTime
}
})