DevOps

Monitoring & Observability: Prometheus, Grafana, OpenTelemetry

Master monitoring and observability with Prometheus, Grafana, and OpenTelemetry. Learn how to collect, visualize, and analyze metrics, logs, and traces for robust system health.

Khader Vali September 14, 2026 17 min read

Monitoring & Observability: The Unholy Trinity of Prometheus, Grafana, and OpenTelemetry

In the fast-paced world of modern software development, where microservices, containers, and serverless architectures reign supreme, understanding the health and performance of your systems is no longer a luxury but a fundamental necessity. Systems are becoming increasingly distributed, complex, and dynamic, making traditional monitoring approaches insufficient. This is where the powerful trio of Prometheus, Grafana, and OpenTelemetry steps in, forming a robust foundation for both monitoring and comprehensive observability.

As a senior software engineer who has navigated the murky waters of production incidents, I can tell you that a well-implemented observability strategy is your most potent weapon. It transforms reactive firefighting into proactive problem-solving, illuminates the unknown unknowns, and ultimately contributes to more reliable and resilient applications. In this extensive guide, we’ll dive deep into each of these technologies, understand their individual strengths, and explore how they integrate to provide an unparalleled view into your system’s inner workings. We’ll cover architecture, practical examples, and real-world scenarios, ensuring you’re equipped to build a state-of-the-art observability stack.

Monitoring vs. Observability: Demystifying the Terms

Before we delve into the tools, let’s clarify two often-interchanged but distinct concepts: monitoring and observability. Understanding the difference is crucial for designing an effective strategy.

What is Monitoring?

Monitoring is about knowing the known unknowns. It’s the practice of collecting, aggregating, and displaying metrics and logs from your systems to understand their current state. You typically define what to monitor based on anticipated problems or critical performance indicators. Think of it as checking the vitals of a patient – heart rate, blood pressure, temperature. You know what measurements are important, and you set alerts when they go outside a healthy range.

Key characteristics of monitoring:

  • Focus: Known metrics, predefined dashboards, and alerts.
  • Questions: Is the CPU utilization high? Is the error rate increasing? Is the disk full?
  • Tools: Often involves dashboards (like Grafana), time-series databases (like Prometheus), and basic log aggregators.
  • Outcome: Alerts you when something you expect to go wrong, does go wrong.

What is Observability?

Observability, on the other hand, is the ability to understand the internal state of a system by examining its external outputs. It’s about being able to ask arbitrary questions about your system’s behavior *without* having to ship new code. While monitoring tells you *if* something is wrong, observability helps you understand *why* it’s wrong, even for problems you hadn’t anticipated.

Going back to the patient analogy, observability is like having access to all medical records, lab results, and diagnostic imaging, allowing doctors to diagnose a complex, previously unknown condition. It’s about having enough rich data (metrics, logs, and traces) that you can explore and correlate to debug novel problems.

Key characteristics of observability:

  • Focus: Unknown unknowns, deep exploration, correlation of different data types.
  • Questions: Why is this specific user request slow? Which service introduced this latency? What was the exact sequence of events leading to this error?
  • Pillars: Metrics, Logs, and Traces.
  • Tools: Sophisticated data correlation and visualization platforms (like Grafana with multiple data sources), distributed tracing systems (like Jaeger/Tempo enabled by OpenTelemetry), and advanced log analysis tools.
  • Outcome: Enables rapid debugging and root cause analysis for any issue, known or unknown.

In essence, monitoring is a subset of observability. You monitor *for* problems, while you use observability *to solve* them. Both are indispensable, and a truly resilient system leverages both effectively.

<

Monitoring & Observability: Prometheus, Grafana, OpenTelemetry
Generated Image

>

The Three Pillars of Observability

Observability stands firm on three foundational data types: Metrics, Logs, and Traces. Each provides a unique lens through which to view your system’s behavior, and their combined power is what truly unlocks deep understanding.

Metrics: The “What”

Metrics are numerical measurements collected over time, often aggregated, that represent a specific aspect of your system’s health or performance. They are ideal for monitoring system-wide trends, detecting anomalies, and setting alerts. Metrics are lightweight, efficient to store, and excellent for long-term trending.

