ZyVOP Logo
Content That Connects
SeriesCategoriesTags
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Tags
  • Write Article

Company

  • About Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeYou Don't Have a Logging Problem. You Have an Observability Problem.

You Don't Have a Logging Problem. You Have an Observability Problem.

Structured logging with Pino, metrics with Prometheus, and error tracking with Sentry — the three pillars that turn a 2-hour outage into an 8-minute fix

#Node.js observability#Pino structured logging#Prometheus Node.js#Grafana VPS#Sentry Express#equest ID correlation#Node.js logging production#metrics Express 2026
Z
ZyVOP

Senior Developer

May 25, 2026
8 min read
6 views
You Don't Have a Logging Problem. You Have an Observability Problem.

Every Node.js app has console.log in it. Most production apps have a lot of console.log in them. And when something breaks at 2 AM, developers find themselves scrolling through thousands of lines of unstructured text, grepping for clues, and hoping the error message is somewhere in there.

This is not a logging problem. It is an observability problem — the difference between knowing your app is running and understanding what it is doing.

Observability has three pillars: logs, metrics, and traces. This guide covers all three, practically, for a Node.js app on a VPS. No Kubernetes required. No $500/month observability platform required.


Pillar 1 — Structured Logging with Pino

console.log outputs strings. Strings are hard to search, filter, or aggregate. Structured logging outputs JSON — machine-readable lines that your logging infrastructure can query, filter, and alert on.

Pino is the fastest Node.js logging library and the one most production teams use.

npm install pino pino-pretty
// src/lib/logger.js
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',

  // In production: output raw JSON
  // In development: pretty-print for readability
  transport: process.env.NODE_ENV !== 'production'
    ? {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'HH:MM:ss',
          ignore: 'pid,hostname',
        },
      }
    : undefined,

  // Base fields added to every log line
  base: {
    service: 'your-app-name',
    version: process.env.APP_VERSION || 'unknown',
    env: process.env.NODE_ENV,
  },

  // Redact sensitive fields — they will show as [Redacted] in logs
  redact: {
    paths: [
      'req.headers.authorization',
      'req.headers.cookie',
      'body.password',
      'body.token',
      '*.creditCard',
      '*.ssn',
    ],
    censor: '[Redacted]',
  },
});

export default logger;

Usage — structured, not string-concatenated:

import logger from '../lib/logger.js';

// Bad — unstructured, unsearchable
console.log('User ' + userId + ' logged in from ' + ip);

// Good — structured, filterable, correlated
logger.info({ userId, ip, action: 'login' }, 'User logged in');

// With error details
logger.error({
  userId,
  error: err.message,
  stack: err.stack,
  context: 'payment-processing',
}, 'Payment failed');

Now you can query your logs: "show me all login events from this IP" or "show me all payment failures in the last hour" — in milliseconds.


Pillar 2 — Request Logging and Correlation IDs

Every request should produce a log entry with a unique correlation ID. When a user reports a problem, you ask them for their request ID and pull up everything that happened during that request — across every service.

npm install uuid
// src/middleware/requestLogger.js
import { randomUUID } from 'crypto';
import logger from '../lib/logger.js';

export function requestLogger(req, res, next) {
  const requestId = req.headers['x-request-id'] || randomUUID();
  const startTime = Date.now();

  // Attach request ID to request and response
  req.id = requestId;
  res.setHeader('X-Request-ID', requestId);

  // Create a child logger with request context
  // Every log from this request will include these fields
  req.log = logger.child({
    requestId,
    method: req.method,
    url: req.originalUrl,
    ip: req.ip,
    userAgent: req.headers['user-agent'],
  });

  req.log.info('Request started');

  // Log on response finish
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    const level = res.statusCode >= 500 ? 'error'
      : res.statusCode >= 400 ? 'warn'
      : 'info';

    req.log[level]({
      statusCode: res.statusCode,
      duration,
      contentLength: res.getHeader('content-length'),
    }, 'Request completed');
  });

  next();
}

In your route handlers, use req.log instead of the base logger so the request context is included automatically:

router.post('/orders', authenticate, async (req, res) => {
  req.log.info({ userId: req.user.id }, 'Creating order');

  try {
    const order = await createOrder(req.body, req.user.id);
    req.log.info({ orderId: order.id, total: order.total }, 'Order created');
    res.status(201).json(order);
  } catch (err) {
    req.log.error({ error: err.message, stack: err.stack }, 'Order creation failed');
    res.status(500).json({ error: 'Order failed', requestId: req.id });
  }
});

When the user calls support and says "my order failed", you have their requestId. One query finds every log line from that request.


Pillar 3 — Metrics with Prometheus

Metrics answer the question: "How is my system behaving right now and over time?" Logs tell you what happened. Metrics tell you the shape of what is happening.

npm install prom-client
// src/lib/metrics.js
import client from 'prom-client';

// Collect default Node.js metrics (memory, CPU, event loop lag, etc.)
client.collectDefaultMetrics({
  prefix: 'app_',
  labels: { service: 'your-app-name' },
});

// HTTP request duration histogram
export const httpRequestDuration = new client.Histogram({
  name: 'app_http_request_duration_seconds',
  help: 'HTTP request duration 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],
});

// Active connections gauge
export const activeConnections = new client.Gauge({
  name: 'app_active_connections',
  help: 'Number of active HTTP connections',
});

