Integrate Express with StatLite Metrics

September 22, 2026 ยท View on GitHub

This guide adds lightweight Node.js/Express application monitoring using the fixed statlite-metrics/v1 JSON profile and one small, dependency-light helper.

A runnable and tested demo accompanies the guide.

When to use this integration

Express does not have a first-class StatLite target type. Use this direct v1 integration when StatLite's fixed traffic, error, average-latency, status, restart, and process signals fit the application's operational needs. This dependency-light example supports one process or worker. Cluster mode, multi-process managers, and replica deployments are outside this drop-in integration.

StatLite cannot determine request counts, HTTP errors, or request latency from outside the application. The middleware below measures those values where requests and responses pass through Express.

The example was exercised with Node.js 24.21.0 LTS and Express 5.1.0. It uses stable Node.js process APIs and the standard Express middleware and response APIs, but this guide claims only that tested baseline rather than compatibility with every supported Express release.

Minimal dependency-light integration

Save this complete helper as statlite-metrics.js:

// Source and updates: https://github.com/PVRLabs/statlite/blob/main/docs/integrate/node/express.md

const { performance } = require("node:perf_hooks");

const METRICS_PATH = "/statlite/metrics";
const startedAt = new Date(Date.now() - process.uptime() * 1000);

const counters = {
  requestsTotal: 0,
  responses404Total: 0,
  responses4xxTotal: 0,
  responses5xxTotal: 0,
  requestDurationSecondsTotal: 0,
};

let previousCpu = process.cpuUsage();
let previousCpuTime = performance.now();

function record(statusCode, durationSeconds) {
  counters.requestsTotal += 1;
  counters.requestDurationSecondsTotal += durationSeconds;
  if (statusCode === 404) counters.responses404Total += 1;
  if (statusCode >= 400 && statusCode < 500) counters.responses4xxTotal += 1;
  if (statusCode >= 500 && statusCode < 600) counters.responses5xxTotal += 1;
}

function statliteMetricsMiddleware(req, res, next) {
  if (req.path === METRICS_PATH) return next();

  const requestStarted = performance.now();
  res.once("finish", () => {
    record(res.statusCode, (performance.now() - requestStarted) / 1000);
  });
  next();
}

function snapshot() {
  const now = performance.now();
  const cpu = process.cpuUsage();
  const elapsedSeconds = (now - previousCpuTime) / 1000;
  const cpuSeconds =
    (cpu.user - previousCpu.user + cpu.system - previousCpu.system) / 1e6;
  const processCpuUsage = elapsedSeconds > 0 ? cpuSeconds / elapsedSeconds : 0;
  previousCpu = cpu;
  previousCpuTime = now;

  return {
    schema: "statlite-metrics/v1",
    integration: "express",
    status: "UP",
    started_at: startedAt.toISOString(),
    metrics: {
      requests_total: counters.requestsTotal,
      responses_404_total: counters.responses404Total,
      responses_4xx_total: counters.responses4xxTotal,
      responses_5xx_total: counters.responses5xxTotal,
      request_duration_seconds_total: counters.requestDurationSecondsTotal,
      process_cpu_usage: processCpuUsage,
      runtime_heap_used_bytes: process.memoryUsage().heapUsed,
      uptime_seconds: process.uptime(),
    },
  };
}

function statliteMetricsEndpoint(req, res) {
  res.json(snapshot());
}

module.exports = {
  METRICS_PATH,
  statliteMetricsEndpoint,
  statliteMetricsMiddleware,
};

Register it before application routes so every application response is observed. Keep Express's error handler after the routes:

const express = require("express");
const {
  METRICS_PATH,
  statliteMetricsEndpoint,
  statliteMetricsMiddleware,
} = require("./statlite-metrics");

const app = express();

app.use(statliteMetricsMiddleware);
app.get(METRICS_PATH, statliteMetricsEndpoint);

