From 633d54a19662626d9e38a18e15b82c6c9c265f66 Mon Sep 17 00:00:00 2001 From: Guillaume Quittet Date: Wed, 28 Aug 2024 00:33:02 +0200 Subject: [PATCH] docs: split documentation in different .md files --- README.md | 384 ++------------------------------------------- docs/API.md | 43 +++++ docs/DOCKER.md | 51 ++++++ docs/EXAMPLE.md | 223 ++++++++++++++++++++++++++ docs/KUBERNETES.md | 43 +++++ 5 files changed, 373 insertions(+), 371 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/DOCKER.md create mode 100644 docs/EXAMPLE.md create mode 100644 docs/KUBERNETES.md diff --git a/README.md b/README.md index 5b48e44..4fe9d9d 100644 --- a/README.md +++ b/README.md @@ -33,26 +33,14 @@ - [Requirements](#requirements) - [Installation](#installation) - [NPM](#npm) - - [Yarn](#yarn) - [PNPM](#pnpm) + - [YARN](#yarn) - [Endpoint](#endpoint) - [/live](#live) - [/ready](#ready) - [Example](#example) - - [ExpressJS](#expressjs) - - [Fastify](#fastify) - - [Koa](#koa) - - [HTTP Server](#http-server) -- [API](#api) - - [GracefulServer](#gracefulserver) - - [IGracefulServerOptions](#igracefulserveroptions) - - [GracefulServer Instance](#gracefulserver-instance) +- [API Doc](#api-doc) - [Integration with Docker](#integration-with-docker) - - [HEALTH CHECK in Dockerfile](#health-check-in-dockerfile) - - [Content of _healthcheck.js_](#content-of-healthcheckjs) - - [Example of Dockerfile](#example-of-dockerfile) - - [POC level](#poc-level) - - [Company level](#company-level) - [Integration with Kubernetes](#integration-with-kubernetes) - [Thanks](#thanks) - [Sponsors](#sponsors) @@ -94,6 +82,12 @@ npm install --save @gquittet/graceful-server pnpm add @gquittet/graceful-server ``` +### YARN + +``` +yarn add @gquittet/graceful-server +``` + ## Endpoint Below you can find the default endpoint but you can setup or disable them. To do that, check out the [Options](#options) part. @@ -128,371 +122,19 @@ The endpoint responds: ## Example -### ExpressJS - -The library works with the default HTTP NodeJS object. So, when you're using Express you can't pass -directly the `app` object from Express. But, you can easily generate an HTTP NodeJS object from the `app` object. - -Just follow the bottom example: - -```javascript -const express = require('express') -const helmet = require('helmet') -const http = require('http') -const GracefulServer = require('@gquittet/graceful-server') -const { connectToDb, closeDbConnection } = require('./db') - -const app = express() -const server = http.createServer(app) -const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] }) - -app.use(helmet()) - -app.get('/test', (_, res) => { - return res.send({ uptime: process.uptime() | 0 }) -}) - -gracefulServer.on(GracefulServer.READY, () => { - console.log('Server is ready') -}) - -gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { - console.log('Server is shutting down') -}) - -gracefulServer.on(GracefulServer.SHUTDOWN, error => { - console.log('Server is down because of', error.message) -}) - -server.listen(8080, async () => { - await connectToDb() - gracefulServer.setReady() -}) -``` - -As you can see, we're using the `app` object from Express to set up the endpoints and middleware. -But it can't listen (you can do it but `app` hasn't any liveness or readiness). The listening -of HTTP calls need to be done by the default NodeJS HTTP object (aka **_server_**). - -### Fastify - -```javascript -const fastify = require('fastify')({ logger: true }) -const GracefulServer = require('@gquittet/graceful-server') - -const gracefulServer = GracefulServer(fastify.server) - -gracefulServer.on(GracefulServer.READY, () => { - console.log('Server is ready') -}) - -gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { - console.log('Server is shutting down') -}) - -gracefulServer.on(GracefulServer.SHUTDOWN, error => { - console.log('Server is down because of', error.message) -}) - -// Declare a route -fastify.get('/', async (request, reply) => { - return { hello: 'world' } -}) - -// Run the server! -const start = async () => { - try { - await fastify.listen({ port: 3000 }) - fastify.log.info(`server listening on ${fastify.server.address().port}`) - gracefulServer.setReady() - } catch (err) { - fastify.log.error(err) - process.exit(1) - } -} -start() -``` - -**Be careful, if you are using Fastify v4.x.x with Node 16 and below**, you have to use - -```javascript -await fastify.listen({ port: 3000, host: '0.0.0.0' }) -``` - -because Node 16 and below does not support multiple addresses binding. - -See: https://github.com/fastify/fastify/issues/3536 - -### Koa - -```javascript -const GracefulServer = require('@gquittet/graceful-server') -const Koa = require('koa') -const http = require('http') -const Router = require('koa-router') - -const app = new Koa() -const router = new Router() - -const server = http.createServer(app.callback()) -gracefulServer = GracefulServer(server) +See: [EXAMPLE.md](./docs/EXAMPLE.md) -router.get('/test') -app.use(router.routes()) - -// response -app.use(ctx => { - ctx.body = 'Hello Koa' -}) - -gracefulServer.on(GracefulServer.READY, () => { - console.log('Server is ready') -}) - -gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { - console.log('Server is shutting down') -}) - -gracefulServer.on(GracefulServer.SHUTDOWN, error => { - console.log('Server is down because of', error.message) -}) - -server.listen(8080, async () => { - gracefulServer.setReady() -}) -``` - -As you can see, we're using the `app` object from Express to set up the endpoints and middleware. -But it can't listen (you can do it but `app` hasn't any liveness or readiness). The listening -of HTTP calls need to be done by the default NodeJS HTTP object (aka **_server_**). - -### HTTP Server - -```javascript -import http from 'http' -import url from 'url' -import GracefulServer from '@gquittet/graceful-server' -import { connectToDb, closeDbConnection } from './db' - -const server = http.createServer((req, res) => { - if (req.url === '/test' && req.method === 'GET') { - res.statusCode = 200 - res.setHeader('Content-Type', 'application/json') - return res.end(JSON.stringify({ uptime: process.uptime() | 0 })) - } - res.statusCode = 404 - return res.end() -}) - -const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] }) - -gracefulServer.on(GracefulServer.READY, () => { - console.log('Server is ready') -}) - -gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { - console.log('Server is shutting down') -}) - -gracefulServer.on(GracefulServer.SHUTDOWN, error => { - console.log('Server is down because of', error.message) -}) - -server.listen(8080, async () => { - await connectToDb() - gracefulServer.setReady() -}) -``` +## API Doc -## API - -### GracefulServer - -```typescript -;((server: http.Server, options?: IGracefulServerOptions | undefined) => IGracefulServer) & typeof State -``` - -where `State` is an enum that contains, `STARTING`, `READY`, `SHUTTING_DOWN` and `SHUTDOWN`. - -### IGracefulServerOptions - -All of the below options are optional. - -| Name | Type | Default | Description | -|-------------------|:--------------------------:|:-------:|-----------------------------------------------------------------:| -| syncClose | boolean | false | Run the closePromises in a series. | -| closePromises | (() => Promise)[] | [] | The functions to run when the API is stopping | -| timeout | number | 1000 | The time in milliseconds to wait before shutting down the server | -| healthCheck | boolean | true | Enable/Disable the default endpoints (liveness and readiness) | -| kubernetes | boolean | false | Enable/Disable the kubernetes mode | -| livenessEndpoint | string | /live | The liveness endpoint | -| readinessEndpoint | string | /ready | The readiness endpoint | - -If you use Kubernetes, enable the kubernetes mode to let it handles the incoming traffic of your application. - -The Kubernetes mode will only work if you haven't disabled the health checks. - -### GracefulServer Instance - -```typescript -export default interface IGracefulServer { - isReady: () => boolean - setReady: () => void - on: (name: string, callback: (...args: any[]) => void) => EventEmitter -} -``` +See: [API.md](./docs/API.md) ## Integration with Docker -### HEALTH CHECK in Dockerfile - -```Dockerfile -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"] -``` - -### Content of _healthcheck.js_ - -```javascript -const http = require('http') - -const options = { - timeout: 2000, - host: 'localhost', - port: 8080, - path: '/live' -} - -const request = http.request(options, res => { - console.info('STATUS:', res.statusCode) - process.exitCode = res.statusCode === 200 ? 0 : 1 - process.exit() -}) - -request.on('error', err => { - console.error('ERROR', err) - process.exit(1) -}) - -request.end() -``` - -### Example of Dockerfile - -#### POC level - -```Dockerfile -FROM node:12-slim - -WORKDIR /usr/src/app - -COPY package*.json ./ - -RUN npm ci --only=production - -COPY . . - -EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"] -CMD [ "node", "server.js" ] -``` - -#### Company level - -```Dockerfile -FROM node:12-slim as base -ENV NODE_ENV=production -ENV TINI_VERSION=v0.18.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini && \ - mkdir -p /node_app/app && \ - chown -R node:node /node_app -WORKDIR /node_app -USER node -COPY --chown=node:node package.json package-lock*.json ./ -RUN npm ci && \ - npm cache clean --force -WORKDIR /node_app/app - -FROM base as source -COPY --chown=node:node . . - -FROM source as dev -ENV NODE_ENV=development -ENV PATH=/node_app/node_modules/.bin:$PATH -RUN npm install --only=development --prefix /node_app -CMD ["nodemon", "--inspect=0.0.0.0:9229"] - -FROM source as test -ENV NODE_ENV=development -ENV PATH=/node_app/node_modules/.bin:$PATH -COPY --from=dev /node_app/node_modules /node_app/node_modules -RUN npm run lint -ENV NODE_ENV=test -RUN npm test -CMD ["npm", "test"] - -FROM test as audit -RUN npm audit --audit-level critical -USER root -ADD https://get.aquasec.com/microscanner / -RUN chmod +x /microscanner && \ - /microscanner your_token --continue-on-failure - -FROM source as buildProd -ENV PATH=/node_app/node_modules/.bin:$PATH -COPY --from=dev /node_app/node_modules /node_app/node_modules -RUN npm run build - -FROM source as prod -COPY --from=buildProd --chown=node:node /node_app/app/build ./build -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"] -ENTRYPOINT ["/tini", "--"] -CMD ["node", "./build/src/main.js"] -``` +See: [DOCKER.md](./docs/DOCKER.md) ## Integration with Kubernetes -Don't forget to enable the kubernetes mode. [Check here](#igracefulserveroptions) (related to this [issue](https://github.com/gquittet/graceful-server/issues/5)) - -```yml -readinessProbe: - httpGet: - path: /ready - port: 8080 - failureThreshold: 1 - initialDelaySeconds: 5 - periodSeconds: 5 - successThreshold: 1 - timeoutSeconds: 5 -livenessProbe: - httpGet: - path: /live - port: 8080 - failureThreshold: 3 - initialDelaySeconds: 10 - # Allow sufficient amount of time (90 seconds = periodSeconds * failureThreshold) - # for the registered shutdown handlers to run to completion. - periodSeconds: 30 - successThreshold: 1 - # Setting a very low timeout value (e.g. 1 second) can cause false-positive - # checks and service interruption. - timeoutSeconds: 5 - -# As per Kubernetes documentation (https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#when-should-you-use-a-startup-probe), -# startup probe should point to the same endpoint as the liveness probe. -# -# Startup probe is only needed when container is taking longer to start than -# `initialDelaySeconds + failureThreshold × periodSeconds` of the liveness probe. -startupProbe: - httpGet: - path: /live - port: 8080 - failureThreshold: 3 - initialDelaySeconds: 10 - periodSeconds: 30 - successThreshold: 1 - timeoutSeconds: 5 -``` +See: [KUBERNETES.md](./docs/KUBERNETES.md) ## Thanks diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..45f3b51 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,43 @@ +# API + +- [API](#api) + - [GracefulServer](#gracefulserver) + - [IGracefulServerOptions](#igracefulserveroptions) + - [GracefulServer Instance](#gracefulserver-instance) + + +## GracefulServer + +```typescript +;((server: http.Server, options?: IGracefulServerOptions | undefined) => IGracefulServer) & typeof State +``` + +where `State` is an enum that contains, `STARTING`, `READY`, `SHUTTING_DOWN` and `SHUTDOWN`. + +## IGracefulServerOptions + +All of the below options are optional. + +| Name | Type | Default | Description | +| ----------------- | :------------------------: | :-----: | ---------------------------------------------------------------: | +| syncClose | boolean | false | Run the closePromises in a series. | +| closePromises | (() => Promise)[] | [] | The functions to run when the API is stopping | +| timeout | number | 1000 | The time in milliseconds to wait before shutting down the server | +| healthCheck | boolean | true | Enable/Disable the default endpoints (liveness and readiness) | +| kubernetes | boolean | false | Enable/Disable the kubernetes mode | +| livenessEndpoint | string | /live | The liveness endpoint | +| readinessEndpoint | string | /ready | The readiness endpoint | + +If you use Kubernetes, enable the kubernetes mode to let it handles the incoming traffic of your application. + +The Kubernetes mode will only work if you haven't disabled the health checks. + +## GracefulServer Instance + +```typescript +export default interface IGracefulServer { + isReady: () => boolean + setReady: () => void + on: (name: string, callback: (...args: any[]) => void) => EventEmitter +} +``` diff --git a/docs/DOCKER.md b/docs/DOCKER.md new file mode 100644 index 0000000..80f6d90 --- /dev/null +++ b/docs/DOCKER.md @@ -0,0 +1,51 @@ +# Integration with Docker + +## HEALTH CHECK in Dockerfile + +```Dockerfile +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"] +``` + +## Content of _healthcheck.js_ + +```javascript +const http = require('http') + +const options = { + timeout: 2000, + host: 'localhost', + port: 8080, + path: '/live' +} + +const request = http.request(options, res => { + console.info('STATUS:', res.statusCode) + process.exitCode = res.statusCode === 200 ? 0 : 1 + process.exit() +}) + +request.on('error', err => { + console.error('ERROR', err) + process.exit(1) +}) + +request.end() +``` + +## Example of Dockerfile + +```Dockerfile +FROM node:20-slim + +WORKDIR /usr/src/app + +COPY package*.json ./ + +RUN npm ci --only=production + +COPY . . + +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"] +CMD [ "node", "server.js" ] +``` diff --git a/docs/EXAMPLE.md b/docs/EXAMPLE.md new file mode 100644 index 0000000..e2d4a1f --- /dev/null +++ b/docs/EXAMPLE.md @@ -0,0 +1,223 @@ +# Example + +- [Example](#example) + - [ExpressJS](#expressjs) + - [Fastify](#fastify) + - [Koa](#koa) + - [HTTP Server](#http-server) + - [GracefulServer](#gracefulserver) + - [IGracefulServerOptions](#igracefulserveroptions) + - [GracefulServer Instance](#gracefulserver-instance) + + +## ExpressJS + +The library works with the default HTTP NodeJS object. So, when you're using Express you can't pass +directly the `app` object from Express. But, you can easily generate an HTTP NodeJS object from the `app` object. + +Just follow the bottom example: + +```javascript +const express = require('express') +const helmet = require('helmet') +const http = require('http') +const GracefulServer = require('@gquittet/graceful-server') +const { connectToDb, closeDbConnection } = require('./db') + +const app = express() +const server = http.createServer(app) +const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] }) + +app.use(helmet()) + +app.get('/test', (_, res) => { + return res.send({ uptime: process.uptime() | 0 }) +}) + +gracefulServer.on(GracefulServer.READY, () => { + console.log('Server is ready') +}) + +gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { + console.log('Server is shutting down') +}) + +gracefulServer.on(GracefulServer.SHUTDOWN, error => { + console.log('Server is down because of', error.message) +}) + +server.listen(8080, async () => { + await connectToDb() + gracefulServer.setReady() +}) +``` + +As you can see, we're using the `app` object from Express to set up the endpoints and middleware. +But it can't listen (you can do it but `app` hasn't any liveness or readiness). The listening +of HTTP calls need to be done by the default NodeJS HTTP object (aka **_server_**). + +## Fastify + +```javascript +const fastify = require('fastify')({ logger: true }) +const GracefulServer = require('@gquittet/graceful-server') + +const gracefulServer = GracefulServer(fastify.server) + +gracefulServer.on(GracefulServer.READY, () => { + console.log('Server is ready') +}) + +gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { + console.log('Server is shutting down') +}) + +gracefulServer.on(GracefulServer.SHUTDOWN, error => { + console.log('Server is down because of', error.message) +}) + +// Declare a route +fastify.get('/', async (request, reply) => { + return { hello: 'world' } +}) + +// Run the server! +const start = async () => { + try { + await fastify.listen({ port: 3000 }) + fastify.log.info(`server listening on ${fastify.server.address().port}`) + gracefulServer.setReady() + } catch (err) { + fastify.log.error(err) + process.exit(1) + } +} +start() +``` + +**Be careful, if you are using Fastify v4.x.x with Node 16 and below**, you have to use + +```javascript +await fastify.listen({ port: 3000, host: '0.0.0.0' }) +``` + +because Node 16 and below does not support multiple addresses binding. + +See: https://github.com/fastify/fastify/issues/3536 + +## Koa + +```javascript +const GracefulServer = require('@gquittet/graceful-server') +const Koa = require('koa') +const http = require('http') +const Router = require('koa-router') + +const app = new Koa() +const router = new Router() + +const server = http.createServer(app.callback()) +gracefulServer = GracefulServer(server) + +router.get('/test') +app.use(router.routes()) + +// response +app.use(ctx => { + ctx.body = 'Hello Koa' +}) + +gracefulServer.on(GracefulServer.READY, () => { + console.log('Server is ready') +}) + +gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { + console.log('Server is shutting down') +}) + +gracefulServer.on(GracefulServer.SHUTDOWN, error => { + console.log('Server is down because of', error.message) +}) + +server.listen(8080, async () => { + gracefulServer.setReady() +}) +``` + +As you can see, we're using the `app` object from Express to set up the endpoints and middleware. +But it can't listen (you can do it but `app` hasn't any liveness or readiness). The listening +of HTTP calls need to be done by the default NodeJS HTTP object (aka **_server_**). + +## HTTP Server + +```javascript +import http from 'http' +import url from 'url' +import GracefulServer from '@gquittet/graceful-server' +import { connectToDb, closeDbConnection } from './db' + +const server = http.createServer((req, res) => { + if (req.url === '/test' && req.method === 'GET') { + res.statusCode = 200 + res.setHeader('Content-Type', 'application/json') + return res.end(JSON.stringify({ uptime: process.uptime() | 0 })) + } + res.statusCode = 404 + return res.end() +}) + +const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] }) + +gracefulServer.on(GracefulServer.READY, () => { + console.log('Server is ready') +}) + +gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => { + console.log('Server is shutting down') +}) + +gracefulServer.on(GracefulServer.SHUTDOWN, error => { + console.log('Server is down because of', error.message) +}) + +server.listen(8080, async () => { + await connectToDb() + gracefulServer.setReady() +}) +``` + +## GracefulServer + +```typescript +;((server: http.Server, options?: IGracefulServerOptions | undefined) => IGracefulServer) & typeof State +``` + +where `State` is an enum that contains, `STARTING`, `READY`, `SHUTTING_DOWN` and `SHUTDOWN`. + +## IGracefulServerOptions + +All of the below options are optional. + +| Name | Type | Default | Description | +| ----------------- | :------------------------: | :-----: | ---------------------------------------------------------------: | +| syncClose | boolean | false | Run the closePromises in a series. | +| closePromises | (() => Promise)[] | [] | The functions to run when the API is stopping | +| timeout | number | 1000 | The time in milliseconds to wait before shutting down the server | +| healthCheck | boolean | true | Enable/Disable the default endpoints (liveness and readiness) | +| kubernetes | boolean | false | Enable/Disable the kubernetes mode | +| livenessEndpoint | string | /live | The liveness endpoint | +| readinessEndpoint | string | /ready | The readiness endpoint | + +If you use Kubernetes, enable the kubernetes mode to let it handles the incoming traffic of your application. + +The Kubernetes mode will only work if you haven't disabled the health checks. + +## GracefulServer Instance + +```typescript +export default interface IGracefulServer { + isReady: () => boolean + setReady: () => void + on: (name: string, callback: (...args: any[]) => void) => EventEmitter +} +``` diff --git a/docs/KUBERNETES.md b/docs/KUBERNETES.md new file mode 100644 index 0000000..fe83283 --- /dev/null +++ b/docs/KUBERNETES.md @@ -0,0 +1,43 @@ +# Integration with Kubernetes + +Don't forget to enable the kubernetes mode. [Check here](#igracefulserveroptions) (related to this [issue](https://github.com/gquittet/graceful-server/issues/5)) + +```yml +readinessProbe: + httpGet: + path: /ready + port: 8080 + failureThreshold: 1 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 +livenessProbe: + httpGet: + path: /live + port: 8080 + failureThreshold: 3 + initialDelaySeconds: 10 + # Allow sufficient amount of time (90 seconds = periodSeconds * failureThreshold) + # for the registered shutdown handlers to run to completion. + periodSeconds: 30 + successThreshold: 1 + # Setting a very low timeout value (e.g. 1 second) can cause false-positive + # checks and service interruption. + timeoutSeconds: 5 + +# As per Kubernetes documentation (https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#when-should-you-use-a-startup-probe), +# startup probe should point to the same endpoint as the liveness probe. +# +# Startup probe is only needed when container is taking longer to start than +# `initialDelaySeconds + failureThreshold × periodSeconds` of the liveness probe. +startupProbe: + httpGet: + path: /live + port: 8080 + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 +```