Common types of metrics:

  • Counters: Monotonically increasing values, like the number of requests served, errors encountered, or bytes sent.
  • Gauges: A single numerical value that can go up or down, like current CPU utilization, memory usage, or queue size.
  • Histograms: Samples observations (e.g., request durations, response sizes) and groups them into configurable buckets, allowing for calculation of quantiles (e.g., p95 latency).
  • Summaries: Similar to histograms but calculate configurable quantiles on the client side over a sliding time window.

Metrics are invaluable for answering questions like: “What is the average latency of my API?” or “How many errors per second is my service experiencing?”

Logs: The “Where and When”

Logs are discrete, timestamped records of events that occur within an application or system. They provide granular context about specific operations, state changes, or errors. While metrics tell you *that* something is happening, logs tell you *what* happened, *when*, and with *what context*.

Logs are crucial for debugging specific issues, especially when correlated with metrics and traces. However, logs can be voluminous, making storage and querying challenging at scale. Best practices involve structured logging (e.g., JSON) to make them machine-readable and easier to parse and filter.

Logs help answer questions like: “What was the specific error message when user X tried to perform action Y?” or “What sequence of events led to this transaction failing?”

Traces: The “How”

Traces, specifically distributed traces, illustrate the end-to-end journey of a single request or transaction as it propagates through a distributed system. A trace is composed of multiple “spans,” where each span represents a logical unit of work within that request, such as a function call, a database query, or an RPC call between services.

Traces are the cornerstone of understanding performance in microservice architectures. They help visualize dependencies, identify latency bottlenecks across services, and pinpoint where errors originate. Without traces, debugging a slow request that touches five different services becomes a Herculean task of piecing together disparate logs and metrics.

Traces answer questions like: “Which service in my request path added the most latency?” or “What was the full path a specific user request took through my system, and where did it fail?”

Prometheus: The Metrics Powerhouse

Prometheus has emerged as the de facto standard for open-source monitoring in cloud-native environments. It’s a powerful, self-contained monitoring system that collects and stores time-series data, offering a flexible query language and robust alerting capabilities.

Prometheus Architecture and Components

Prometheus operates on a pull-based model, meaning it actively scrapes metrics endpoints exposed by your applications and infrastructure. Here’s a breakdown of its core components:

  • Prometheus Server: The heart of the system. It handles scraping targets, storing time-series data in its local disk-based TSDB, and executing queries using PromQL.
  • Exporters: Applications or services don’t natively speak the Prometheus format. Exporters are small agents that translate existing metrics from third-party systems (e.g., node_exporter for OS metrics, cAdvisor for container metrics, various database exporters) into a Prometheus-readable format, exposing them via an HTTP endpoint.
  • Client Libraries: For custom applications, client libraries (available for Go, Java, Python, Ruby, etc.) allow developers to instrument their code directly, exposing custom application metrics at an HTTP endpoint (typically /metrics).
  • Service Discovery: Prometheus needs to know *what* to scrape. It integrates with various service discovery mechanisms (e.g., Kubernetes, Consul, DNS) to dynamically discover and monitor targets.
  • Pushgateway: For short-lived or batch jobs that cannot be reliably scraped, the Pushgateway acts as an intermediary. Jobs push their metrics to the Pushgateway, which then exposes them for Prometheus to scrape.
  • Alertmanager: A separate component that handles alerts sent by the Prometheus server. It de-duplicates, groups, and routes alerts to appropriate receivers (email, Slack, PagerDuty, etc.) and can suppress alerts during maintenance windows.

PromQL: Prometheus Query Language

PromQL is Prometheus’s functional query language, designed for flexible querying of its time-series data. It allows you to select and aggregate data, apply mathematical functions, and perform sophisticated analysis. Mastering PromQL is key to extracting meaningful insights from your metrics.

Here are some basic PromQL examples:

# Get the current value of CPU utilization for all nodes
node_cpu_seconds_total

# Get the rate of HTTP requests over the last 5 minutes
rate(http_requests_total[5m])

# Calculate the 99th percentile of request duration over the last 1 hour
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[1h])))

# Find instances with more than 10 errors per second over the last 30 minutes
sum by (instance) (rate(http_requests_total{status_code="5xx"}[30m])) > 10

Real-world Scenario: Monitoring a Microservice with Prometheus

Imagine you have a Python microservice using Flask, and you want to monitor its request count, latency, and error rates. You’d instrument it using the Prometheus Python client library.

from flask import Flask, request
from prometheus_client import Counter, Histogram, generate_latest, Gauge
import time

app = Flask(__name__)