// Business metrics — track what matters to your product
export const ordersCreated = new client.Counter({
  name: 'app_orders_created_total',
  help: 'Total orders created',
  labelNames: ['status'],
});

export const queueDepth = new client.Gauge({
  name: 'app_queue_depth',
  help: 'Number of jobs waiting in queue',
  labelNames: ['queue'],
});

export const registry = client.register;
// Middleware to record request metrics
export function metricsMiddleware(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const route = req.route?.path || req.path || 'unknown';

    httpRequestDuration
      .labels(req.method, route, res.statusCode)
      .observe(duration);
  });

  next();
}
// Expose metrics endpoint for Prometheus to scrape
// Protect this endpoint — metrics can reveal system internals
router.get('/metrics',
  basicAuth({ users: { prometheus: process.env.METRICS_PASSWORD } }),
  async (req, res) => {
    res.set('Content-Type', registry.contentType);
    res.end(await registry.metrics());
  }
);
// Record business metrics in your handlers
import { ordersCreated } from '../lib/metrics.js';

router.post('/orders', authenticate, async (req, res) => {
  try {
    const order = await createOrder(req.body, req.user.id);
    ordersCreated.labels('success').inc();
    res.status(201).json(order);
  } catch (err) {
    ordersCreated.labels('failed').inc();
    res.status(500).json({ error: 'Order failed' });
  }
});

The Prometheus + Grafana Stack on a VPS

Add this to your docker-compose.yml:

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
    networks:
      - backend
    # Not exposed publicly — only accessible via Grafana

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3001:3000"     # Accessible at yourdomain.com:3001 (or behind Nginx)
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
      GF_USERS_ALLOW_SIGN_UP: "false"
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana/provisioning:/etc/grafana/provisioning
    depends_on:
      - prometheus
    networks:
      - backend

volumes:
  prometheus_data:
  grafana_data:

monitoring/prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'your-app'
    static_configs:
      - targets: ['app:3000']
    metrics_path: '/metrics'
    basic_auth:
      username: 'prometheus'
      password: 'your-metrics-password'

Pillar 4 — Error Tracking

Logs and metrics tell you things are broken. Error tracking tells you exactly what broke, how many users were affected, and whether it is getting worse.

Sentry is the standard choice. The setup takes five minutes:

npm install @sentry/node
// src/lib/sentry.js — initialize before anything else
import * as Sentry from '@sentry/node';

if (process.env.SENTRY_DSN) {
  Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    release: process.env.APP_VERSION,

    // Only send 20% of transactions to stay on the free tier
    tracesSampleRate: 0.2,

    // Filter out noise
    ignoreErrors: [
      'TokenExpiredError',      // Expected auth errors
      'NotFoundError',          // Expected 404s
    ],
  });
}

export { Sentry };
// In your Express app — Sentry handlers must be in specific positions
import { Sentry } from './lib/sentry.js';

// Must be first middleware
app.use(Sentry.Handlers.requestHandler());

// Your routes
app.use('/api', apiRouter);

// Must be before your error handler
app.use(Sentry.Handlers.errorHandler());

// Your error handler
app.use((err, req, res, next) => {
  req.log?.error({ error: err.message, sentry_id: res.sentry }, 'Unhandled error');
  res.status(500).json({
    error: 'Internal server error',
    requestId: req.id,
  });
});

Add user context so errors are linked to affected users:

// In your authenticate middleware, after verifying the token:
Sentry.setUser({ id: req.user.id });

What Good Observability Looks Like in Practice

When something breaks in production, here is the workflow:

  1. Alert fires — Prometheus detects error rate above 1% for 5 minutes and fires a webhook to your Slack

  2. Check the dashboard — Grafana shows the spike started at 14:32, affects the /orders endpoint, latency jumped from 50ms to 3s

  3. Check Sentry — A new error appeared at 14:32: Connection pool exhausted in order.service.js:47

  4. Pull the logs — Filter by url: /orders AND level: error since 14:30, find 10 request IDs

  5. Trace one request — Pull logs for one requestId, see the full timeline of what happened during that request

  6. Root cause — A deploy at 14:30 introduced a missing await in a transaction, leaving connections open

Total time from alert to root cause: 8 minutes. Without structured logs, metrics, and error tracking: 2 hours and a lot of grep.


The Quick Start Checklist

// In order of setup priority:

// 1. Structured logging — replaces console.log
import logger from './lib/logger.js';

// 2. Request ID middleware — correlation across services
app.use(requestLogger);

// 3. Metrics middleware
app.use(metricsMiddleware);

// 4. Sentry error tracking
app.use(Sentry.Handlers.requestHandler());

// 5. Your routes
app.use('/api', router);

// 6. Sentry error handler
app.use(Sentry.Handlers.errorHandler());

// 7. Your error handler
app.use(errorHandler);

Start with structured logging and request IDs. Add Prometheus and Grafana when you have more than one service or when on-call debugging starts taking more than 30 minutes. Add Sentry from day one — it is free for small volumes and pays for itself the first time it catches a bug before a user reports it.

Z

ZyVOP

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Stay Updated

Get the latest articles delivered to your inbox.

We respect your privacy. Unsubscribe anytime.

Popular Tags

#.env.example Node.js#0x profiling#12-factor#AI agents#AI code security#AI coding tools 2026#AI-assisted development#AI-generated vulnerabilities#ALTER TABLE no lock#API Design