0.3.0 • Published 5 days ago

staying-alive v0.3.0

Weekly downloads
-
License
MIT
Repository
github
Last release
5 days ago

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 is ok or degraded
    • Returns 500 when status is error
  • GET /health/my-database health endpoint for the my-database integration
    • Returns 200 when alive=true
    • Returns 500 when alive=false
  • GET /health/my-web-check health endpoint for the my-database integration
    • Returns 200 when alive=true
    • Returns 500 when alive=false
  • GET /health/liveness
    • Returns the same as the overall /health endpoint, but my-cache is optional
  • GET /health/readiness
    • Returns the same as the overall /health endpoint, all integrations are required

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

PropertyTypeDescription
dbNamestringThe name of the database to be checked.
dialectDialectThe dialect of the database to be checked.
dialectOptionsobject(Optional) The dialect specific options of the database to be checked.
hoststringThe host where the database to be checked is located.
passwordstringThe password to connect to the database.
portnumberThe port to connect to the database.
userstringThe 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

PropertyTypeDescription
connectTimeoutnumberOptional connection timeout in milliseconds
dbstringOptional database name
hoststringHostname of the Redis server
passwordstringOptional password for authentication
portnumberPort 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

PropertyTypeDescription
connectTimeout() => 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 with require() 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 ESM import in a JS project.
  • package-ts - A package setup with TypeScript. This package holds most of the integration tests that run in CI in src/main.ts.

Yalc is used to link the packages:

  1. from the repo root, build the library: yarn build
  2. publish the package (locally) with Yalc: yalc publish
  3. cd into test/package-ts and run yalc 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, you can run node dist/main.js in test/package-ts, but the health check will fail unless database, redis, etc... are running the same as in CI. Integration tests are probably easier to run in CI.

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:

  1. Add the package as a devDependency: yarn add -D some-package
  2. In package.json, copy the devDependency to peerDependencies
  3. In package.json, mark the dependency as optional in peerDependenciesMeta

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 need redis@2 while this package already depends on redis@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:

  1. Add the service docker image in .circleci/config.yml under jobs.integration-tests.docker
  2. Add 2 checks in test/package-ts/src/main.ts - one should be required and succeed, the other should be optional and fail.
  3. Add an expect call in test/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:

  1. Make your changes in code in a feature branch
  2. 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.
  3. 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.
  4. Commit these changes and push to the feature branch
  5. Merge the feature branch

Please be thoughtful and correct on patch, minor, and breaking releases based on the changes you are introducing.

0.3.0

5 days ago

0.2.0

9 months ago

0.1.2

1 year ago

0.1.1

1 year ago

0.1.1-beta.0

1 year ago

0.1.0-beta.0

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.4-beta.0

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago

0.0.0

1 year ago