DevOps

Observability with Prometheus, Grafana & OpenTelemetry

Master observability with Prometheus for metrics, Grafana for visualization, and OpenTelemetry for traces and logs. A complete guide to building a robust monitoring stack.

Khader Vali July 3, 2026 15 min read

Mastering Observability: Prometheus, Grafana, and OpenTelemetry in Harmony

As software systems grow in complexity, particularly with the advent of microservices and cloud-native architectures, simply knowing if your application is “up” is no longer enough. We need deep insights into why things are happening, to understand the intricate dances between services, and to predict potential issues before they impact users. This is where the powerful trio of Prometheus, Grafana, and OpenTelemetry comes into play, forming the bedrock of a robust and insightful observability stack.

In this comprehensive guide, we’ll embark on a journey to demystify monitoring and observability. We’ll explore the individual strengths of Prometheus for metrics collection, Grafana for stunning visualizations and alerting, and OpenTelemetry for standardized instrumentation of traces, metrics, and logs. More importantly, we’ll learn how to weave them together into a coherent system that empowers you to understand, diagnose, and optimize your applications like never before.

By the end of this article, you’ll have a solid grasp of:

  • The fundamental differences between monitoring and observability.
  • How Prometheus collects, stores, and queries time-series metrics.
  • Leveraging Grafana to build insightful dashboards and powerful alerts.
  • The role of OpenTelemetry in providing a vendor-neutral standard for instrumenting your code.
  • Architecting a unified observability stack with these three tools.
  • Best practices and advanced considerations for production environments.

Let’s dive in and elevate our understanding of our systems.

Monitoring vs. Observability: A Critical Distinction

Before we introduce our tools, it’s crucial to understand the conceptual difference between monitoring and observability. While often used interchangeably, they represent distinct approaches to understanding system health.

What is Monitoring?

Monitoring is about collecting predefined sets of metrics and logs to answer known questions. Think of it as checking the vital signs of a patient: heart rate, blood pressure, temperature. You’re looking for deviations from expected norms. Traditional monitoring tools focus on “what” is happening:

  • Is the CPU utilization high?
  • Is the disk space critically low?
  • Are requests per second within an acceptable range?
  • Are there any 5xx errors?

Monitoring provides dashboards and alerts based on symptoms. It’s excellent for reactive responses to anticipated problems. If you know what to look for, monitoring will tell you if it’s there. It thrives on “known unknowns.”

What is Observability?

Observability, on the other hand, is the ability to infer the internal state of a system by examining its external outputs (metrics, logs, traces). It’s about being able to ask arbitrary, unforeseen questions about your system’s behavior, even in complex, distributed environments. If monitoring tells you “what” happened, observability helps you understand “why.”

Observability provides the tools and data to debug distributed systems and answer “unknown unknowns.” It focuses on:

  • Why is latency spiking specifically for users in region X accessing feature Y?
  • What specific microservice dependency is causing a cascade of errors?
  • How does a new database query impact the end-to-end performance of a user request?

Observability leverages three core pillars:

  1. Metrics: Numerical data points collected over time, often aggregated (e.g., CPU utilization, request rate, error count). Excellent for spotting trends and overall health.
  2. Logs: Discrete, timestamped text records of events that occurred within an application (e.g., error messages, request details, user actions). Crucial for debugging specific incidents.
  3. Traces: Represent the end-to-end journey of a single request or operation as it propagates through a distributed system. Each step in the journey is a “span,” showing timing and metadata. Essential for understanding latency and pinpointing bottlenecks in microservices.

While monitoring is a subset of observability, a truly observable system goes beyond just basic health checks to provide the rich, granular, and correlated data needed for deep introspection. This is where our chosen tools shine.

Prometheus: The Metric 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 monitoring landscape, primarily due to its powerful multi-dimensional data model and flexible query language (PromQL).

Key Characteristics of Prometheus

  • Pull Model: Unlike many traditional monitoring systems that push data, Prometheus actively “scrapes” metrics endpoints (usually /metrics) exposed by your applications or dedicated exporters. This makes it robust to transient network issues and simplifies configuration.
  • Time-Series Database (TSDB): Prometheus stores all data as time-series, identified by a metric name and key/value pairs (labels). These labels allow for powerful and flexible querying.
  • PromQL: A functional query language that lets you slice, dice, aggregate, and perform complex calculations on your time-series data.
  • Alerting: Integrated with Alertmanager, Prometheus can send sophisticated alerts based on PromQL expressions.
  • Service Discovery: Supports various mechanisms (Kubernetes, AWS EC2, DNS, file-based) to automatically discover targets to scrape.