# Prometheus metrics
REQUEST_COUNT = Counter(
    'http_requests_total', 'Total HTTP Requests',
    ['method', 'endpoint', 'status_code']
)
REQUEST_LATENCY = Histogram(
    'http_request_duration_seconds', 'HTTP Request Latency',
    ['method', 'endpoint']
)
IN_PROGRESS_REQUESTS = Gauge(
    'http_requests_in_progress', 'In progress HTTP requests',
    ['method', 'endpoint']
)

@app.before_request
def before_request():
    request.start_time = time.time()
    IN_PROGRESS_REQUESTS.labels(request.method, request.path).inc()

@app.after_request
def after_request(response):
    request_latency = time.time() - request.start_time
    REQUEST_LATENCY.labels(request.method, request.path).observe(request_latency)
    REQUEST_COUNT.labels(request.method, request.path, response.status_code).inc()
    IN_PROGRESS_REQUESTS.labels(request.method, request.path).dec()
    return response

@app.route('/')
def hello():
    return "Hello, World!"

@app.route('/slow')
def slow_endpoint():
    time.sleep(0.5) # Simulate a slow operation
    return "This was slow!"

@app.route('/error')
def error_endpoint():
    raise Exception("Simulated error!")

@app.route('/metrics')
def metrics():
    return generate_latest(), 200, {'Content-Type': 'text/plain; version=0.0.4; charset=utf-8'}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

You would then configure Prometheus to scrape your Flask app’s /metrics endpoint:

# prometheus.yml snippet
scrape_configs:
  - job_name: 'my-flask-app'
    static_configs:
      - targets: ['localhost:5000']

With this setup, Prometheus would regularly scrape your application, collecting vital metrics about its performance, which can then be visualized and alerted upon.

Grafana: The Visualization Maestro

While Prometheus is excellent at collecting and storing metrics, it’s not designed for rich visualization. That’s where Grafana comes in. Grafana is an open-source analytics and interactive visualization web application. It allows you to query, visualize, alert on, and explore your metrics, logs, and trace data from various sources.

Integrating Grafana with Prometheus

Grafana seamlessly integrates with Prometheus as a data source. Once configured, you can build dynamic, interactive dashboards using PromQL queries to display your metrics.

