DevOps

Prometheus, Grafana, OpenTelemetry: The Observability Stack

Master monitoring and observability with Prometheus, Grafana, and OpenTelemetry. Learn to collect metrics, traces, and logs for robust, scalable systems.

Khader Vali July 13, 2026 16 min read

In the intricate world of modern software, where monolithic applications have given way to dynamic, distributed microservices, simply knowing if your application is “up” is no longer enough. We’ve moved beyond basic monitoring into the realm of observability – the ability to understand *why* a system is behaving a certain way, even for conditions you haven’t explicitly anticipated. As a senior engineer, I’ve seen firsthand how crucial a robust observability strategy is for maintaining system health, ensuring customer satisfaction, and accelerating development cycles.

This article dives deep into building a powerful, vendor-neutral observability stack using three industry titans: Prometheus for collecting and storing metrics, Grafana for stunning visualizations and alerting, and OpenTelemetry for standardized instrumentation across metrics, traces, and logs. We’ll explore their individual strengths, how they integrate seamlessly, and how you can leverage them to gain unparalleled insights into your applications and infrastructure.

Monitoring vs. Observability: A Crucial Distinction

Before we delve into the tools, let’s clarify two terms often used interchangeably but with distinct meanings:

Monitoring: Knowing the Known Unknowns

Monitoring is about collecting predefined sets of metrics and logs to track the health and performance of your systems. You set up dashboards and alerts for specific conditions you *expect* to encounter. For example, CPU utilization above 80%, disk space critically low, or HTTP 500 errors exceeding a threshold. Monitoring answers questions like: “Is the system working as expected?” or “Is the CPU utilization high?”.

  • Focus: Known failure modes, specific metrics (e.g., CPU, memory, request rates).
  • Approach: Predefined dashboards, alerts on thresholds.
  • Best for: Detecting known issues, verifying service health.

Observability: Uncovering the Unknown Unknowns

Observability, on the other hand, is the ability to infer the internal state of a system merely by examining its external outputs (metrics, traces, logs). It provides the tools and data to ask arbitrary questions about your system’s behavior, even questions you didn’t think to ask when you designed it. When an unexpected issue arises, observability allows you to drill down, correlate events, and pinpoint the root cause without deploying new code or adding custom logging.

  • Focus: Understanding complex system behavior, debugging unexpected issues.
  • Approach: Correlating metrics, traces, and logs; dynamic querying.
  • Best for: Root cause analysis, understanding distributed system interactions, innovation.

In essence, monitoring tells you *if* something is wrong, while observability helps you understand *why* it’s wrong and *what’s happening*. For today’s complex, distributed architectures, observability is indispensable.

Prometheus: The Metrics Powerhouse

Prometheus is an open-source systems monitoring and alerting toolkit originally built at SoundCloud. It has since become a cornerstone of the cloud-native ecosystem, offering a robust and flexible solution for collecting and storing time-series data.

What is Prometheus?

At its core, Prometheus is a pull-based metrics system. This means it actively scrapes (pulls) metrics from configured targets (your applications, servers, databases) at regular intervals. These metrics are stored in a highly efficient time-series database (TSDB) and can be queried using its powerful query language, PromQL.

Key Concepts in Prometheus

  • Metrics Types: Prometheus defines four core metric types:
    • Counter: A single monotonically increasing counter whose value can only be incremented or reset to zero on restart. Ideal for counting requests, errors, or completed tasks.
    • Gauge: A metric that can go up and down arbitrarily. Suitable for current memory usage, CPU load, or active connections.
    • Histogram: Samples observations (e.g., request durations or response sizes) and counts them in configurable buckets. Provides sum and count of all observed values. Useful for calculating percentiles.
    • Summary: Similar to a histogram but calculates configurable quantiles over a sliding time window on the client side. More resource-intensive on the client, less flexible for dynamic aggregation.
  • Exporters: Applications often don’t expose Prometheus metrics natively. Exporters are standalone programs that scrape metrics from other systems (e.g., Node Exporter for host metrics, Blackbox Exporter for endpoint probing, database exporters) and expose them in a Prometheus-compatible format.
  • Service Discovery: In dynamic environments like Kubernetes, targets come and go. Prometheus integrates with various service discovery mechanisms (e.g., Kubernetes, Consul, EC2) to automatically find and scrape new targets.
  • Alertmanager: A separate component that handles alerts sent by Prometheus. It deduplicates, groups, and routes alerts to appropriate receivers (email, PagerDuty, Slack, etc.).
  • PromQL: Prometheus Query Language. A flexible and powerful language for querying and aggregating time-series data.

