hono-status-monitor
v1.0.9
Published
Real-time server monitoring dashboard for Hono.js. Works with Node.js, Bun, and Cloudflare Workers. Express-status-monitor style metrics with polling updates.
Downloads
18,580
Maintainers
Readme
hono-status-monitor
Real-time monitoring dashboard for Hono.js — one middleware, a zero-dependency dashboard, plus Prometheus, health-check and SSE endpoints. Runs on Node.js, Bun, and Cloudflare Workers / Edge.
Trusted by the Hono community: 4,000+ weekly npm downloads and growing.
Features
- Live metrics — CPU, memory, heap, load, response time, RPS, event-loop lag (real
perf_hookshistogram) + GC stats. - Analytics — P50/P95/P99 latency, top / slowest / error routes, status-code breakdown, recent errors.
- Endpoints — HTML dashboard, JSON API,
/prometheusscrape,/health(200/503),/api/streamSSE. - Auth hook, alert callbacks, multiple named health checks, dark mode, cluster (PM2) aggregation.
- Safe by default — route paths are HTML-escaped (no stored XSS), route map is LRU-capped (no unbounded memory growth).
Runtime support
| Runtime | Import | Server | Metrics |
|---|---|---|---|
| Node.js | hono-status-monitor | @hono/node-server | Full system + request |
| Bun | hono-status-monitor | Bun.serve | Full system + request |
| Cloudflare / Edge | hono-status-monitor/edge | runtime default | Request-only (no CPU/mem/heap) |
Install
npm install hono-status-monitor # + npm install @hono/node-server for NodeQuick start (Node.js / Bun)
import { Hono } from 'hono';
import { serve } from '@hono/node-server'; // omit for Bun
import { statusMonitor } from 'hono-status-monitor';
const app = new Hono();
const monitor = statusMonitor();
app.use('*', monitor.middleware); // must be first — tracks all requests
app.route('/status', monitor.routes); // mount dashboard + endpoints
app.get('/', (c) => c.text('Hello World!'));
serve({ fetch: app.fetch, port: 3000 }); // or Bun.serve({ fetch: app.fetch, port: 3000 })
// → dashboard at http://localhost:3000/statusIf you mount at a non-default path, set
pathto match (e.g.statusMonitor({ path: '/mystatus' })) so the dashboard's own polling isn't counted as traffic.
Cloudflare Workers / Edge
Use the /edge entry (zero Node.js deps — the main entry pulls in os/cluster and won't bundle for Workers):
import { Hono } from 'hono';
import { statusMonitor } from 'hono-status-monitor/edge';
const app = new Hono();
const monitor = statusMonitor({ pollingInterval: 3000 });
app.use('*', monitor.middleware);
app.route('/status', monitor.routes);
export default app;Edge exposes request metrics only (CPU/memory/heap/event-loop/load and the SSE stream are unavailable). Each isolate keeps its own counters.
Endpoints
| Endpoint | Description |
|---|---|
| GET /status | Dashboard HTML |
| GET /status/api/metrics | { snapshot, charts } JSON |
| GET /status/api/stream | SSE stream of the same JSON (Node/Bun only) |
| GET /status/health | { status, uptime, checks } — 200 if all checks pass, 503 if degraded |
| GET /status/prometheus | Prometheus/OpenMetrics text (disable via prometheus: false) |
Configuration
statusMonitor({
path: '/status', // mount path (keep in sync with app.route)
title: 'My App Status',
pollingInterval: 1000, // dashboard refresh ms (Node 1000 / edge 5000) — now honored on Node too
updateInterval: 1000, // metrics sampling ms
retentionSeconds: 60, // chart history window
maxRecentErrors: 10,
maxRoutes: 10, // routes shown in analytics
maxTrackedRoutes: 1000, // hard cap on distinct routes in memory (LRU eviction)
alerts: { cpu: 80, memory: 90, responseTime: 500, errorRate: 5, eventLoopLag: 100 },
// Fired once on each OK <-> breached transition (wire to Slack/webhooks)
onAlert: (e) => console.warn(`${e.metric} ${e.active ? 'ALERT' : 'recovered'} @ ${e.value}`),
// Guard the whole /status surface; falsy return => 401
authorize: (c) => c.req.header('x-admin-token') === process.env.ADMIN_TOKEN,
// One or many named health checks (surfaced on /health and the dashboard)
healthChecks: {
mongo: async () => ({ connected: mongoose.connection.readyState === 1, latencyMs: 2 }),
redis: async () => ({ connected: await redis.ping() === 'PONG', latencyMs: 1 }),
},
prometheus: true, // expose /prometheus
prometheusPrefix: 'hono', // metric name prefix
chartjsUrl: '/vendor/chart.umd.js', // self-host Chart.js under a strict CSP
normalizePath: (p) => p.replace(/\/users\/\d+/g, '/users/:id'),
});Instance methods: getMetrics(), getCharts(), getHealth(), trackRateLimit(blocked), resetStats(), stop(), plus monitor (underlying instance).
Prometheus / Grafana
Scrape /status/prometheus — emits <prefix>_cpu_percent, _heap_used_bytes, _rps, _response_time_p95_ms, _requests_total, _http_responses_total{code="..."}, etc.
scrape_configs:
- job_name: my-app
metrics_path: /status/prometheus
static_configs: [{ targets: ['localhost:3000'] }]Rate-limit tracking
if (isRateLimited) { monitor.trackRateLimit(true); return c.text('Too many requests', 429); }
monitor.trackRateLimit(false);PM2 / Cluster mode
Metrics aggregate across workers with no Redis. In your cluster entry file, call setupClusterPrimary() in the primary — it relays worker metrics to every worker and respawns dead ones:
import cluster from 'node:cluster';
import * as os from 'node:os';
import { setupClusterPrimary } from 'hono-status-monitor';
if (cluster.isPrimary) {
for (let i = 0; i < os.cpus().length; i++) cluster.fork();
setupClusterPrimary(); // relay + auto-respawn
} else {
await import('./server.js'); // your app (auto-detects worker mode)
}pm2 start cluster.js --name my-app # single PM2 instance; the script forks workersDon't use pm2 start app.js -i max directly — isolated instances can't share IPC.
Security
The dashboard exposes hostname, PID, routes and errors. Protect it in production:
// Built-in guard
statusMonitor({ authorize: (c) => c.req.header('x-token') === process.env.STATUS_TOKEN });
// …or Hono basic-auth
import { basicAuth } from 'hono/basic-auth';
app.use('/status/*', basicAuth({ username: 'admin', password: process.env.STATUS_PASSWORD! }));
app.route('/status', monitor.routes);Route paths are HTML-escaped before rendering, so hostile request paths can't inject scripts into the dashboard.
The status surface is public by default — anyone who can reach the mounted path gets the dashboard, /api/metrics, /api/stream, /prometheus and /health. Set authorize (or front it with your own auth) in any environment where that's not acceptable. Note that when authorize is set it also gates /health; if a load balancer or k8s liveness probe hits /health unauthenticated, either exempt that path in your own middleware or point the probe at an unguarded route.
Notes for existing users (1.0.9)
All changes are backward-compatible with the documented statusMonitor() factory. Two things worth a glance if you depend on internals:
getDatabaseStats/databasein the snapshot now reports real pool numbers from yourhealthCheck'sdetails.poolSize/details.availableConnections, falling back to0instead of the previous hardcoded10. If your dashboards keyed off the old constant, surface the real values viahealthCheck.- The exported
StatusMonitortype dropped three members that the factory never actually returned (start,getDashboard,config) and addedgetHealth,resetStats,isEdgeMode,routes. Runtime behavior is unchanged; only hand-written: StatusMonitorannotations against the old shape need updating.
Requirements
Node ≥ 18 · Bun ≥ 1.0 · Hono ≥ 4.0 · @hono/node-server ≥ 1.0 (Node only).
License
MIT © Vinit Kumar Goel
Pairing app telemetry with external risk awareness? ThreatFrontier tracks emerging CVEs and exploitation trends — a solid cybersecurity news source for security teams.
