UNPKG

next-actuator

Version:

A fully composable actuator implementation for Next.js projects

317 lines (258 loc) 11.1 kB
# Next Actuator [![Version](https://img.shields.io/npm/v/next-actuator?logo=npm&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/next-actuator) [![Status](https://img.shields.io/github/actions/workflow/status/rossyman/next-actuator/release.yaml?logo=githubactions&logoColor=fff&style=flat&colorA=000000&colorB=000000)](https://github.com/rossyman/next-actuator/actions) [![Semantic Release: conventional](https://img.shields.io/badge/semantic--release-conventional-e10079?logo=semantic-release&style=flat&colorA=000000&colorB=000000)](https://github.com/semantic-release/semantic-release) [![Next.js Version](https://img.shields.io/npm/dependency-version/next-actuator/peer/next?logo=nextdotjs&style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/next-actuator) Next actuator provides a suite of production-ready observability endpoints to monitor and audit the health, metrics, and general status of your application. The library is loosely inspired by [Spring Actuator](https://docs.spring.io/spring-boot/reference/actuator/index.html). ## Installation ```bash npm install next-actuator yarn add next-actuator pnpm add next-actuator bun add next-actuator ``` ## Usage ### Route handler Create a new route handler, nominally at `/app/api/actuator/[...actuator]/route.ts`, and re-export the `GET` function returned by `createNextActuator()`. ```typescript import { createNextActuator } from 'next-actuator' const { GET } = createNextActuator() export { GET } ``` ## API Reference | Endpoint | Configure | Description | |:-------------------------------------|:-------------------|:---------------------------------------------------| | `/health` | `endpoints.health` | Displays application and component statuses | | `/info` | `endpoints.info` | Displays build and metadata information | | `/metrics` | `endpoint.metrics` | Displays a list of all available metrics | | `/metrics/<metric>` | | Displays aggregate information about a metric | | `/metrics/<metric>?dimension=<name>` | | Displays information about a dimension of a metric | ## Configuration | **Property** | **Type** | **Description** | **Default** | |---------------------|------------------------------------------|-------------------------------------------------------------------------|----------------------| | `disabled` | `boolean` | Whether to disable all endpoints, useful for disabling per-environment. | `false` | | `endpoints.health` | `string \| false` | Configure the health endpoint or disable it. | `'/health'` | | `endpoints.metrics` | `string \| false` | Configure the metrics endpoint or disable it. | `'/metrics'` | | `endpoints.info` | `string \| false` | Configure the info endpoint or disable it. | `'/info'` | | `components` | `Record<string, Component>` | Register your application components. | | | `metrics` | `Record<string, Metric>` | Register your custom metrics. | See included metrics | | `info` | `() => Promise<Record<string, unknown>>` | Application details, nominally elements from `package.json` and git | | ## Health ### Create a component Components represent a segment of your apps general availability. If one of these components were to go down, then the functionality of your application would be greatly degraded. By default, an outage returns a `'DEGRADED'` response. There may be some instances in which you cannot recover from an external provider outage. Therefore, you can configure the aggregate status by providing `strategy: 'DOWN'` to ensure your app is marked as `'DOWN'` during this period. | **Property** | **Type** | **Description** | |--------------|----------------------------------------------------|----------------------------------------------------------------------------| | `status` | `(req: NextRequest) => Promise<number \| boolean>` | Return the status of a component, either as a boolean or HTTP status code. | | `strategy` | `'DEGRADED' \| 'DOWN'` | Tells the actuator how it should handle aggregating outage statuses | | `details` | `Record<string, unknown>` | Any extra metadata you wish to include alongside the component status. | ### Usage ```typescript import { createNextActuator, type Component } from 'next-actuator' const api: Component = { status: async () => fetch('https://api.acme.org').then(res => res.status), details: { description: 'Our application\'s external API', endpoint: 'https://api.acme.org' } } const { GET } = createNextActuator({ components: { api } }) export { GET } ``` ### Sample response ```json { "status": "UP", "components": { "api": { "status": "UP", "details": { "description": "Our application's external API", "endpoint": "https://api.acme.org" } } } } ``` ## Metrics ### Included metrics By default, we collect some metrics that may be useful for auditing your app, namely: `memory.used`, `cpu.used` and `uptime`. ### Create a Metric Metrics come in two different shapes. You can either supply a straightforward singular metric, or a dimensional metric. Dimensional metrics are aggregated, but an optional search parameter can be supplied to the metrics endpoint in order to filter. | **Property** | **Type** | **Description** | |---------------|-------------------------------------------------------------------------|----------------------------------------------------------------------| | `description` | `string` | A description of the metric. | | `baseUnit` | `string` | The unit that the metric is represented in. | | `value` | `() => Promise<number>` | The value of a metric. | | `dimensions` | `Record<string, { value: () => Promise<number>, description: string }>` | A collection of dimensions representing the aggregated metric value. | ### Usage ```typescript import { createNextActuator, type Metric } from 'next-actuator' const complexSeconds: Metric = { description: 'Total time in seconds', baseUnit: 'seconds', dimensions: { first: { value: async () => 10, description: 'Some seconds' }, second: { value: async () => 11, description: 'Some more seconds' } } } const simpleSeconds: Metric = { description: 'My super simple seconds metric', baseUnit: 'seconds', value: async () => 1 } const { GET } = createNextActuator({ metrics: { 'complex.seconds': complexSeconds, 'simple.seconds': simpleSeconds } }) export { GET } ``` ### Sample responses #### `/metrics` ```json { "names": [ "memory.used", "cpu.used", "uptime", "complex.seconds", "simple.seconds" ] } ``` #### `/metrics/{name}` This request displays an aggregate view of all given dimensions (If present), or the returned value of the simple metric. ```json { "name": "complex.seconds", "description": "Total time in seconds", "baseUnit": "seconds", "measurements": [ { "statistic": "VALUE", "value": 21 } ], "availableDimensions": [ "first", "second" ] } ``` #### `/metrics/{name}?dimension={dimension}` This request only displays information related to the filtered dimension ```json { "name": "complex.seconds", "description": "Some seconds", "baseUnit": "seconds", "measurements": [ { "statistic": "VALUE", "value": 10 } ], "availableDimensions": [ ] } ``` ## Info The info endpoint by default returns the `BUILD_ID` generated by Next.js out of the box. Read [these docs](https://nextjs.org/docs/app/api-reference/next-config-js/generateBuildId) to learn how to customise your `BUILD_ID`. Besides `BUILD_ID`, Next.js automatically strips a lot of the other files we could use to enrich the `/info` endpoint (i.e.: `package.json` and `.git/`). Therefore, if you want to include any of those extra details, you'll need to manually include them in the `/info` response. The usage below outlines an example of how to do this with a combination of [`properties-reader`](https://npmjs.com/package/properties-reader) and [`node-git-info`](https://npmjs.com/package/node-git-info). ### Usage ```typescript import { createNextActuator } from 'next-actuator' import { name, version, description, author } from '@/package.json' import { join } from 'node:path' import propertiesReader from 'properties-reader' const { GET } = createNextActuator({ components: { internal, external }, info: async () => { const gitInfo = propertiesReader(join(process.cwd(), 'git.properties')) return { application: { name, version, description, author }, git: { time: gitInfo.get('git.commit.time'), branch: gitInfo.get('git.branch'), id: { full: gitInfo.getRaw('git.commit.id'), short: gitInfo.getRaw('git.commit.id.abbrev') }, message: { full: gitInfo.get('git.commit.message.full'), short: gitInfo.get('git.commit.message.short') }, author: { email: gitInfo.get('git.commit.user.email'), name: gitInfo.get('git.commit.user.name') } } } } }) export { GET } ``` ### Sample response ```json { "build": "cc31f8f", "application": { "name": "next-actuator-app", "version": "0.0.0", "description": "A fully composable actuator implementation for Next.js projects", "author": "Ross MacPhee (https://ross.software)" }, "git": { "time": "2024-11-26T12:00:00.000Z", "branch": "main", "id": { "full": "cc31f8f8838f24b9490660fa4f89470c9850be36", "short": "cc31f8f" }, "message": { "full": "Initial commit", "short": "Initial commit" }, "author": { "email": "r@acme.org", "name": "Ross MacPhee" } } } ```