Phase 2: Observability

Application instrumentation: counters, histograms & gauges

Intermediate ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you're running the best lemonade stand ever. You’ve got delicious lemonade, happy customers, and maybe even some fancy straws. But how do you know if your stand is doing well? Or if you need to make more lemonade? Or if your special new 'speedy serving' technique is actually speedy? Just like you want to know what's happening with your lemonade stand, people who build computer programs, like games or websites, want to know what their programs are doing too. They can’t just stand there and watch everything at once. So, they add special tools inside their programs to help them keep track of things, almost like little secret helpers that report back important info. This is called "instrumentation" – adding tools to measure how your program works.

One of these helpers is like a counter. Think of it as a clicker, like the kind a doorman uses to count how many people enter a building. Every single time a customer buys a cup of lemonade, your little helper clicks it up by one. It never goes down, only up! So, by the end of the day, if you look at your clicker, you’ll know exactly how many cups of lemonade you’ve sold total. If you check it every hour, you can see how many cups you sold in that hour. This tells you things like, "Wow, we sold 50 cups today! That’s great!" or "Between 2 and 3 PM was our busiest hour, we sold 20 cups then!"

Another helper is like a gauge. Imagine a thermometer that tells you the current temperature, or a fuel gauge in a car that shows how much gas you have left. This helper tells you something that can go up or down. For your lemonade stand, a gauge could tell you how much lemonade you currently have left in your big pitcher – it goes down as you serve, and up when you refill it. Or it could show you how much money is currently in your cash box. These gauges tell you the current situation right now, like "We only have one pitcher left, better make more!" or "We have $30 in the till at this moment."

So, by using counters and gauges, you can get a really good idea of how your lemonade stand is performing. You know your total sales and your current stock. This means when you build your own computer programs later, you can add these same kinds of helpers to understand exactly what your program is doing. You can see how many times someone clicked a button (a counter) or how many people are currently using your game (a gauge). But what these helpers don’t tell you is something like, 'how long did it take each customer to get their lemonade?' or 'how long did it take to make each cup?' That’s where even more special helpers come in, but we'll talk about those later!

Application instrumentation is the act of embedding code within your application to expose internal metrics about its behavior and performance. For an SRE, this is the foundational layer of observability, providing the raw data Prometheus will scrape and Grafana will visualize. We primarily focus on three core metric types: Counters, Gauges, and Histograms. A Counter is a simple, monotonically increasing value, meaning it only ever goes up (or resets to zero on application restart). Use counters for tracking events like the total number of HTTP requests received, errors encountered, or bytes sent over a network. They are perfect for understanding cumulative activity, and when queried over time, can show rates (e.g., requests per second). In contrast, a Gauge represents a single numerical value that can go up or down at any point. Think of it as a thermometer or a speedometer, reflecting the current state. Examples include the current number of active users, available memory, CPU utilization, or the size of a processing queue.

While counters and gauges are excellent for simple counts and current states, they fall short when you need to understand the distribution of values, especially for critical performance metrics like request latency or response sizes. This is where Histograms shine. A histogram samples observations and groups them into configurable buckets (e.g., 0-10ms, 10-50ms, 50-100ms, etc.). Alongside these buckets, it also provides the total count of observations and their sum. The power of histograms lies in their ability to calculate percentiles (e.g., p99 latency) directly within Prometheus. This is crucial because averages can mask critical performance issues – a high average latency might hide that 1% of your users are experiencing very slow responses, something a histogram clearly reveals.

Effectively instrumenting your applications with these metric types is non-negotiable for building robust SRE practices. Prometheus client libraries (available for most languages) simplify exposing these metrics via a standard HTTP endpoint, which Prometheus then scrapes. In Grafana, you'll use Prometheus's powerful query language (PromQL) to turn these raw metrics into meaningful insights: calculating rates from counters, plotting current values from gauges, and deriving crucial latency percentiles from histograms. This direct feedback loop from your application's internals allows you to quickly identify performance bottlenecks, diagnose issues, and proactively improve system reliability, moving beyond simple "up/down" monitoring.

Key Takeaways

  • Counters track monotonically increasing events (e.g., total requests, errors).
  • Gauges track current state values that can go up or down (e.g., active users, CPU usage).
  • Histograms track the distribution of observations (e.g., request latency) allowing for percentile calculations.
  • Proper instrumentation is fundamental for deep observability into application behavior.
  • Prometheus client libraries simplify exposing these metrics from your code.

Code Example

go
import (
	"github.com/prometheus/client_golang/prometheus"
	"time"
)

// Declare metrics using Prometheus client library
var (
	requestsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{Name: "app_requests_total", Help: "Total requests."}, []string{"endpoint"})
	activeConnections = prometheus.NewGauge(prometheus.GaugeOpts{
		Name: "app_active_connections", Help: "Current active connections."})
	requestLatency = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{Name: "app_request_latency_seconds", Help: "Request latency."},
		[]string{"endpoint"})
)

func init() {
	prometheus.MustRegister(requestsTotal, activeConnections, requestLatency)
}

// Example usage within your application logic
func handleAppEvent(endpoint string, duration time.Duration) {
	requestsTotal.WithLabelValues(endpoint).Inc() // Increment counter
	activeConnections.Inc()                       // Adjust gauge (could also Dec() or Set())
	requestLatency.WithLabelValues(endpoint).Observe(duration.Seconds()) // Observe histogram
}

How this code works

This code instruments an application to emit crucial metrics for monitoring using the Prometheus client library. Its primary job is to declare and prepare three types of metrics: a counter for total requests, a gauge for current active connections, and a histogram for request latency. prometheus.NewCounterVec creates requestsTotal, designed to count events and categorize them by endpoint labels. prometheus.NewGauge defines activeConnections to track a value that fluctuates. prometheus.NewHistogramVec sets up requestLatency to measure the distribution of durations, also categorized by endpoint. The init() function automatically registers these metrics with the Prometheus system, making them discoverable and available for data collection.

The handleAppEvent function then demonstrates how an application's logic updates these metrics. requestsTotal.WithLabelValues(endpoint).Inc() increments the counter for a specific endpoint, tracking each request. activeConnections.Inc() adjusts the gauge, reflecting changes in active connections (it could also Dec() or Set() directly). For requestLatency.WithLabelValues(endpoint).Observe(), it's vital to note that histograms expect values in seconds, represented as a float64. Therefore, duration.Seconds() explicitly converts the time.Duration into the necessary format, preventing misinterpretation of latency values if other time units were implicitly used.