Giám sát ứng dụng Node.js với prom-client: Request Rate, Latency và Custom Business Metrics theo thời gian thực

Monitoring tutorial - IT technology blog
Monitoring tutorial - IT technology blog

Bạn có bao giờ nhận điện thoại lúc 2 giờ sáng vì API bị chậm mà không biết nguyên nhân ở đâu không? Mình từng như vậy — SSH vào từng server kiểm tra log, chạy top, netstat, mà vẫn không ra vấn đề. Từ khi tích hợp prom-client + Grafana, mọi thứ thay đổi hẳn. Chỉ cần mở dashboard là thấy ngay: request rate đang bao nhiêu, endpoint nào chậm, error rate là mấy phần trăm.

Blog đã có bài cài Prometheus + Grafana để monitor server (CPU, RAM, disk). Bài này đi sâu hơn một tầng: giám sát ở application level — tức là chính code Node.js của bạn đang làm gì, endpoint nào bị bottleneck, và business logic có đang chạy đúng không.

3 cách giám sát Node.js — so sánh trước khi chọn

Không phải lúc nào prom-client cũng là đáp án đúng. Dưới đây là 3 hướng phổ biến — mỗi cái có lý do tồn tại riêng.

Cách 1: Chỉ dùng log + tìm thủ công

Ghi log request/response ra file, dùng grep, awk hoặc Graylog để phân tích sau.

  • Ưu điểm: Không cần setup thêm gì, log có sẵn rồi, dễ debug lỗi cụ thể
  • Nhược điểm: Không thấy được trend theo thời gian, không có alerting real-time, phân tích thủ công tốn thời gian — nhất là khi sự cố xảy ra lúc 3 giờ sáng

Cách 2: APM commercial (Datadog, New Relic, Dynatrace)

Cài agent, tự động trace mọi thứ, dashboard đẹp sẵn ngay từ đầu.

  • Ưu điểm: Cực kỳ dễ setup, có distributed tracing, anomaly detection, không cần tự quản lý hạ tầng
  • Nhược điểm: Chi phí cao (Datadog từ $15/host/tháng, chưa kể $0.10/GB data ingested), vendor lock-in, không tự định nghĩa được metrics theo business logic riêng

Cách 3: prom-client + Prometheus + Grafana (self-hosted)

Bạn tự expose metrics từ code, Prometheus scrape định kỳ, Grafana visualize và alert.

  • Ưu điểm: Hoàn toàn miễn phí, full control, tự định nghĩa metrics theo ý muốn, cộng đồng lớn, tích hợp tốt với Kubernetes
  • Nhược điểm: Cần biết PromQL để query, tự quản lý hạ tầng Prometheus + Grafana

Tại sao chọn prom-client?

Nếu dự án đã có Prometheus rồi (hoặc đang tính cài), prom-client là lựa chọn tự nhiên nhất. APM commercial phù hợp cho team lớn với budget cao, cần distributed tracing phức tạp. Với startup, side project, hoặc khi bạn muốn track chính xác business metrics theo cách riêng — prom-client + Grafana là đủ dùng và free hoàn toàn.

prom-client có 4 loại metric. Counter chỉ tăng — dùng đếm request, lỗi. Gauge tăng giảm tùy — active connections, memory. Histogram cho phân phối — request duration. Summary tính quantile phía client. Với web API, Counter và Histogram là hai cái bạn sẽ đụng nhiều nhất.

Tích hợp prom-client vào Express.js — từng bước

Bước 1: Cài package

npm install prom-client

Bước 2: Khởi tạo metrics trong file riêng

Tách logic monitoring ra file metrics.js riêng để không lẫn vào business code:

// metrics.js
const client = require('prom-client');

const register = new client.Registry();

// Thu thập default metrics của Node.js (memory heap, event loop lag, GC...)
client.collectDefaultMetrics({ register });

// Counter: đếm tổng số HTTP request
const httpRequestsTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
});

// Histogram: phân phối thời gian xử lý request (latency)
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
  registers: [register],
});

// Business metric: đếm đơn hàng tạo ra (ví dụ)
const ordersCreatedTotal = new client.Counter({
  name: 'orders_created_total',
  help: 'Total number of orders created',
  labelNames: ['status', 'payment_method'],
  registers: [register],
});

// Gauge: số user đang active (có thể tăng/giảm)
const activeUsers = new client.Gauge({
  name: 'active_users_current',
  help: 'Number of currently active users',
  registers: [register],
});

module.exports = { register, httpRequestsTotal, httpRequestDuration, ordersCreatedTotal, activeUsers };

Bước 3: Middleware tự động track mọi HTTP request

// middleware/metricsMiddleware.js
const { httpRequestsTotal, httpRequestDuration } = require('../metrics');

function metricsMiddleware(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    // req.route.path cho ra pattern như /api/users/:id thay vì /api/users/123
    const route = req.route ? req.route.path : req.path;
    const labels = { method: req.method, route, status_code: res.statusCode };

    httpRequestsTotal.inc(labels);
    httpRequestDuration.observe(labels, duration);
  });

  next();
}