app.get("/", (req, res) => res.json({ message: "hello" }));
app.get("/failure", (req, res, next) => next(new Error("example failure")));

app.use((err, req, res, next) => {
  console.error(err);
  if (res.headersSent) return next(err);
  res.status(500).json({ error: "internal server error" });
});

app.listen(3000, "127.0.0.1");

Existing-library path

No additional metrics-library path is recommended for this integration. A general instrumentation library would still need an adapter that emits the fixed v1 JSON fields. StatLite does not ingest arbitrary Prometheus or OpenMetrics output.

Endpoint behavior

GET /statlite/metrics returns a JSON snapshot with a successful 2xx response. The middleware excludes that path, including requests with a query string, so StatLite polling does not inflate application traffic or latency.

The finish listener runs once after Express and any later error middleware choose the final response status. It therefore counts completed normal, 404, 4xx, and 5xx responses without double-counting an error passed through next(err). Connections that close before a response finishes are not counted as completed responses.

A 404 increments both responses_404_total and responses_4xx_total, as the v1 contract defines 404 as a 4xx subset. Snapshot collection performs no database or network I/O.

Required, optional, and status fields

The response always includes the required schema: "statlite-metrics/v1" and a non-empty status. Its optional integration: "express" identifies the canonical helper that produced the response. The example's status: "UP" is the application's simple operational assertion at snapshot time. It does not assert that every dependency is healthy. A successful response proves reporting availability, not universal application or dependency health.

The helper omits database_status. Add it only from an authoritative, inexpensive or cached application signal. Do not query the database merely to serve a StatLite poll, and do not invent a value when no suitable signal exists.

All metrics fields and started_at are optional under v1. This helper emits cumulative response metrics, process CPU use in CPU cores, V8 heap use in bytes, process uptime, and an estimated process start time. V8 heapUsed is runtime-managed heap, not process RSS, container memory, or a maximum heap value. Unsupported host CPU, memory, and disk fields are omitted.

Configure StatLite

Save this as statlite.yaml:

targets:
  - name: "node-express-app"
    type: "statlite-metrics"
    url: "http://127.0.0.1:3000/statlite/metrics"

Application integration is still required. The YAML tells StatLite where to poll; it does not add the middleware or endpoint to Express.

Run and verify the integration

Install Express and start the application:

npm install express@5.1.0
node app.js

Leave the application running. In another terminal, generate normal, missing, and error responses:

curl -s http://127.0.0.1:3000/
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/missing
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/failure
curl -s http://127.0.0.1:3000/statlite/metrics

The snapshot should report three completed application requests, one 404 in both the 404 and 4xx counters, and one 5xx. Verify discovery separately:

statlite inspect 'http://127.0.0.1:3000/statlite/metrics'
statlite --config statlite.yaml

Open http://127.0.0.1:9090. Use a 30-second or longer polling interval in production.

Optional metrics and deployment caveats

This in-memory helper is supported by default only for a single Node.js process. Cluster mode and process managers give each worker separate counters and a separate started_at. When load-balanced polls alternate workers, counters can decrease and started_at can change, producing misleading deltas or apparent restarts rather than merely a partial aggregate. StatLite does not aggregate workers. Multi-process, cluster, and replica deployments are outside this helper's supported model. If you need one of those setups, open an issue or start a GitHub Discussion.

Add host fields only when the application can accurately describe its visible execution environment. Do not substitute process RSS for runtime_heap_used_bytes.

If a reverse proxy mounts the application below a prefix, ensure the configured URL reaches the same /statlite/metrics path that the middleware excludes. The statlite-metrics target type does not currently send target credentials, and statlite inspect does not accept authentication options. Make the endpoint reachable by StatLite through network or proxy access controls that do not require StatLite to authenticate. Do not place a Basic Auth or other credential challenge between StatLite and this endpoint. Restrict access because the endpoint exposes operational data.

References and future first-class support

Interested in a first-class Express target? Open an issue or start a GitHub Discussion. We're open to exploring it.