Prometheus Architecture Components

To understand Prometheus, let’s break down its core components:

  1. Prometheus Server: The brain of the operation. It scrapes metrics from configured targets, stores them in its TSDB, and executes PromQL queries.
  2. Exporters: Lightweight agents that expose metrics from third-party systems (e.g., Node Exporter for host metrics, Blackbox Exporter for endpoint probing, database exporters). Applications can also expose their own metrics directly using client libraries.
  3. Pushgateway: A specialized intermediary for ephemeral or batch jobs that cannot be reliably scraped. These jobs push their metrics to the Pushgateway, which Prometheus then scrapes.
  4. Alertmanager: Handles alerts sent by Prometheus. It can deduplicate, group, and route alerts to various notification channels (email, Slack, PagerDuty, etc.) and handle silences.
  5. Client Libraries: Available for various programming languages (Go, Java, Python, Ruby, Node.js, etc.) to instrument your application code to expose custom metrics.

Observability with Prometheus, Grafana & OpenTelemetry
Generated Image

Prometheus Architecture (Diagram in words):


+-------------------+      +-------------------+      +-------------------+
|   Application 1   |      |   Application 2   |      |   Node Exporter   |
| (Prometheus Client| ---- | (Prometheus Client| ---- | (Host Metrics)    |
|   Library)        |      |   Library)        |      |                   |
| /metrics endpoint |      | /metrics endpoint |      | /metrics endpoint |
+-------------------+      +-------------------+      +-------------------+
          ^                          ^                          ^
          |                          |                          |  (HTTP Scrape)
          |                          |                          |
          |  +-------------------------------------------------+
          |  |                                                 |
          v  v                                                 v
+-----------------------------------------------------------------+
|                                                                 |
|                    Prometheus Server                            |
|  - Scrapes /metrics endpoints                                   |
|  - Stores time-series data (TSDB)                               |
|  - Executes PromQL queries                                      |
|  - Generates alerts based on rules                              |
|                                                                 |
+-----------------------------------------------------------------+
          |                                   ^
          | (Alerts)                          | (Queries)
          v                                   |
+-------------------+                     +-------------------+
|   Alertmanager    |                     |      Grafana      |
|  - Deduplicates   |                     |  - Dashboards     |
|  - Groups         |                     |  - Visualization  |
|  - Routes alerts  |                     |  - Ad-hoc Queries |
|  (e.g., Slack, Email)                   |                   |
+-------------------+                     +-------------------+

Instrumenting an Application with Prometheus (Go Example)

Let’s see how easy it is to add Prometheus metrics to a simple Go application. We’ll expose a counter for HTTP requests and a gauge for the number of currently active requests.

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	httpRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
		Name: "app_http_requests_total",
		Help: "Total number of HTTP requests.",
	}, []string{"path", "method", "status"})

	httpActiveRequests = promauto.NewGauge(prometheus.GaugeOpts{
		Name: "app_http_active_requests",
		Help: "Number of active HTTP requests.",
	})

	httpLatencySeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "app_http_request_duration_seconds",
		Help:    "Histogram of HTTP request latencies in seconds.",
		Buckets: prometheus.DefBuckets, // Default buckets: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10
	}, []string{"path", "method", "status"})
)

func handler(w http.ResponseWriter, r *http.Request) {
	start := time.Now()
	httpActiveRequests.Inc()
	defer httpActiveRequests.Dec()

	// Simulate some work
	time.Sleep(time.Duration(200+randomInt(500)) * time.Millisecond) // Simulate 200-700ms latency

	status := http.StatusOK
	if r.URL.Path == "/error" {
		status = http.StatusInternalServerError
		http.Error(w, "Simulated Error", status)
	} else {
		fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
	}

	duration := time.Since(start).Seconds()
	httpRequestsTotal.WithLabelValues(r.URL.Path, r.Method, fmt.Sprintf("%d", status)).Inc()
	httpLatencySeconds.WithLabelValues(r.URL.Path, r.Method, fmt.Sprintf("%d", status)).Observe(duration)
}

