staying-alive
v1.0.0
Published
> ๐ถ Ah, ha, ha, ha, stayin' aliiiiiiiiiiiiiive! [๐ค](https://www.youtube.com/watch?v=I_izvAbhExY)
Downloads
6,917
Readme
Staying Alive ๐บ
๐ถ Ah, ha, ha, ha, stayin' aliiiiiiiiiiiiiive! ๐ค
staying-alive
is a funky package that helps creating health endpoints for NodeJS. Whether you need the simplest health check or mutliple probes with different config, you're stayin' alive.
Installation
$ yarn add staying-alive
Usage
Info
If you're using Express, please use the express middleware.
If you're using Koa, please use the koa router.
import { createHealthCheck, optional } from 'staying-alive';
import { databaseCheck } from 'staying-alive/integrations/database';
// create a health check function
const healthCheck = createHealthCheck({
integrations: {
'my-database': databaseCheck({
host: 'localhost',
port: 5432,
dbName: 'my-db',
user: 'postgres',
password: 'password',
dialect: 'postgres',
}),
'my-redis': optional(
redisCheck({
host: 'localhost',
port: 6379,
}),
),
},
});
// check health
const health = await healthCheck();
If the database is reachable but the redis is not, the example above will return:
{
status: 'degraded',
duration: 0.045,
integrations: {
'my-db': {
alive: true,
duration: 0.033,
required: true
},
'my-redis': {
alive: false,
duration: 0.012,
required: false
}
}
}
All integrations are required by default.
- If any required integration is not "alive", the overall status will be
"error"
. - If only optional integrations are not "alive", the overall status will be
"degraded"
To mark an integration as optional, wrap it in optional()
.
Express middleware
If you're using Express, you can install the middleware like this:
import { healthCheckMiddleware } from 'staying-alive/express';
import express from 'express';
const app = express();
app.use(
'/health',
healthCheckMiddleware({
integrations: {
'my-database': databaseCheck({
host: 'localhost',
port: 5432,
dbName: 'my-db',
user: 'postgres',
password: 'password',
dialect: 'postgres',
}),
},
}),
);
The Express middleware can also define additional "probes", see healthCheckMiddleware API.
Koa router
If you're using Koa, you can install the health router like this:
import Koa from 'koa';
import { healthCheckRouter } from 'staying-alive/koa';
const app = new Koa();
const healthRouter = healthCheckRouter({
prefix: '/health',
integrations: {
'my-database': databaseCheck({
host: 'localhost',
port: 5432,
dbName: 'my-db',
user: 'postgres',
password: 'password',
dialect: 'postgres',
}),
},
});
app.use(healthRouter.routes()).use(healthRouter.allowedMethods());
๐ฆ Requires
@koa/router
and@types/koa__router
for TypeScript.
The Koa router can also define additional "probes", just like the Express middleware. See healthCheckMiddleware API.
Integrations
Integrations are direct dependencies of an app, like its database or cache. They can be checked using staying-alive
's built-in integrations, or by building your own.
Integrations will require you to install additional packages, like sequelize
for databases or redis
for redis.
See the Integrations API:
Writing your own integration
There are 2 ways to use a custom integration: use the customCheck
or create a custom integration:
import type { CheckFunction } from 'staying-alive';
function myCustomIntegration(options): CheckFunction {
return async () => {
if (somethingFails(options)) {
throw new Error('it failed!'); // throw to mark failure
}
// nothing needs to be returned
};
}
API
createHealthCheck()
createHealthCheck()
is the core function to create a health check function:
import { createHealthCheck } from 'staying-alive';
const healthCheck = createHealthCheck({
integrations: {
'name-of-integration': webCheck('https://example.org'),
// other integrations
},
});
It will return an async function that will check each integrations:
const health = await healthCheck();
The returned health
object contains the following properties:
type HealthObject = {
status: 'ok' | 'degraded' | 'error';
duration: number; // the overall time it took to run all checks, in seconds
integrations?: {
'name-of-integration': {
alive: boolean;
duration: number;
required: boolean;
};
};
};
optional()
All integrations are required by default. To mark an integration as optional, wrap it in optional()
:
import { createHealthCheck, optional } from 'staying-alive';
import { databaseCheck } from 'staying-alive/integrations/database';
const healthCheck = createHealthCheck({
integrations: {
'my-optional-database': optional(databaseCheck(options)),
},
});
healthCheckMiddleware()
healthCheckMiddleware()
returns an Express middleware.
import { healthCheckMiddleware } from 'staying-alive/express';
import express from 'express';
const app = express();
app.use(
'/health', // the path where all endpoints are mounted
healthCheckMiddleware({
integrations: {
'my-database': databaseCheck(options),
'my-cache': redisCheck(options),
},
probes: {
liveness: {
'my-database': 'required',
'my-cache': 'optional', // here my-cache is marked as optional
},
readiness: {
'my-database': 'required',
'my-cache': 'required',
},
},
}),
);
The middleware will create multiple endpoints:
- an overall health endpoint
- an endpoint for each integration
- an endpoint for each "probe", where you can redefine if integrations are required or not.
The above example will create the following endpoints:
GET /health
the overall health endpoint- Returns
200
when status isok
ordegraded
- Returns
500
when status iserror
- Returns
GET /health/my-database
health endpoint for themy-database
integration- Returns
200
whenalive=true
- Returns
500
whenalive=false
- Returns
GET /health/my-web-check
health endpoint for themy-database
integration- Returns
200
whenalive=true
- Returns
500
whenalive=false
- Returns
GET /health/liveness
- Returns the same as the overall
/health
endpoint, butmy-cache
is optional
- Returns the same as the overall
GET /health/readiness
- Returns the same as the overall
/health
endpoint, all integrations are required
- Returns the same as the overall
Integrations
Database
import { databaseCheck } from 'staying-alive/integrations/database';
createHealthCheck({
integrations: {
'my-db': databaseCheck({
user: 'postgres',
password: 'password',
host: 'localhost',
port: 5432,
dbName: 'postgres',
dialect: 'postgres',
}),
},
});
๐ฆ Requires
sequelize
and an accompanying database client library. See sequelize docs
Options
| Property | Type | Description |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| dbName
| string | The name of the database to be checked. |
| dialect
| Dialect | The dialect of the database to be checked. |
| dialectOptions
| object | (Optional) The dialect specific options of the database to be checked. |
| host
| string | The host where the database to be checked is located. |
| password
| string | The password to connect to the database. |
| port
| number | The port to connect to the database. |
| user
| string | The username to connect to the database. |
Redis
import { redisCheck } from 'staying-alive/integrations/redis';
createHealthCheck({
integrations: {
'my-redis': redisCheck({
host: 'localhost',
port: 6379,
}),
},
});
๐ฆ Requires the
redis
package
Options
| Property | Type | Description |
| ---------------- | ------ | ------------------------------------------- |
| connectTimeout
| number | Optional connection timeout in milliseconds |
| db
| number | Optional database name |
| host
| string | Hostname of the Redis server |
| password
| string | Optional password for authentication |
| port
| number | Port of the Redis server |
DynamoDB
import { dynamodbCheck } from 'staying-alive/integrations/dynamodb';
createHealthCheck({
integrations: {
'my-dynamo': dynamodbCheck({
region: 'us-east-1',
credentials: {
accessKeyId: '***',
secretAccessKey: '***',
},
endpoint: 'http://localhost:8000',
}),
},
});
๐ฆ Requires the
@aws-sdk/client-dynamodb
package
Options
Options for dynamodbCheck
is the same object as new DynamoDBClient(options);
from @aws-sdk/client-dynamodb
: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/interfaces/dynamodbclientconfig.html
Web
import { webCheck } from 'staying-alive/integrations/web';
createHealthCheck({
integrations: {
'my-web-check': webCheck('http://example.org', options),
},
});
๐ฆ Requires the
node-fetch
package (v2)
Options
The webCheck
function accepts the same parameters as the fetch
function from node-fetch
: https://github.com/node-fetch/node-fetch/tree/2.x#fetchurl-options
Custom
You can write your own custom integration by using the customCheck
function:
import { customCheck } from 'staying-alive/integrations/custom';
createHealthCheck({
integrations: {
'my-custom-check': customCheck({
func: async () => {
await checkSomething();
},
}),
},
});
Options
| Property | Type | Description |
| -------- | --------------------- | -------------------------------------------------------------------------- |
| func
| () => Promise<void>
| Async check function. The function is expected to throw if the check fails |
Recipes
Health endpoints with custom route handlers
You can get the same functionalities as the Express middleware (integrations and probe endpoints):
import { createHealthCheck, optional } from 'staying-alive';
import { databaseCheck } from 'staying-alive/integrations/database';
// define integrations
const myDatabase = databaseCheck(options);
const myRedis = redisCheck(options);
// create multiple health check functions
const healthCheckAllRequired = createHealthCheck({
integrations: {
'my-database': myDatabase,
'my-redis': myRedis,
},
});
const healthCheckRedisOptional = createHealthCheck({
integrations: {
'my-database': myDatabase,
'my-redis': optional(myRedis), // here redis is marked as optional
},
});
Then simply call the healthCheck functions in your route handlers and return the health object.
Contributing
Running locally
This package must be imported into another to be used. In the test
directory are 3 sample packages to do this:
package-cjs
- a traditional CommonJS package, pure javascript. This package is mostly used to make sure the library can be imported withrequire()
in a JS project.package-esm
- an ESM-only package (type: "module"
), pure javascript. This package is mostly used to make sure the library can be imported with ESMimport
in a JS project.package-ts
- A package setup with TypeScript. This package holds most of the integration tests that run in CI insrc/main.ts
.package-ts-koa
- A package setup with TypeScript & koa. This package is a demo of using a basic health check with Koa.
Yalc is used to link the packages:
- from the repo root, build the library:
yarn build
- publish the package (locally) with Yalc:
yalc publish dist
cd
intotest/package-ts
and runyalc add staying-alive
CI is also using Yalc this way, checkout the CircleCI config.
Testing locally
To run unit tests, yarn test
.
To run integration tests:
- Run
docker compose up -d
in the root to set up the necessary Docker containers cd
intotest/package-ts
and runyarn && yarn build
to install dependencies and build the app.- Execute the tests by running
node dist/main.js
Adding an integration
To add an integration, create a file in src/integrations
. It should export a single function ending with Check
, ie. fooCheck()
. This function should return an async function () => Promise<void>
. To mark a failure, throw an Error
.
You'll most likely need an NPM package to implement the integration:
- Add the package as a devDependency:
yarn add -D some-package
- In
package.json
, copy thedevDependency
topeerDependencies
- In
package.json
, mark the dependency as optional inpeerDependenciesMeta
Don't forget to add an export entry in package.json
under the exports
key (see package.json for similar entries).
Info
If the integration you want to add requires a package that conflicts with one already installed (ie. if you needredis@2
while this package already depends onredis@3
), you'll have to implement this integration in a separate package. There are plans to support this, but not at present. Please talk with Frontend Platform in case you need this.
You'll also need to include an integration test:
- Add the service docker image in
.circleci/config.yml
underjobs.integration-tests.docker
- Add 2 checks in
test/package-ts/src/main.ts
- one should be required and succeed, the other should be optional and fail. - Add an
expect
call intest/package-ts/src/main.ts
to make sure the optional check does fail.
Changesets
This repo uses changesets to manage changelogs and releases.
How it works:
- Make your changes in code in a feature branch
- Once happy with the changes, run
yarn changeset
. This will generate a temporary file in.changeset
. Commit this file. If your PR includes multiple changes, you can produce multiple changesets. - When the PR is approved and ready to merge, create a release for any changesets by running
yarn release
. This will remove all the temporary files in.changeset
and generate required changes in each package's CHANGELOG.md and package.json. - Commit these changes and push to the feature branch
- Merge the feature branch
Please be thoughtful and correct on patch, minor, and breaking releases based on the changes you are introducing.