{"schemaVersion":"1.0","type":"Article","types":["Article"],"slug":"you-don-t-have-a-logging-problem-you-have-an-observability-problem-caicd","url":"https://zyvop.com/you-don-t-have-a-logging-problem-you-have-an-observability-problem-caicd","title":"You Don't Have a Logging Problem. You Have an Observability Problem.","subtitle":"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","tldr":"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 s...","keywords":["metrics Express 2026","Sentry Express","Node.js observability","Pino structured logging","Prometheus Node.js","equest ID correlation","Node.js logging production","Grafana VPS"],"entities":["ZyVOP","metrics Express 2026","Sentry Express","Node.js observability","Pino structured logging","Prometheus Node.js","equest ID correlation","Node.js logging production","Grafana VPS"],"keyTakeaways":["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."],"headings":["Pillar 1 — Structured Logging with Pino","Pillar 2 — Request Logging and Correlation IDs","Pillar 3 — Metrics with Prometheus","The Prometheus + Grafana Stack on a VPS","Pillar 4 — Error Tracking","What Good Observability Looks Like in Practice","The Quick Start Checklist"],"outboundLinks":[],"contentText":"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', () =&gt; { const duration = Date.now() - startTime; const level = res.statusCode &gt;= 500 ? 'error' : res.statusCode &gt;= 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) =&gt; { 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', () =&gt; { 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) =&gt; { 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) =&gt; { 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) =&gt; { 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: Alert fires — Prometheus detects error rate above 1% for 5 minutes and fires a webhook to your Slack Check the dashboard — Grafana shows the spike started at 14:32, affects the /orders endpoint, latency jumped from 50ms to 3s Check Sentry — A new error appeared at 14:32: Connection pool exhausted in order.service.js:47 Pull the logs — Filter by url: /orders AND level: error since 14:30, find 10 request IDs Trace one request — Pull logs for one requestId, see the full timeline of what happened during that request 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.","contentHash":"sha256:506cbc7180bb39eabfd9f31c28ed1d1571ececfe7781ab15ccab77091f0b51f5","authorName":"ZyVOP","authorUrl":"https://zyvop.com/author/zyvop","authorSameAs":["https://zyvop.com","https://github.com/zyvop","https://x.com/zyvop1"],"category":null,"tags":["metrics Express 2026","Sentry Express","Node.js observability","Pino structured logging","Prometheus Node.js","equest ID correlation","Node.js logging production","Grafana VPS"],"audience":"Software engineers and developers building applications with metrics Express 2026","tone":"Practical and evidence-based engineering guidance","readingTimeMinutes":7,"wordCount":1460,"faqs":null,"primaryTopic":"metrics Express 2026","publishedAt":"2026-05-25T09:48:24.056Z","updatedAt":"2026-08-18T16:50:00.221Z","canonicalUrl":"https://zyvop.com/you-don-t-have-a-logging-problem-you-have-an-observability-problem-caicd"}