node-actuator-lite
v3.1.0
Published
Spring Boot Actuator for Node.js — health (shallow/deep), env, threaddump, heapdump, prometheus
Maintainers
Readme
Node Actuator Lite
Spring Boot Actuator for Node.js — production-ready monitoring endpoints with a single dependency.
Why?
If you're coming from Spring Boot, you expect /actuator/health, /actuator/env, and /actuator/prometheus out of the box. This library gives you exactly that for Node.js — one dependency, zero config, works with any framework or serverless.
Features
- Health — shallow (status only) and deep (per-component details), custom indicators, health groups (liveness / readiness)
- Environment —
process.envwith automatic sensitive-value masking - Thread Dump — event loop state, active handles/requests, V8 heap stats, worker threads
- Heap Dump — V8 heap snapshots saved to disk
- Prometheus — all default Node.js metrics + custom counters, gauges, histograms, summaries via
prom-client - Discovery —
GET /actuatorlists all enabled endpoints (like Spring Boot) - Dual mode — standalone HTTP server or serverless (direct method calls, no port needed)
- Single runtime dependency —
prom-client
Installation
npm install node-actuator-liteRequires Node.js >= 18.
Quick Start
Standalone (HTTP server)
import { NodeActuator } from 'node-actuator-lite';
const actuator = new NodeActuator({
port: 8081,
health: {
showDetails: 'always',
custom: [
{
name: 'database',
critical: true,
check: async () => ({ status: 'UP', details: { latency: '2ms' } }),
},
],
groups: {
liveness: ['process'],
readiness: ['diskSpace', 'database'],
},
},
prometheus: {
customMetrics: [
{ name: 'http_requests_total', help: 'Total HTTP requests', type: 'counter', labels: ['method', 'path'] },
],
},
});
await actuator.start();
// Actuator listening on http://localhost:8081/actuatorExpress (one-liner)
import express from 'express';
import { actuatorMiddleware } from 'node-actuator-lite';
const app = express();
const { handler, actuator } = actuatorMiddleware({ prometheus: { defaultMetrics: true } });
app.use(handler);
// All /actuator/* endpoints are now live.
// Access the actuator instance for custom metrics:
// actuator.prometheus.metric('my_counter')!.inc();
app.listen(3000);Fastify (plugin)
import Fastify from 'fastify';
import { actuatorPlugin } from 'node-actuator-lite';
const app = Fastify();
await app.register(actuatorPlugin, { prometheus: { defaultMetrics: true } });
// All /actuator/* routes registered. Access via app.actuator.
await app.listen({ port: 3000 });Serverless (Vercel, Lambda, etc.)
import { NodeActuator } from 'node-actuator-lite';
const actuator = new NodeActuator({ serverless: true });
await actuator.start(); // no-op, no server started
const health = await actuator.getHealth(); // shallow
const deep = await actuator.getHealth('always'); // deep
const prom = await actuator.getPrometheus();
const env = actuator.getEnv();
const threads = actuator.getThreadDump();
const heap = await actuator.getHeapDump();Endpoints
All endpoints live under the configured basePath (default /actuator).
| Method | Path | Description |
|--------|------|-------------|
| GET | /actuator | Discovery — lists all enabled endpoints |
| GET | /actuator/health | Health check (shallow or deep based on config) |
| GET | /actuator/health?showDetails=always | Force deep health check |
| GET | /actuator/health/{component} | Single health component |
| GET | /actuator/health/{group} | Health group (e.g. liveness, readiness) |
| GET | /actuator/env | Environment variables (masked) |
| GET | /actuator/env/{name} | Single environment variable |
| GET | /actuator/threaddump | Thread / event-loop dump |
| POST | /actuator/heapdump | Generate and save a V8 heap snapshot |
| GET | /actuator/prometheus | Prometheus metrics (text exposition format) |
Configuration
interface ActuatorOptions {
port?: number; // default 0 (random)
basePath?: string; // default '/actuator'
serverless?: boolean; // default false
health?: {
enabled?: boolean; // default true
showDetails?: 'never' | 'always'; // default 'always'
timeout?: number; // per-indicator timeout in ms, default 5000
indicators?: {
diskSpace?: { enabled?: boolean; threshold?: number; path?: string };
process?: { enabled?: boolean };
};
groups?: Record<string, string[]>; // e.g. { liveness: ['process'], readiness: ['diskSpace', 'db'] }
custom?: Array<{
name: string;
check: () => Promise<{ status: 'UP' | 'DOWN' | 'OUT_OF_SERVICE' | 'UNKNOWN'; details?: Record<string, any> }>;
critical?: boolean; // if true, DOWN here → overall DOWN
}>;
};
env?: {
enabled?: boolean; // default true
mask?: {
patterns?: string[]; // default ['PASSWORD','SECRET','KEY','TOKEN','AUTH','CREDENTIAL','PRIVATE','SIGNATURE']
additional?: string[]; // extra variable names to mask
replacement?: string; // default '******'
};
};
threadDump?: { enabled?: boolean }; // default true
heapDump?: {
enabled?: boolean; // default true
outputDir?: string; // default './heapdumps'
};
prometheus?: {
enabled?: boolean; // default true
defaultMetrics?: boolean; // collect default Node.js metrics, default true
prefix?: string;
customMetrics?: Array<{
name: string;
help: string;
type: 'counter' | 'gauge' | 'histogram' | 'summary';
labels?: string[];
buckets?: number[]; // histogram only
}>;
};
}Health — Shallow vs Deep
Shallow (showDetails: 'never' or default GET /actuator/health):
{ "status": "UP" }Deep (showDetails: 'always' or GET /actuator/health?showDetails=always):
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": { "total": 499963174912, "free": 250000000000, "threshold": 10485760, "path": "/" }
},
"process": {
"status": "UP",
"details": { "pid": 12345, "uptime": 3600, "version": "v20.11.0" }
},
"database": {
"status": "UP",
"details": { "latency": "2ms" }
}
}
}Health Groups
Model Kubernetes liveness and readiness probes:
const actuator = new NodeActuator({
health: {
groups: {
liveness: ['process'],
readiness: ['diskSpace', 'database'],
},
},
});GET /actuator/health/liveness→ aggregated status ofprocessonlyGET /actuator/health/readiness→ aggregated status ofdiskSpace+database
Returns HTTP 200 when UP, 503 when DOWN.
Custom Health Indicators
const actuator = new NodeActuator({
health: {
custom: [
{
name: 'redis',
critical: true,
check: async () => {
const ok = await redis.ping();
return ok
? { status: 'UP', details: { latency: '1ms' } }
: { status: 'DOWN', details: { error: 'ping failed' } };
},
},
],
},
});Add/remove at runtime:
actuator.health.addIndicator({ name: 'cache', check: async () => ({ status: 'UP' }) });
actuator.health.removeIndicator('cache');Environment
The /env endpoint returns a Spring-style property-source response:
{
"activeProfiles": ["production"],
"propertySources": [
{
"name": "systemEnvironment",
"properties": {
"PATH": { "value": "/usr/local/bin:..." },
"DATABASE_PASSWORD": { "value": "******" }
}
},
{
"name": "systemProperties",
"properties": {
"node.version": { "value": "v20.11.0" },
"os.hostname": { "value": "my-server" }
}
}
]
}Masking
Sensitive values are masked by default. Customise patterns:
const actuator = new NodeActuator({
env: {
mask: {
patterns: ['PASSWORD', 'SECRET', 'KEY', 'TOKEN'],
additional: ['MY_CUSTOM_VAR'],
replacement: '[REDACTED]',
},
},
});Runtime management:
actuator.env.addMaskPattern('STRIPE');
actuator.env.addMaskVariable('SPECIAL_KEY');
actuator.env.removeMaskPattern('KEY');Prometheus
Default Node.js metrics (CPU, memory, event loop lag, GC, etc.) are collected automatically. Add custom metrics:
const actuator = new NodeActuator({
prometheus: {
customMetrics: [
{ name: 'http_requests_total', help: 'Total requests', type: 'counter', labels: ['method', 'status'] },
{ name: 'request_duration_seconds', help: 'Request duration', type: 'histogram', buckets: [0.01, 0.05, 0.1, 0.5, 1] },
{ name: 'active_connections', help: 'Active connections', type: 'gauge' },
],
},
});
await actuator.start();
// Use metrics
const counter = actuator.prometheus.metric('http_requests_total');
counter.inc({ method: 'GET', status: '200' });
const gauge = actuator.prometheus.metric('active_connections');
gauge.set(42);Access the raw prom-client registry:
actuator.prometheus.getRegistry();Thread Dump
GET /actuator/threaddump returns:
{
"timestamp": "2025-01-15T10:30:00.000Z",
"pid": 12345,
"nodeVersion": "v20.11.0",
"platform": "linux",
"uptime": 3600,
"mainThread": { "name": "main", "state": "RUNNABLE", "cpuUsage": { "user": 500000, "system": 100000 } },
"eventLoop": {
"activeHandles": { "count": 5, "types": ["Server", "Socket", "Timer"] },
"activeRequests": { "count": 0, "types": [] }
},
"workers": [],
"memory": { "rss": 52428800, "heapTotal": 20971520, "heapUsed": 15728640, "external": 1048576 },
"v8HeapStats": { "total_heap_size": 20971520, "used_heap_size": 15728640, "..." : "..." }
}Heap Dump
POST /actuator/heapdump generates a V8 heap snapshot and saves it to disk:
{
"timestamp": "2025-01-15T10:30:00.000Z",
"pid": 12345,
"filePath": "./heapdumps/heapdump-2025-01-15T10-30-00-000Z-a1b2c3d4.heapsnapshot",
"fileSize": 15728640,
"duration": 1250,
"memoryBefore": { "heapUsed": 15728640 },
"memoryAfter": { "heapUsed": 16777216 }
}Open the .heapsnapshot file in Chrome DevTools → Memory → Load.
Serverless Integration
Vercel
// api/actuator/[...path].js
import { NodeActuator } from 'node-actuator-lite';
const actuator = new NodeActuator({ serverless: true });
export default async function handler(req, res) {
const segments = req.query['...path'];
const path = Array.isArray(segments) ? segments.join('/') : segments || '';
switch (path) {
case '':
return res.json(actuator.discovery());
case 'health':
return res.json(await actuator.getHealth());
case 'env':
return res.json(actuator.getEnv());
case 'threaddump':
return res.json(actuator.getThreadDump());
case 'prometheus':
res.setHeader('Content-Type', 'text/plain');
return res.send(await actuator.getPrometheus());
default:
return res.status(404).json({ error: 'Not found' });
}
}AWS Lambda
import { NodeActuator } from 'node-actuator-lite';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
const actuator = new NodeActuator({ serverless: true });
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const path = event.pathParameters?.proxy || '';
const routes: Record<string, () => Promise<{ code: number; type: string; body: string }>> = {
health: async () => ({ code: 200, type: 'application/json', body: JSON.stringify(await actuator.getHealth()) }),
env: async () => ({ code: 200, type: 'application/json', body: JSON.stringify(actuator.getEnv()) }),
prometheus: async () => ({ code: 200, type: 'text/plain', body: await actuator.getPrometheus() }),
threaddump: async () => ({ code: 200, type: 'application/json', body: JSON.stringify(actuator.getThreadDump()) }),
};
const route = routes[path];
if (!route) return { statusCode: 404, body: '{"error":"Not found"}' };
const result = await route();
return { statusCode: result.code, headers: { 'Content-Type': result.type }, body: result.body };
};Programmatic API
All data is available via methods — no HTTP server required.
const actuator = new NodeActuator({ serverless: true });
// Discovery
actuator.discovery();
// Health
await actuator.getHealth(); // shallow or deep (based on config)
await actuator.getHealth('always'); // force deep
await actuator.getHealthComponent('diskSpace');
await actuator.getHealthGroup('readiness');
// Environment
actuator.getEnv();
actuator.getEnvVariable('NODE_ENV');
// Thread dump
actuator.getThreadDump();
// Heap dump
await actuator.getHeapDump();
// Prometheus
await actuator.getPrometheus();Contributing
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
License
MIT — see LICENSE.
