DEV Community

JULIO SAMUEL CORTEZ MAMANI
JULIO SAMUEL CORTEZ MAMANI

Posted on

Mastering Production Reliability: Practical Observability with OpenTelemetry, Prometheus, and GitHub Actions

In modern software engineering, traditional monitoring — simply knowing if a system is up or down — is no longer enough. High-velocity engineering teams require Observability: the ability to infer the internal states of a system based solely on its external outputs.

When a critical microservice misbehaves under high traffic, engineering teams cannot afford to guess. We need contextualized telemetry data that points directly to the root cause. This article provides a comprehensive guide to implementing production-grade observability practices using OpenTelemetry (the vendor-neutral industry standard) alongside Prometheus and Grafana, backed by an automated CI/CD validation workflow.

The Practical Implementation: Multi-Dimensional Metrics

Below is a complete, production-ready Node.js microservice. It showcases how to instrument a checkout endpoint to track both throughput (volume/status) and latency distribution using high-cardinality attributes.

const express = require('express');
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const opentelemetry = require('@opentelemetry/api');

// 1. Initialize the Prometheus Exporter (Exposes metrics on port 9464)
const exporter = new PrometheusExporter({}, () => {
  console.log('OpenTelemetry metrics exporter running at http://localhost:9464/metrics');
});

// 2. Configure and bootstrap the OpenTelemetry SDK
const sdk = new NodeSDK({
  metricReader: new PeriodicExportingMetricReader({ exporter: exporter }),
});
sdk.start();

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

// 3. Acquire a Meter from the Global Meter Provider
const meter = opentelemetry.metrics.getMeter('payment-gateway', '1.0.0');

// 4. Define Strategic Metrics: Counters and Histograms
const totalPaymentsCounter = meter.createCounter('payment_requests_total', {
  description: 'Total number of execution checkout requests',
});

const paymentLatencyHistogram = meter.createHistogram('payment_processing_duration_ms', {
  description: 'Latencies associated with external banking handshakes',
  unit: 'ms',
});

// API Route Core Logic
app.post('/api/v1/checkout', async (req, res) => {
  const startTime = Date.now();
  const { paymentMethod, amount } = req.body; // Expected values: 'credit_card', 'crypto', 'paypal'

  try {
    // Simulating external network latency and random payment failure (15% rate)
    const isSuccess = Math.random() > 0.15;
    await new Promise((resolve) => setTimeout(resolve, Math.random() * 350 + 50));

    if (!isSuccess) {
      throw new Error('External banking gateway rejected the transaction.');
    }

    // --- BEST PRACTICE: Increment counter injecting success dimension ---
    totalPaymentsCounter.add(1, {
      method: paymentMethod || 'fallback',
      status: 'success'
    });

    res.status(200).json({ status: 'approved', transactionId: 'tx_live_abc123' });

  } catch (error) {
    // --- BEST PRACTICE: Increment same counter injecting failure dimension ---
    totalPaymentsCounter.add(1, {
      method: paymentMethod || 'fallback',
      status: 'failed'
    });

    // Structured logging supporting telemetry aggregation
    console.error(JSON.stringify({
      timestamp: new Date().toISOString(),
      level: 'ERROR',
      message: error.message,
      metadata: { transactionAmount: amount }
    }));

    res.status(500).json({ error: 'Transaction declined' });
  } finally {
    // --- BEST PRACTICE: Record exact execution boundaries into a Histogram ---
    const totalDuration = Date.now() - startTime;
    paymentLatencyHistogram.record(totalDuration, { method: paymentMethod || 'fallback' });
  }
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Production payment microservice listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Core Observability Architecture Breakdown

1. Eliminating Vendor Lock-in via OpenTelemetry

By avoiding proprietary agents (e.g., native Datadog or New Relic hardcoded libraries) and writing directly against the OpenTelemetry API, the source code remains completely agnostic. If infrastructure changes require switching backend platforms, only the instantiation layer (PrometheusExporter) is swapped out, saving months of engineering refactoring.

2. Multi-Dimensional Metrics vs. Flat Strings

Instead of spawning decoupled metrics like payments_paypal_failed_total, we deploy a single structured metric (payment_requests_total) attached to structured attributes (method, status). This allows complex PromQL querying structures inside Grafana dashboards, such as:

sum(rate(payment_requests_total{status="failed"}[5m])) by (method)
Enter fullscreen mode Exit fullscreen mode

3. Histograms Over Averages

Averages hide outliers. This implementation tracks duration inside an OpenTelemetry Histogram. When visualized via Grafana, this allows the calculation of percentiles (p95, p99), ensuring engineers observe exactly how the slowest 5% or 1% of real-world clients are experiencing the application performance.

Repository Source Code & CI/CD Automation

The entire code layout, infrastructure manifests (Prometheus/Grafana docker-compose configurations), and local test suites are hosted publicly.

To satisfy automated delivery tracking, continuous quality checks, and deployment guarantees, the repository includes a strict automation workflow driven by GitHub Actions.

Code Repository: https://github.com/open-telemetry/opentelemetry-js

Ejemplo:
https://github.com/open-telemetry/opentelemetry-demo

Automation Blueprint (.github/workflows/ci.yml):

name: Observability Service CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  validate-and-test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Source Code
      uses: actions/checkout@v4

    - name: Setup Node.js Environment
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'

    - name: Install Production Dependencies
      run: npm ci

    - name: Execute Linter & Code Style Checks
      run: npm run lint

    - name: Run Automated Telemetry Unit Tests
      run: npm test
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing robust observability is not a luxury for modern engineering teams; it is a fundamental requirement to guarantee system reliability and operational excellence under scale. By shifting from a reactive "monitoring" mindset to a proactive "observability" framework, organizations can drastically reduce Mean Time to Resolution (MTTR) and uncover hidden system bottlenecks.

Top comments (2)

Collapse
 
eduardo_ginofloresnavar profile image
EDUARDO GINO FLORES NAVARRO

Excellent article addressing a critical topic for modern software engineering.

What I value most about this publication is how it translates theoretical observability concepts into practical, production-ready code. It doesn't stay in abstract definitions but provides a concrete implementation that teams can adopt immediately.

Collapse
 
leandro_diegohurtadoort profile image
Leandro Diego HURTADO ORTIZ

Nice article, I liked how it explains the difference between monitoring and observability without making it overly complicated. The practical Node.js example and the use of OpenTelemetry with Prometheus make it easier to understand how these tools work together in a real project.