ZyVOP Logo
Content That Connects
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZyVOP Logo
Content That Connects

The Developer Publishing Hub. Write once, cross-post to Dev.to, Medium, Hashnode, WordPress & Bluesky with automated canonical source tags and zero paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

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

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
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

ZyVOP
ZyVOP
Senior Developer
May 25, 2026
7 min read
You Don't Have a Logging Problem. You Have an Observability Problem.
#metrics Express 2026#Sentry Express#Node.js observability#Pino structured logging#Prometheus Node.js#equest ID correlation#Node.js logging production#Grafana VPS

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.

Comments (0)

Login to post a comment.

ZyVOP
ZyVOP

Founder of Zyvop 🚀 | Building AI-driven tools & premium insights for software engineers, CTOs, and tech leaders. Obsessed with automating workflows and exploring the frontier of AI.

Subscribe to ZyVOP's Newsletter

More from ZyVOP

View profile

Debian Adopts "Responsible Use of Generative AI" After Nine-Way Condorcet Vote

Debian's General Resolution 2026-002 closed on August 28 with "Responsible Use of Generative AI" beating eight rival proposals, including a Social Contract ban, by a clear Condorcet margin, per the project secretary's published beat matrix.

3 minAug 30

How I Built a Real-Time Developer Trend Radar Into My SEO Growth Engine

An AI-powered content intelligence system that streams live developer conversations from Hacker News, Dev.to, Google Search, and GitHub — and turns them into ready-to-write blog opportunities with one click.

12 minAug 29

Qwen3.8-Flash-Next Cost Efficiency, OpenExecutive Satire, and Multi-Vector Retrieval Advances

This week's digest covers Qwen3.8-Flash-Next's push for ultimate cost-efficiency, the viral OpenExecutive project, and the technical release of MultiVectorEncoder in Sentence-Transformers v6.0.

4 minAug 28

Anthropic’s Pricing Shock, Granite 4.2 Open‑Source Leap, and AI‑Powered Security & Policy Shifts

From Anthropic’s flagship model losing steam to IBM’s 512 K‑token Granite 4.2, plus a new wave of AI‑driven security exploits and policy alarms, this week’s digest maps the technical and market forces you need to act on now.

3 minAug 26

Introducing Questions and Discussions: A New Way to Connect!

We are thrilled to announce a major update to how you can interact and share content on our platform! Up until now, sharing your thoughts meant writing a standa...

2 minAug 1