Prometheus Architecture (in words)

Imagine a central brain (the Prometheus Server) constantly reaching out to gather information. This brain has a database (the Time-Series Database – TSDB) where it stores everything it learns. Its primary task is to ‘scrape’ data.

Around this brain are various sources of information, called Targets. These can be your application instances, database servers, or even specialized Exporters (like the Node Exporter for OS metrics). Each target exposes an HTTP endpoint, typically /metrics, that outputs metrics in a simple, human-readable text format.

The Prometheus Server uses Service Discovery (e.g., connecting to a Kubernetes API) to automatically find these targets. It then periodically sends HTTP requests to their /metrics endpoints, pulls the data, and stores it in its TSDB. If a target is ephemeral or behind a firewall, a Pushgateway can be used as an intermediary; ephemeral jobs push their metrics to the Pushgateway, and Prometheus scrapes the Pushgateway.

When certain metric conditions are met (defined by Recording Rules or Alerting Rules in the Prometheus server), Prometheus generates alerts. These alerts aren’t sent directly to you; instead, they are forwarded to the Alertmanager. The Alertmanager then takes these raw alerts, groups similar ones to prevent alert storms, deduplicates them, and routes them to the correct notification channels (email, Slack, PagerDuty, etc.) based on its configuration.

Finally, a visualization tool like Grafana connects to the Prometheus Server as a data source, querying the TSDB using PromQL to build dashboards and visualize the collected metrics.

Prometheus, Grafana, OpenTelemetry: The Observability Stack
Generated Image

Real-world Scenario: Monitoring a Python Flask Microservice

Let’s say you have a simple Flask application. You want to monitor its request count, latency, and active users.

Python Flask App with Prometheus Client

First, install the Prometheus client library:

pip install prometheus_client Flask

Then, modify your Flask app:

from flask import Flask, request
from prometheus_client import Counter, Histogram, generate_latest, Gauge
from prometheus_client import make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import time

app = Flask(__name__)

# Define 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']
)
ACTIVE_USERS = Gauge(
    'active_users_total', 'Number of active users'
)

@app.route('/')
def hello_world():
    start_time = time.time()
    try:
        # Simulate some work
        time.sleep(0.05)
        status_code = 200
        return 'Hello, World!'
    finally:
        REQUEST_LATENCY.labels(request.method, request.path).observe(time.time() - start_time)
        REQUEST_COUNT.labels(request.method, request.path, status_code).inc()

@app.route('/login')
def login():
    start_time = time.time()
    try:
        # Simulate login logic
        time.sleep(0.1)
        ACTIVE_USERS.inc() # Increment active users on login
        status_code = 200
        return 'Login successful!'
    finally:
        REQUEST_LATENCY.labels(request.method, request.path).observe(time.time() - start_time)
        REQUEST_COUNT.labels(request.method, request.path, status_code).inc()

@app.route('/logout')
def logout():
    start_time = time.time()
    try:
        # Simulate logout logic
        time.sleep(0.03)
        ACTIVE_USERS.dec() # Decrement active users on logout
        status_code = 200
        return 'Logout successful!'
    finally:
        REQUEST_LATENCY.labels(request.method, request.path).observe(time.time() - start_time)
        REQUEST_COUNT.labels(request.method, request.path, status_code).inc()

# Add Prometheus metrics endpoint to the Flask app
# This creates a separate /metrics endpoint served by the prometheus_client
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {
    '/metrics': make_wsgi_app()
})

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

This code instruments your Flask app to expose metrics at /metrics. Now, Prometheus can scrape this endpoint.