func randomInt(max int) int {
	return int(time.Now().UnixNano() % int64(max)) // Simple non-cryptographic random
}

func main() {
	http.HandleFunc("/", handler)
	http.HandleFunc("/error", handler) // Expose an error path too

	// Expose Prometheus metrics on a different port or path
	http.Handle("/metrics", promhttp.Handler())

	fmt.Println("Server started on :8080. Metrics exposed on :8081/metrics")
	go func() {
		log.Fatal(http.ListenAndServe(":8081", nil)) // Prometheus metrics endpoint
	}()
	log.Fatal(http.ListenAndServe(":8080", nil)) // Application endpoint
}

To run this:

  1. Save as main.go.
  2. Run go mod init myapp; go mod tidy.
  3. Run go run main.go.

Now, if you visit http://localhost:8081/metrics, you’ll see a long list of metrics, including our custom ones:

# HELP app_http_requests_total Total number of HTTP requests.
# TYPE app_http_requests_total counter
app_http_requests_total{method="GET",path="/",status="200"} 1
app_http_requests_total{method="GET",path="/error",status="500"} 1
# HELP app_http_active_requests Number of active HTTP requests.
# TYPE app_http_active_requests gauge
app_http_active_requests 0
# HELP app_http_request_duration_seconds Histogram of HTTP request latencies in seconds.
# TYPE app_http_request_duration_seconds histogram
app_http_request_duration_seconds_bucket{method="GET",path="/",status="200",le="0.005"} 0
app_http_request_duration_seconds_bucket{method="GET",path="/",status="200",le="0.01"} 0
...
app_http_request_duration_seconds_bucket{method="GET",path="/",status="200",le="10"} 1
app_http_request_duration_seconds_bucket{method="GET",path="/",status="200",le="+Inf"} 1
app_http_request_duration_seconds_sum{method="GET",path="/",status="200"} 0.345
app_http_request_duration_seconds_count{method="GET",path="/",status="200"} 1
...

Configuring Prometheus to Scrape

Create a prometheus.yml file:

global:
  scrape_interval: 15s # How frequently to scrape targets

scrape_configs:
  - job_name: 'my-go-app'
    static_configs:
      - targets: ['localhost:8081'] # Our Go application's metrics endpoint

Run Prometheus (e.g., using Docker):

docker run \
    -p 9090:9090 \
    -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus

Now, Prometheus will scrape your Go application every 15 seconds. You can access the Prometheus UI at http://localhost:9090 and query your metrics.

PromQL Examples

  • Total HTTP requests over the last 5 minutes:
    sum(rate(app_http_requests_total[5m]))
  • Average latency for successful requests on the root path:
    rate(app_http_request_duration_seconds_sum{path="/",status="200"}[5m]) / rate(app_http_request_duration_seconds_count{path="/",status="200"}[5m])
  • Number of active requests:
    app_http_active_requests

Real-world Scenario: Monitoring an E-commerce API

Imagine you run an e-commerce platform. Using Prometheus, you can monitor critical metrics:

  • Request Rate: sum(rate(api_requests_total{service="product-catalog"}[5m])) to see how many requests per second your product catalog service is handling.
  • Error Rate: sum(rate(api_requests_total{service="checkout",status=~"5xx|4xx"}[5m])) / sum(rate(api_requests_total{service="checkout"}[5m])) to calculate the percentage of errors in your checkout service.
  • Latency Percentiles: Using histograms, you can query histogram_quantile(0.99, sum by (le, path) (rate(api_request_duration_seconds_bucket{path="/order"}[5m]))) to understand the 99th percentile latency for order placement requests, crucial for user experience.

Prometheus provides the raw power to collect and query this data efficiently, setting the stage for visualization.

Grafana: Visualizing the Data and Alerting

Grafana is an open-source platform for monitoring and observability that allows you to query, visualize, alert on, and explore your metrics, logs, and traces no matter where they are stored. It’s the “dashboard king,” bringing your data to life with beautiful and interactive visualizations.