Steps to connect Grafana to Prometheus:

  1. Install Grafana (e.g., via Docker, apt, yum).
  2. Access the Grafana web UI (default: http://localhost:3000).
  3. Go to “Configuration” -> “Data Sources” -> “Add data source”.
  4. Select “Prometheus”.
  5. Enter the URL of your Prometheus server (e.g., http://localhost:9090).
  6. Click “Save & Test”.

Building Dashboards and Panels

A Grafana dashboard is a collection of panels, each displaying a specific visualization of your data. Panels can be graphs, single stats, tables, heatmaps, and more.

For our Flask app example, you might create panels to show:

  • Total Requests: A single stat panel showing sum(rate(http_requests_total[5m]))
  • Request Latency (P95): A graph panel showing histogram_quantile(0.95, sum by (le, method, endpoint) (rate(http_request_duration_seconds_bucket[5m])))
  • Error Rate: A graph panel showing sum(rate(http_requests_total{status_code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) * 100
  • In-Progress Requests: A single stat or graph showing sum(http_requests_in_progress)

Grafana’s templating features allow you to create dynamic dashboards where you can select specific instances, endpoints, or methods using dropdowns, making your dashboards reusable and powerful.

Alerting with Grafana and Alertmanager

Grafana can also act as an alerting engine. While Prometheus Alertmanager is typically preferred for its advanced grouping and routing capabilities, Grafana provides a convenient way to define alerts directly from your dashboard panels. These alerts can then notify various channels like Slack, email, PagerDuty, etc.

A common setup involves Prometheus generating alerts based on PromQL rules, which are then sent to Alertmanager for processing. Grafana can also consume Alertmanager’s status to display active alerts on dashboards.

Real-world Scenario: Service Health Dashboard

Consider a critical backend service. A Grafana dashboard for this service might include:

  • Overall Request Rate: To see the load on the service.
  • Error Rate (5xx/4xx): To quickly identify if the service is failing or experiencing client-side issues.
  • Latency (P99, P95, P50): To understand user experience and identify performance degradation.
  • Resource Utilization (CPU, Memory, Disk, Network I/O): From node_exporter or container metrics (cAdvisor) to check host health.
  • Database Connection Pool Usage: To detect resource exhaustion.
  • Queue Lengths: For asynchronous processing, to ensure messages are being processed timely.
  • Custom Business Metrics: E.g., “orders placed per minute,” “users logged in.”

Each panel would use specific PromQL queries targeting metrics exposed by the service or its underlying infrastructure, providing a holistic view of the service’s health at a glance.

<

Monitoring & Observability: Prometheus, Grafana, OpenTelemetry
Generated Image

>

OpenTelemetry: Standardizing Observability

OpenTelemetry (OTel) is a vendor-neutral, open-source set of APIs, SDKs, and tools designed to standardize the generation and collection of telemetry data (metrics, logs, and traces) from your applications. It addresses a critical challenge in observability: the proliferation of proprietary agents and libraries that lead to vendor lock-in and inconsistent data formats.

The Problem OTel Solves

Before OpenTelemetry, if you wanted to switch monitoring vendors, you’d often have to re-instrument your entire application with a new set of client libraries specific to that vendor. This was a costly and time-consuming process. OpenTelemetry provides a single, consistent way to instrument your code, allowing you to choose or switch your observability backend without changing your application’s instrumentation.

OpenTelemetry Components

  • API (Application Programming Interface): Defines the standard for instrumenting code, allowing developers to record telemetry data. It’s language-agnostic.
  • SDK (Software Development Kit): Implements the API for specific languages (e.g., Python, Java, Go). It processes the telemetry data (e.g., sampling traces, aggregating metrics) and exports it.
  • Collector: A vendor-agnostic proxy that receives, processes, and exports telemetry data. It can receive data in various formats (OTLP, Jaeger, Zipkin, Prometheus), apply processors (e.g., batching, filtering, adding attributes), and export to multiple backends simultaneously (e.g., Prometheus, Jaeger, Loki, commercial vendors). The Collector is a crucial component for managing telemetry data at scale and decoupling your application from specific backends.

Instrumenting Applications with OpenTelemetry

Instrumentation involves adding code to your application to generate telemetry data. OpenTelemetry offers two main approaches:

  1. Automatic Instrumentation (Auto-instrumentation): Uses agents or bytecode manipulation to automatically collect telemetry from popular frameworks and libraries (e.g., Flask, Django, Spring Boot, gRPC) without requiring code changes. This is great for getting started quickly.
  2. Manual Instrumentation: Involves explicitly adding OTel API calls to your code to capture custom metrics, specific spans in traces, or detailed log attributes. This provides fine-grained control and is essential for business-specific observability.

Traces in Depth with OpenTelemetry

OpenTelemetry shines particularly brightly in distributed tracing. A trace represents a single operation, like a user request, traversing through multiple services. It’s composed of “spans”:

  • Span: Represents a single logical unit of work within a trace (e.g., a function call, a database query, an RPC call). Each span has a name, start time, end time, attributes (key-value pairs describing the span), and events (timestamped annotations).
  • Trace Context: Critical for linking spans across service boundaries. It includes a Trace ID (unique to the entire request) and a Span ID (unique to the current span). When a service calls another, the trace context is propagated (e.g., via HTTP headers), allowing the receiving service to create a child span correctly linked to the parent.

Here’s a simplified Python example of manual tracing with OTel:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
from opentelemetry.sdk.resources import Resource

# Configure the tracer provider
resource = Resource.create({"service.name": "my-service"})
provider = TracerProvider(resource=resource)
span_processor = SimpleSpanProcessor(ConsoleSpanExporter()) # Export to console for demo
provider.add_span_processor(span_processor)
trace.set_tracer_provider(provider)

# Get a tracer for your application
tracer = trace.get_tracer(__name__)

def my_function_a():
    with tracer.start_as_current_span("function_a") as span:
        span.set_attribute("param_a", "value1")
        print("Inside function A")
        time.sleep(0.1)
        my_function_b()
        span.add_event("event_in_a", {"detail": "processed_something"})

def my_function_b():
    with tracer.start_as_current_span("function_b"):
        print("Inside function B")
        time.sleep(0.05)

if __name__ == "__main__":
    print("Starting trace...")
    with tracer.start_as_current_span("main_operation"):
        my_function_a()
    print("Trace finished.")

This code would generate trace data that the OTel SDK processes and, in this case, prints to the console. In a real application, you’d configure an exporter to send this data to an OTel Collector or directly to a tracing backend like Jaeger or Tempo.

Metrics with OpenTelemetry

OpenTelemetry also provides APIs and SDKs for metrics. While Prometheus has its own client libraries, OTel metrics can be collected by the OTel Collector and then exported in a Prometheus-compatible format, allowing Prometheus to scrape them. This means you can instrument your application once with OTel for all telemetry types and still leverage Prometheus for metric storage and Grafana for visualization.

Logs with OpenTelemetry

The OpenTelemetry log API and SDK are still evolving but aim to standardize the emission of logs, allowing them to be correlated with traces and metrics. This means logs wouldn’t just be isolated events but could carry trace IDs and span IDs, providing incredibly rich context for debugging.

Integrating the Stack: Prometheus, Grafana, OpenTelemetry

The true power of these tools comes from their synergy. Let’s outline a common architectural pattern for a modern observability stack using Prometheus, Grafana, and OpenTelemetry.

Conceptual Architecture Diagram (in Words)

Imagine your distributed application consists of several microservices, a database, and other infrastructure components.

  1. Application Instrumentation:

    • Your microservices are instrumented using OpenTelemetry SDKs (e.g., Python, Java, Go).
    • They generate:
      • Metrics: Custom application metrics (e.g., business metrics, internal queue sizes).
      • Traces: Spans representing operations within the service and propagating trace context to downstream services.
      • Logs: Structured logs, potentially with trace and span IDs embedded.
    • Infrastructure components (Kubernetes, hosts, databases) expose metrics via dedicated Prometheus Exporters (e.g., kube-state-metrics, node_exporter, pg_exporter).
  2. OpenTelemetry Collector:

    • A central component (or sidecar/agent per host) receives telemetry data from all instrumented applications.
    • It can receive data in OTLP (OpenTelemetry Protocol) format from application SDKs.
    • It can also scrape Prometheus-formatted metrics from services or exporters.
    • The Collector processes this data:
      • Metrics: It aggregates, filters, and can transform OTel metrics into a Prometheus-scrapeable format.
      • Traces: It batches, samples, and enriches trace data.
      • Logs: It processes logs (e.g., adds resource attributes).
    • The Collector then exports the processed data to various backends.
  3. Prometheus Server:

    • Scrapes metrics endpoints:
      • Directly from Prometheus Exporters (e.g., node_exporter).
      • From the OpenTelemetry Collector (which exposes processed metrics in Prometheus format).
    • Stores all collected time-series metrics in its TSDB.
    • Evaluates alerting rules and sends firing alerts to Alertmanager.
  4. Alertmanager:

    • Receives alerts from Prometheus.
    • Groups, de-duplicates, and routes alerts to appropriate notification channels (Slack, email, PagerDuty, VictorOps).
  5. Tracing Backend (e.g., Jaeger or Tempo):

    • The OpenTelemetry Collector exports trace data (via OTLP) to a dedicated tracing backend.
    • This backend stores and allows querying of distributed traces.
  6. Logging Backend (e.g., Loki or ELK Stack):

    • The OpenTelemetry Collector (or a dedicated log agent like Promtail/Fluentd) forwards logs to a logging backend.
    • This backend stores and allows querying of structured logs.
  7. Grafana:

    • Acts as the single pane of glass for all observability data.
    • Configured with multiple data sources:
      • Prometheus: For querying and visualizing metrics.
      • Jaeger/Tempo: For querying and displaying traces (often linked from metrics dashboards).
      • Loki/Elasticsearch: For querying and displaying logs (often linked from metrics or trace dashboards).
    • Provides rich, interactive dashboards for monitoring system health and exploring data.
    • Can also display Alertmanager status and trigger alerts itself.

A Simplified Step-by-Step Integration Example

Let’s illustrate with a simple Python Flask app generating metrics and traces, collected by an OTel Collector, stored in Prometheus/Jaeger, and visualized in Grafana.

1. Instrument the Python Flask App (app.py):

from flask import Flask, request
from prometheus_client import generate_latest # Still use for direct Prometheus metrics if desired
import time

# OpenTelemetry specific

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

Securing Your Software Supply Chain: SBOMs & Sigstore

Aug 26, 2026 · 16 min read

GitHub Actions CI/CD: From Basics to Matrix Builds

Aug 23, 2026 · 17 min read

GitHub Actions CI/CD: Basics to Matrix Builds

Sep 23, 2026 · 16 min read