Prometheus Configuration (`prometheus.yml`)

Here’s a basic Prometheus configuration to scrape your Flask app:

global:
  scrape_interval: 15s # How frequently to scrape targets
  evaluation_interval: 15s # How frequently to evaluate rules

scrape_configs:
  - job_name: 'flask-app'
    # metrics_path defaults to /metrics
    static_configs:
      - targets: ['localhost:5000'] # Replace with your app's actual host:port

With this setup, Prometheus will periodically scrape your Flask app’s /metrics endpoint, collecting data on request counts, latency, and active users. You can then query this data using PromQL, e.g., sum(rate(http_requests_total{job="flask-app"}[5m])) by (endpoint) to see requests per second per endpoint.

Advantages of Prometheus

  • Powerful Query Language (PromQL): Unmatched flexibility for data aggregation and analysis.
  • Efficiency: Designed for high-volume time-series data.
  • Cloud-Native Integration: Excellent service discovery for dynamic environments like Kubernetes.
  • Large Ecosystem: Many exporters available for various technologies.

Disadvantages of Prometheus

  • Limited Long-Term Storage: The local TSDB is not designed for petabytes of historical data. Typically integrated with long-term storage solutions (e.g., Thanos, Mimir).
  • Pull Model Limitations: Can be challenging for ephemeral jobs or systems behind firewalls (though Pushgateway helps).
  • No Built-in Tracing or Logging: Primarily focused on metrics. This is where OpenTelemetry comes in.

Grafana: The Visualization Hub

Grafana is an open-source platform for monitoring and observability that allows you to query, visualize, alert on, and understand your metrics, logs, and traces no matter where they are stored. It’s the “single pane of glass” that brings all your observability data together.

What is Grafana?

Grafana connects to a multitude of data sources (Prometheus, Loki, Tempo, Elasticsearch, SQL databases, etc.) and provides a rich set of visualization options, from time-series graphs and heatmaps to tables and stat panels. It’s highly customizable, allowing you to create interactive dashboards tailored to your specific needs.

Key Features of Grafana

  • Data Source Agnostic: Connects to virtually any data source imaginable.
  • Rich Dashboards: Create beautiful, interactive, and highly customizable dashboards with a drag-and-drop interface.
  • Powerful Query Editor: Seamlessly switch between different data sources and their native query languages (e.g., PromQL for Prometheus, LogQL for Loki).
  • Alerting: Define alert rules directly within Grafana based on your dashboard queries, with notifications sent to various channels.
  • Templating and Variables: Create dynamic dashboards that allow users to filter data based on services, environments, or other labels, reducing dashboard sprawl.
  • Explore View: A dedicated view for ad-hoc querying and debugging, allowing you to quickly inspect raw data.

Grafana Architecture (in words)

The Grafana Server acts as a central application that renders dashboards and handles user interactions. It doesn’t store the actual monitoring data itself. Instead, it’s a visualization and query engine.

When a user opens a dashboard in their web browser (the Browser Client), the client sends requests to the Grafana Server. The Grafana Server then acts as a proxy, forwarding these requests to various configured Data Sources. For example, if a panel on a dashboard displays Prometheus metrics, Grafana will send a PromQL query to the Prometheus Server.

The Prometheus Server (or any other data source like Loki for logs, Tempo for traces) processes the query and returns the requested data to Grafana. Grafana then takes this raw data and renders it into the beautiful visualizations you see on your dashboard. This means Grafana is stateless concerning the actual metric, log, or trace data, relying entirely on its connected data sources.

Grafana also has its own Alerting Engine. You can define alert rules within Grafana based on queries from any connected data source. When an alert condition is met, Grafana can send notifications directly to various endpoints like Slack, PagerDuty, email, etc. (though often, for Prometheus, Alertmanager is preferred for its advanced grouping/deduplication features).

Real-world Scenario: Building a Dashboard for Your Flask App