Key Features of Grafana

  • Data Source Agnostic: Connects to a wide array of data sources, including Prometheus, InfluxDB, PostgreSQL, MySQL, Elasticsearch, Loki, Jaeger, and more.
  • Dashboards: Build dynamic and interactive dashboards using a variety of panel types (graphs, single stats, tables, heatmaps, gauge, etc.).
  • Templating: Use variables to create flexible dashboards that can adapt to different services, environments, or instances with a single click.
  • Alerting: Define alert rules based on your queries and send notifications to various channels.
  • Plugins: Extend functionality with community and enterprise plugins for new data sources, panel types, and apps.

Connecting Grafana to Prometheus

After installing Grafana (e.g., via Docker), the first step is to add Prometheus as a data source.

docker run -p 3000:3000 grafana/grafana-oss

Navigate to http://localhost:3000 (default user/pass: admin/admin).

  1. Go to Configuration (gear icon) -> Data Sources.
  2. Click “Add data source” and select “Prometheus.”
  3. Set the URL to http://localhost:9090 (or wherever your Prometheus server is running).
  4. Click “Save & Test.” You should see “Data source is working.”

Building a Basic Dashboard

Let’s create a dashboard to visualize the metrics from our Go application.

  1. Click the Dashboards icon -> “New Dashboard” -> “Add a new panel.”
  2. In the “Query” tab, select your Prometheus data source.
  3. Enter a PromQL query. For example, to see the total HTTP requests per second:
    sum(rate(app_http_requests_total[1m])) by (path, method, status)
  4. Choose a visualization type, like “Graph.”
  5. Give the panel a title (e.g., “HTTP Requests per Second”).
  6. Add another panel for active requests:
    app_http_active_requests

    Visualize this as a “Gauge” or “Stat” panel.

  7. Add a panel for P90 latency:
    histogram_quantile(0.90, sum by (le, path) (rate(app_http_request_duration_seconds_bucket{status="200"}[5m])))

    Visualize this as a “Graph.”

  8. Save your dashboard.

Real-world Scenario: A Microservice Overview Dashboard

Consider a dashboard for a critical microservice like an “Order Processing Service.”

  • Top Row: SLOs at a Glance:
    • Panel 1 (Stat): “Request Rate” – sum(rate(http_requests_total{service="order-processing"}[1m]))
    • Panel 2 (Stat): “Error Rate (5xx)” – sum(rate(http_requests_total{service="order-processing", status=~"5.."}[1m])) / sum(rate(http_requests_total{service="order-processing"}[1m])) * 100
    • Panel 3 (Stat): “P99 Latency” – histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{service="order-processing"}[5m])))
  • Mid Row: Resource Utilization:
    • Panel 4 (Graph): “CPU Utilization” – node_cpu_seconds_total{mode="idle",instance=~"$instance"} (inverted)
    • Panel 5 (Graph): “Memory Usage” – node_memory_MemTotal_bytes{instance=~"$instance"} - node_memory_MemFree_bytes{instance=~"$instance"}
  • Bottom Row: Detailed Request Breakdown:
    • Panel 6 (Graph): “Requests by Endpoint” – sum by (path) (rate(http_requests_total{service="order-processing"}[1m]))
    • Panel 7 (Table): “Top Erroring Endpoints” – Displaying specific error codes and paths.

By using Grafana’s templating features, you can make this dashboard dynamic, allowing users to select different services or instances from a dropdown, making it a powerful tool for operations teams.

OpenTelemetry: The Observability Standard

While Prometheus is fantastic for metrics, and Grafana for visualization, what about logs and traces, especially in distributed systems? This is where OpenTelemetry (OTel) steps in. OpenTelemetry is a vendor-neutral set of APIs, SDKs, and tools designed to standardize the collection of telemetry data (metrics, logs, and traces) from your applications.

The Problem OpenTelemetry Solves

Before OpenTelemetry, collecting observability data was often a fragmented and vendor-specific endeavor:

  • Different client libraries for different types of telemetry (e.g., Prometheus for metrics, Jaeger for traces, Log4j for logs).
  • Vendor lock-in: Once you instrumented your code with a specific vendor’s SDK, switching meant significant refactoring.
  • Inconsistent data formats and correlation across telemetry types.

OpenTelemetry aims to be the single standard for instrumenting your services, regardless of the backend you choose. You instrument once, and you can export to any compatible backend.

OpenTelemetry Components

  1. API: Defines how telemetry data is generated (e.g., creating a span, recording a metric, logging an event).
  2. SDK: Implements the API for specific languages, providing mechanisms for processing and exporting data.
  3. Collector: A proxy that receives, processes, and exports telemetry data. It’s highly configurable and can send data to multiple backends simultaneously (e.g., Prometheus for metrics, Jaeger for traces, Loki/Elasticsearch for logs).

Observability with Prometheus, Grafana & OpenTelemetry
Generated Image

OpenTelemetry Architecture (Diagram in words):


+-------------------+      +-------------------+      +-------------------+
|   Application 1   |      |   Application 2   |      |   Application 3   |
| (OTel SDK: Python)| ---- | (OTel SDK: Java)  | ---- | (OTel SDK: Go)    |
| - Generates Traces|      | - Generates Traces|      | - Generates Traces|
| - Generates Metrics|     | - Generates Metrics|     | - Generates Metrics|
| - Generates Logs  |      | - Generates Logs  |      | - Generates Logs  |
+-------------------+      +-------------------+      +-------------------+
          |                          |                          |
          |                          |                          |  (OTLP/HTTP/gRPC)
          |                          |                          |
          v                          v                          v
+-----------------------------------------------------------------+
|                                                                 |
|                 OpenTelemetry Collector                         |
|  - Receives telemetry data (OTLP, Jaeger, Prometheus, etc.)     |
|  - Processes data (filter, batch, enrich, sample)               |
|  - Exports data to various backends (OTLP, Prometheus, Jaeger,  |
|    Loki, Elasticsearch, Zipkin, etc.)                           |
|                                                                 |
+-----------------------------------------------------------------+
          |          |          |          |          |
          | (Metrics)| (Traces) | (Logs)   |          |
          v          v          v          v          v
+-------------------+  +-------------------+  +-------------------+
|     Prometheus    |  |       Jaeger      |  |        Loki       |
|  (Metrics Backend)|  |  (Tracing Backend)|  |  (Logging Backend)|
+-------------------+  +-------------------+  +-------------------+
          |                     |                     |
          |                     |                     | (Queries)
          v                     v                     v
+-----------------------------------------------------------------+
|                                                                 |
|                            Grafana                              |
|  - Dashboards for Metrics, Traces, Logs                         |
|  - Correlation across data types                                |
|                                                                 |
+-----------------------------------------------------------------+

Focusing on Traces with OpenTelemetry (Python Example)

While OTel supports metrics and logs, its strength often lies in providing a unified approach to distributed tracing. Let’s instrument a simple Python Flask application to generate traces.

First, install necessary libraries:

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

Python Flask application (app.py):

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
import time
import requests

app = Flask(__name__)

# --- OpenTelemetry Configuration ---
# Resource for identifying our service
resource = Resource.create({
"service.name": "my-python-service",
"service.version": "1.0.0",
"environment": "development"
})

# Tracer Provider
provider = TracerProvider(resource=resource)
trace.set_tracer_provider(provider)

# OTLP Exporter (sends traces to OpenTelemetry Collector, default gRPC port 4317)
span_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True) # Use insecure for local dev

# Batch Span Processor (batches spans before exporting)
span_processor = BatchSpanProcessor(span_exporter)
provider.add_span_processor(span_processor)

# Instrument Flask to automatically create spans for requests
FlaskInstrumentor().instrument_app(app)

# Get a tracer for manual instrumentation
tracer = trace.get_tracer(__name__)

# --- Application Routes ---
@app.route("/")
def hello_world():
# Manual span for some internal processing
with tracer.start_as_current_span("process_root_request"):
time.sleep(0.05) # Simulate some work
return "Hello, World! From Python Service."

@app.route("/greet/")
def greet(name):

Written by

Khader Vali

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

Share this article

Related Articles

Kubernetes Production Patterns for Real-Time Applications

Sep 28, 2024 · 2 min read

CI/CD Pipeline Architecture for Multi-Team Organizations

Aug 26, 2024 · 1 min read

SBOM & Sigstore: Securing Your Software Supply Chain

Jun 26, 2026 · 15 min read