We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Custom Metrics

Now that we have Linko configured to export the default Prometheus metrics, it's time to set up some custom metrics!

The most basic custom metric that virtually every web app should export is a count of HTTP requests by method, path, and status.

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

This snippet defines a package-level Prometheus counter vector with three important attributes:

  • Name – The key we'll query via Prometheus/Grafana to read the counter values
  • Help – Human-readable text describing the counter
  • Labels – Labels let us track fine-grained attributes for each counter value.

Counter Labels

I defined three labels above: method, path, and status. These labels give us three "dimensions" across which we can track HTTP requests.

Imagine over the course of a minute, your web application receives four HTTP requests, as a user attempts to log in, unsuccessfully at first:

  • GET /login 200 OK
  • POST /login 401 Unauthorized
  • POST /login 401 Unauthorized
  • POST /login 200 OK

This increments the http_requests_total counter four times, each with different labels.

method path status http_requests_total
GET /login 200 1
POST /login 401 2
POST /login 200 1

Request-Tracking Middleware

I prefer middleware that handles metric tracking so my "business logic" handlers (the code that does what users care about, like shortening links) don't have to worry about it.

First, define a custom http.ResponseWriter wrapper that captures an HTTP status code when it's written:

type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}

Then I just write a little middleware function that wraps each request, but also captures the method, path, and status and increments the counter:

func metricsMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rec := &statusRecorder{
			ResponseWriter: w,
			status:         http.StatusOK,
		}

		next.ServeHTTP(rec, r)

		path := r.URL.Path
		method := r.Method
		status := strconv.Itoa(rec.status)

		httpRequestsTotal.
			WithLabelValues(method, path, status).
			Inc()
	})
}

Assignment

go run .

Run and submit the CLI tests from the root of the Linko repo.