Once Prometheus is scraping your Flask app, you can visualize the data in Grafana. Here’s how you’d typically set it up:

  1. Install Grafana: Follow the official Grafana documentation for your operating system or use Docker.
  2. Add Prometheus as a Data Source:
    • Log in to Grafana.
    • Go to “Connections” -> “Data sources” -> “Add data source”.
    • Select “Prometheus”.
    • Set the URL to your Prometheus server (e.g., http://localhost:9090).
    • Save and Test.
  3. Create a New Dashboard:
    • Click the “+” icon on the left navigation bar and select “New Dashboard”.
    • Click “Add new panel”.
    • Select your Prometheus data source.
    • In the “Query” editor, use PromQL.

Example Panels:

  • Total HTTP Requests (Gauge/Graph):
    sum(rate(http_requests_total{job="flask-app"}[5m])) by (endpoint)
    

    This query shows the average requests per second over the last 5 minutes, broken down by endpoint. Visualize this as a time-series graph.

  • Request Latency (Histogram):
    histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="flask-app"}[5m])) by (le, endpoint))
    

    This calculates the 95th percentile (p95) of request latency, aggregated by endpoint. A crucial metric for user experience.

  • Active Users (Stat Panel):
    active_users_total{job="flask-app"}
    

    A simple query to display the current number of active users as a single stat.

Grafana’s intuitive interface allows you to select visualization types, customize colors, set up thresholds, and organize panels into rows. You can create variables for dynamic filtering, for instance, to select a specific endpoint or instance from a dropdown, making your dashboards highly interactive and reusable.

Advantages of Grafana

  • Unified View: Connects to virtually all data sources for a single pane of glass.
  • Powerful Visualizations: Extensive options for presenting data clearly and effectively.
  • Flexible Alerting: Create alerts based on any query.
  • Community & Ecosystem: Large library of pre-built dashboards and plugins.
  • Ease of Use: User-friendly interface for building and exploring dashboards.

OpenTelemetry: The Observability Standard

While Prometheus and Grafana are excellent for metrics, modern distributed systems demand more. This is where OpenTelemetry (OTel) comes in. OpenTelemetry is a vendor-neutral set of APIs, SDKs, and tools designed to standardize the generation and collection of telemetry data (metrics, logs, and traces).

Why OpenTelemetry?

Before OpenTelemetry, instrumenting applications for observability often meant choosing a specific vendor’s SDK (e.g., Jaeger client for tracing, a specific APM agent). This led to vendor lock-in, inconsistent data formats, and difficulty in switching providers. OpenTelemetry solves this by providing:

  • Standardized Instrumentation: A consistent way to instrument applications regardless of the backend.
  • Vendor Neutrality: Your application code remains independent of the observability backend. You can switch from Jaeger to Tempo, or from a proprietary APM to an open-source solution, without re-instrumenting your code.
  • Unified Data Types: Handles metrics, traces, and logs under a single specification.
  • Collector: A powerful agent/gateway that can receive, process, and export telemetry data to various backends.

Key Concepts in OpenTelemetry

  • Traces: Represent the end-to-end journey of a request or transaction through a distributed system.
    • Span: A single operation within a trace (e.g., an HTTP request, a database query). Spans have a name, start/end times, attributes (key-value pairs), and can have parent-child relationships.
    • Context Propagation: The mechanism by which trace and span IDs are passed across service boundaries (e.g., via HTTP headers) to link operations into a single trace.
  • Metrics: Provides a consistent API for application-level metrics, similar to Prometheus but with a unified API across languages. OTel metrics can be exported to Prometheus, ensuring compatibility.
  • Logs: While still evolving, OpenTelemetry aims to provide a standardized way to emit structured logs that can be correlated with traces and metrics.
  • Resource: Represents the entity producing telemetry (e.g., a service, an application instance). Contains attributes that describe the entity (service name, host ID, version).
  • OpenTelemetry Collector: A powerful proxy that can receive telemetry data in various formats (OTLP, Jaeger, Zipkin, Prometheus remote write), process it (filter, batch, enrich), and export it to multiple backends.

OpenTelemetry Architecture (in words)

At the heart of OpenTelemetry is the idea of **instrumentation**. Your application code, instead of talking directly to an observability backend, interacts with the **OpenTelemetry SDKs** (available for many languages like Python, Java, Go, Node.js). These SDKs provide APIs for generating metrics, traces (spans), and logs.