module.exports = metricsMiddleware;

Bước 4: Đăng ký middleware và expose /metrics

// app.js
const express = require('express');
const { register } = require('./metrics');
const metricsMiddleware = require('./middleware/metricsMiddleware');

const app = express();
app.use(express.json());
app.use(metricsMiddleware);

// Prometheus scrape endpoint — KHÔNG để public, xem lưu ý bên dưới
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

app.get('/api/orders', (req, res) => {
  res.json({ orders: [] });
});

app.listen(3000, () => console.log('Server :3000 | Metrics: :3000/metrics'));

Bước 5: Track custom business metrics ngay trong route handler

Đây là chỗ prom-client tỏa sáng so với monitoring generic — bạn track được chính xác logic nghiệp vụ của riêng mình:

// routes/orders.js
const { ordersCreatedTotal, activeUsers } = require('../metrics');

app.post('/api/orders', async (req, res) => {
  try {
    const order = await createOrder(req.body);
    ordersCreatedTotal.inc({ status: 'success', payment_method: order.paymentMethod });
    res.json({ success: true, orderId: order.id });
  } catch (err) {
    ordersCreatedTotal.inc({ status: 'failed', payment_method: req.body.paymentMethod || 'unknown' });
    res.status(500).json({ error: err.message });
  }
});

app.post('/api/login', async (req, res) => {
  // ... auth logic
  activeUsers.inc();
  res.json({ token: '...' });
});

app.post('/api/logout', (req, res) => {
  activeUsers.dec();
  res.json({ success: true });
});

Cấu hình Prometheus scrape Node.js app

Mở file prometheus.yml, thêm job mới song song với job node-exporter đã có:

scrape_configs:
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['localhost:9100']

  # Job mới cho Node.js application
  - job_name: 'nodejs-app'
    static_configs:
      - targets: ['localhost:3000']
    metrics_path: '/metrics'
    scrape_interval: 15s

Reload Prometheus config (không cần restart):

curl -X POST http://localhost:9090/-/reload

PromQL queries cho Grafana Dashboard

Prometheus đã scrape xong, giờ là lúc xây dashboard. 4 panel dưới đây là đủ để có cái nhìn toàn cảnh về sức khỏe API:

Request Rate (requests/giây)

sum(rate(http_requests_total[5m])) by (route, method)

P95 Latency — metric quan trọng nhất

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route))

P95 nghĩa là 95% request hoàn thành trong thời gian X. Thực tế hơn average nhiều — average dễ bị kéo bởi những request nhanh và che giấu những request thực sự chậm.

Error Rate (% lỗi 5xx)

rate(http_requests_total{status_code=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100

Business Metric: Tỷ lệ đơn hàng thành công

rate(orders_created_total{status="success"}[5m]) / rate(orders_created_total[5m]) * 100

Kiểm tra nhanh trước khi kết nối Grafana

# Chạy app
node app.js

# Gửi vài request test
curl http://localhost:3000/api/orders
curl -X POST http://localhost:3000/api/orders \
  -H "Content-Type: application/json" \
  -d '{"item":"product-1","paymentMethod":"card"}'

# Xem raw metrics output
curl http://localhost:3000/metrics

Nếu thấy output dạng này là ổn:

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",route="/api/orders",status_code="200"} 3
http_requests_total{method="POST",route="/api/orders",status_code="200"} 1

# HELP http_request_duration_seconds Duration of HTTP requests in seconds  
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005",...} 2
http_request_duration_seconds_bucket{le="0.01",...} 4

Những lưu ý từ thực tế

  • Không dùng user_id làm label: Label phải có cardinality thấp. Dùng user_id hay request_id làm label sẽ tạo hàng triệu time series — Prometheus tràn bộ nhớ rất nhanh. Method, route, status_code là an toàn.
  • Bảo vệ /metrics endpoint: Đừng để public. Dùng Basic Auth, whitelist IP nội bộ, hoặc bind metrics server ra port riêng chỉ Prometheus nội bộ mới reach được. Endpoint này tiết lộ khá nhiều thông tin về hạ tầng.
  • scrape_interval 15s là đủ: Đừng cài 5s hay thấp hơn nếu không có lý do cụ thể — tăng load không cần thiết cho cả app lẫn Prometheus.
  • Test route normalization: Với nested Express router, req.route.path có thể trả về path tương đối. Test kỹ để chắc /api/users/:id không bị lẫn thành /api/users/123.

Có metrics trong Grafana rồi, bước tiếp là setup alert — ví dụ cảnh báo khi P95 latency vượt 500ms, hoặc error rate > 5% liên tục 5 phút. Alertmanager đã có bài riêng trên blog; kết hợp với dashboard này là có vòng lặp giám sát hoàn chỉnh.

Share: