Instrumenting & Monitoring Go Apps with Prometheus
This article offers a comprehensive guide to integrating Prometheus metrics in your Go application.
It covers essential concepts such as instrumenting your code with different metric types, tracking HTTP server activity, and efficiently exposing metrics for Prometheus to scrape.
You'll find the complete source code for this tutorial in this GitHub repository.
Let's dive in!
Prerequisites
- Prior Go development experience and a recent version installed.
- Familiarity with Docker and Docker Compose.
- Basic familiarity with the Prometheus metric types.
Step 1 — Setting up the demo project
Let's begin by setting up the demo project, which is a simple "Hello World" server. Clone the following GitHub repository to your local machine using the command below:
Next, navigate to the project directory:
This project includes a docker-compose.yml file that defines two services:
app: This service runs the application on port 8000 and uses air to enable live reloading whenever files are changed.prometheus: This service runs the Prometheus server, configured using theprometheus.ymlfile.
Before starting the services, rename the .env.example file to .env. This
file contains basic configuration settings for the application:
Use the following command to rename the file:
Now, start both services in the background with this command:
You should observe output similar to this:
To verify the application is working, send a request to the root endpoint using
curl:
This should return the following response:
You can also navigate to http://localhost:9090/targets to view the Prometheus
UI and confirm that your golang-demo application is accessible:
With the services running successfully, we can proceed to integrate the Prometheus packages in the next step.
Step 2 — Exposing the default metrics
Before you can instrument your application with Prometheus, you need to install the necessary packages first:
This command installs the prometheus and promhttp packages. The former is the core library for defining and managing metrics, while the latter provides an HTTP handler for exposing metrics in a Prometheus-compatible format.
To integrate them into your Go application, modify your main.go file as
follows:
This imports the promhttp package which provides an HTTP handler that serves
metrics in a format that Prometheus can scrape.
If you visit the http://localhost:8000/metrics in your browser, you will see
the following output:
By default, Prometheus uses the global registry, which automatically registers standard Go runtime metrics and process metrics. While Prometheus gathers a wide range of default metrics, they're not usually essential for typical monitoring needs.
To disable these, you must create a custom registry and register only the
metrics you care about. Then pass your custom registry to
promhttp.HandlerFor() to expose only the metrics you explicitly register:
Since we haven't explicitly registered any custom metrics, the /metrics route
will return an empty response. In the following sections, you will instrument
the Counter, Gauge, Histogram, and Summary metric in turn.
Step 3 — Instrumenting a Counter metric
Let's start with a rather basic metric that keeps track of the total number of HTTP requests made to the server. Since this number always goes up, it is best represented as a Counter.
Edit your main.go file to include the counter instrumentation:
In this snippet, the httpRequestCounter metric is created using a CounterVec
which is a Prometheus metric type that tracks a cumulative count of events and
supports labels for categorization. The prometheus package also exposes a
Counter type, but it does not support using labels.
The CounterOpts struct specifies the metric's name, and provides a short
description of its purpose. The metric is then labeled with HTTP status code,
requested URL path, and the HTTP method like GET or POST.
These labels allow you to distinguish between different types of requests and provide detailed breakdowns for analysis when querying with PromQL later on.
To automatically increment the request count for all current and future HTTP
routes, you created a prometheusMiddleware() function that wraps the existing
HTTP handler. It uses a statusRecorder to capture the HTTP status code, which
is not directly accessible during request handling.
After processing the request with next.ServeHTTP(), the middleware extracts
the HTTP method, path, and status code, then increments the counter using
httpRequestCounter.WithLabelValues.Inc(). This ensures that every HTTP request
is counted and categorized by its attributes.
Finally, the httpRequestCounter must be explicitly registered with the custom
Prometheus registry, and the existing HTTP multiplexer (mux) is wrapped with
the prometheusMiddleware().
By doing this, every incoming HTTP request will pass through the middleware, ensuring that request counter metrics are updated as part of the request lifecycle.
Once you've saved the file, you may refresh the http://localhost:8000/metrics
page a couple of times. You will see the following results:
To send a steady stream of requests to a route, you can use a benchmarking tool like wrk:
You can visit the Prometheus web interface at http://localhost:9090 to see
your metrics in action. Typing http_requests_total in the expression input and
clicking Execute will display the raw metric values:
You can then switch to the Graph tab to see the counter increasing continually as long as the service continues to handle HTTP requests:
Let's look at how to instrument a Gauge metric next.
Step 4 — Instrumenting a Gauge metric
A Gauge represents a value that can go up or down, making it ideal for tracking values like active connections, queue sizes, or memory usage.
In this section, we'll use a Prometheus Gauge to monitor the number of active connections to the service.
Here's what you need to add to your main.go file:
The activeRequestsGauge metric is created with prometheus.NewGauge() to keep
track of the number of active HTTP requests being processed at any given time.
If you want to add labels, use NewGaugeVec() instead.
At the start of the middleware function, the activeRequestsGauge.Inc() method
is called to increment the gauge when a new HTTP request arrives.
The time.Sleep() method is then used to simulate a delay of one second. In
real-world scenarios, the time taken to process the request would depend on
factors such as database queries, external API calls, and other business logic.
Finally, once the request handler returns, the activeRequestsGauge.Dec()
method is called to decrement the gauge, indicating that the has finished
processing and is no longer active.
Ensure to register the activeRequestsGauge and save the file, then send some
load to your server with:
When you visit your metric endpoint, you'll see the following output:
This indicates that there are currently 407 active connections to the service.
You may query this metric and see the Graph tab to check how this number is changing over time.
Next up, you'll instrument a Histogram metric to track the HTTP request latency for your routes.
Step 5 — Instrumenting an Histogram metric
Histograms are helpful for tracking distributions of measurements, such as the
duration of HTTP requests. In Prometheus, creating a Histogram metric is
straightforward with the prometheus.NewHistogramVec() method:
Instead of a fixed delay of one second, we've introduced a random delay between
0ms and 1s using rand.Intn() so that you can observe a range of values.
After the request is processed, the duration is calculated as the difference between the current and recorded start times. This value is then converted to seconds and recorded into the histogram.
Once the histogram is registered, it will be exposed at the /metrics endpoint.
By generating some load to your endpoints, you'll see the following output in
/metrics:
Each _bucket line shows the cumulative count of requests completed in less
than or equal to a specific duration. For example:
le="0.005": No requests completed within 5 milliseconds.le="0.025": 4 requests completed within 25 milliseconds.le="0.5": 85 requests completed within 0.5 seconds (500ms).le="+Inf": 154 requests completed in total (all durations).
The default buckets for a histogram metric are:
If this doesn't make sense for your use case, you can define a custom bucket with:
In the Prometheus UI, you can use the following PromQL query to calculate the 99th percentile latency for HTTP requests over a 1-minute window:
Step 6 — Instrumenting a Summary metric
A Summary metric in Prometheus is useful for capturing pre-aggregated quantiles (e.g., median, 95th, or 99th percentile) and providing overall counts and sums for specific observations.
A key feature of summaries is their ability to calculate quantiles directly on the client side, which makes them particularly valuable in scenarios where you need quantiles for individual instances and don't want to rely on Prometheus for these computations.
While the histogram is usually preferred for tracking request latency, the Summary can be used for precomputing quantiles for response times of critical API endpoints where aggregation isn't required.
Here's how to set it up:
The postLatencySummary metric is used here to measure and monitor the latency
of requests to the external API endpoint
https://jsonplaceholder.typicode.com/posts.
The Objectives field in the SummaryOpts structure specifies the quantiles to
calculate. Each quantile is associated with a tolerance value, which defines
the allowable margin of error for the quantile calculation.
- For
0.5: 0.05, the actual quantile value may have up to a 5% error margin. - For
0.9: 0.01, the margin of error is stricter at 1%.
When choosing tolerances, keep in mind that lower values require more memory and processing power.
In /posts handler, the duration for the request is calculated and recorded
into the summary metric.
Once you've saved the file, send some requests to the /posts endpoint:
When you scrape your application metrics now, you'll see an output that looks like this:
This shows the shows precomputed quantiles (50th, 90th, and 99th percentiles)
and overall statistics (sum and count) for request durations to
https://jsonplaceholder.typicode.com/posts.
From the results, we can deduce that:
- Half of all requests completed in less than or equal to 0.1095 seconds (about 110 milliseconds),
- 90% of all requests completed in less than or equal to 0.1271 seconds (about 127 milliseconds),
- and 99% of all requests completed in less than or equal to 0.5448 seconds (about 545 milliseconds).
This 0.99 quantile is much higher than the 0.9 quantile, which indicates the presence of a few slower requests.
Final thoughts
In this tutorial, we explored the fundamentals of setting up and using Prometheus metrics in a Go application, covering key concepts such as defining and registering counters, gauges, histograms, and summaries.
As the next steps, consider integrating alerting with Prometheus Alertmanager or visualizing metrics using tools like Grafana and Better Stack.
Exploring advanced PromQL queries can also help you unlock powerful ways to analyze and act on your application's data.
Thanks for reading, and happy monitoring!