When your application generates telemetry, it sends this data, typically using the **OpenTelemetry Protocol (OTLP)**, to an **OpenTelemetry Collector**. The Collector is a standalone agent or service that acts as a universal receiver, processor, and exporter of telemetry data. It can run as an agent sidecar to your application or as a dedicated service.

The Collector has three main stages:

  1. Receivers: Listen for incoming telemetry data from applications or other collectors. They can receive data in various formats (e.g., OTLP, Jaeger, Zipkin, Prometheus).
  2. Processors: Modify or enrich the telemetry data. This can include batching data, adding resource attributes, filtering sensitive information, or sampling traces to manage volume.
  3. Exporters: Send the processed data to one or more observability backends. This is where the vendor neutrality shines. You can export metrics to Prometheus, traces to Jaeger or Tempo, and logs to Loki or Elasticsearch, all from the same OTel Collector.

This architecture decouples your application instrumentation from your observability backends, giving you immense flexibility.

Prometheus, Grafana, OpenTelemetry: The Observability Stack
Generated Image

Real-world Scenario: Tracing a Python Flask Service with OpenTelemetry

Let’s instrument our Flask app for tracing using OpenTelemetry. We’ll send traces to the OTel Collector, which can then forward them to a tracing backend like Jaeger or Grafana Tempo.

First, install the necessary OpenTelemetry packages:

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests

Then, modify your Flask app:

from flask import Flask, request
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
import time
import requests

# Configure OpenTelemetry
resource = Resource.create({
    "service.name": "my-flask-service",
    "service.version": "1.0.0",
    "host.name": "localhost"
})

provider = TracerProvider(resource=resource)
# Configure exporter to send data to OpenTelemetry Collector
otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True) # Default OTLP gRPC endpoint
span_processor = BatchSpanProcessor(otlp_exporter)
provider.add_span_processor(span_processor)
trace.set_tracer_provider(provider)

app = Flask(__name__)

# Auto-instrument Flask and Requests
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()

tracer = trace.get_tracer(__name__)

@app.route('/')
def hello_world():
    with tracer.start_as_current_span("hello-world-request"):
        time.sleep(0.05)
        # Make an outgoing request to simulate another service call
        try:
            requests.get("http://localhost:5001/data") # Assuming another service or mock
        except requests.exceptions.ConnectionError:
            print("Could not connect to http://localhost:5001/data, skipping external call.")
        return 'Hello, World from Traced Flask!'

@app.route('/login')
def login():
    with tracer.start_as_current_span("login-request"):
        time.sleep(0.1)
        return 'Login successful with Tracing!'

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

This code instruments your Flask app to automatically generate spans for incoming requests and outgoing `requests` calls. These spans are then sent to an OTLP endpoint (your OpenTelemetry Collector).

OpenTelemetry Collector Configuration (`otel-collector-config.yaml`)

You’ll need an OpenTelemetry Collector running. Here’s a basic configuration to receive OTLP traces and export them to a Jaeger backend (or Grafana Tempo):

receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  batch:

exporters:
  # For Jaeger (install Jaeger locally or use its Docker image)
  jaeger:
    endpoint: "localhost:14250" # Jaeger gRPC endpoint
    insecure: true
  # For Grafana Tempo (if you're using it)
  # otlp/tempo:
  #   endpoint: "tempo:4317" # Tempo's OTLP gRPC receiver endpoint
  #   tls:
  #     insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger] # Or [otlp/tempo]

Run the collector (e.g., using Docker: docker run -p 4317:4317 -v $(pwd)/otel-collector-config.yaml

Written by

Khader Vali

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

Share this article

Related Articles

Platform Engineering: Building Internal Developer Platforms

Jul 25, 2026 · 16 min read

GitHub Actions: CI/CD from Basics to Matrix Builds

Jun 20, 2026 · 17 min read

Blue/Green & Canary: Zero-Downtime Deployment Mastery

Jul 12, 2026 · 16 min read