Setting up Prometheus with Goa 🐊

Goa is a useful tool when designing services with golang. It generates code for you that allows for quick designs to be replaced by large volumes of code from those designs. I was recently given the task of implementing Prometheus metrics inside a golang service designed with Goa. Due to the nature of the Goa's design integrating this with Prometheus was not the easiest task and hopefully someone who is looking to do the same can look here. Setting up a Prometheus middleware within go alone is not that hard. package main import ( "fmt" "log" "net/http" "strconv" "" "" "" "" ) type responseWriter struct { http.ResponseWriter statusCode int } func NewResponseWriter(w http.ResponseWriter) *responseWriter { return &responseWriter{w, http.StatusOK} } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } var totalRequests = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "example_http_requests_total", Help: "Counter for total requests received", }, []string{"path"}, ) var responseStatus = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "example_response_status", Help: "Status of HTTP response", }, []string{"status"}, ) var httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "example_http_request_duration_seconds", Help: "Duration of HTTP requests in seconds.", }, []string{"path"}) func promMid(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { route := mux.CurrentRoute(r) path, _ := route.GetPathTemplate() timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) rw := NewResponseWriter(w) next.ServeHTTP(rw, r) statusCode := rw.statusCode responseStatus.WithLabelValues(strconv.Itoa( statusCode)).Inc() totalRequests.WithLabelValues(path).Inc() timer.ObserveDuration() }) } func init() { prometheus.Register(totalRequests) prometheus.Register(responseStatus) prometheus.Register(httpDuration) } func main() { router := mux.NewRouter() router.Use(promMid) // Prometheus endpoint router.Path("/metrics").Handler(promhttp.Handler()) // Serving static files router.PathPrefix("/").Handler(http.FileServer (http.Dir("./static/"))) fmt.Println("http://localhost:3000/") err := http.ListenAndServe(":3000", router) log.Fatal(err) } This is very similar to the examples given in the official golang prometheus libraries. The tricky parts when setting up the designs for goa. The main problem that arose for me was having to set the /metrics endpoint the same across golang and typescript services. The typescript services were manually generated routers and therefore routing was easier. The main downfall of the goa generation is specifying prefixes to routes. Suppose you have a prefix /v1/ before each of your routes. Inside your goa design you may then be tempted to state the following package design import . "" var _ = API("auth", func() { Title("Auth Service") Server("auth", func() { Services( "auth", "appkeys", "accesskeyssecrets", ) }) HTTP(func() { Path("/v1") }) }) With the Path("/v1") defining the prefix to each service. This, however, will not allow you to define your own prefix for the /metrics endpoint. Instead you must define each endpoint design as follows: var _ = Service("example", func() { Method("create", func() { Payload(ExampleCreateRequestPayload) Result(ExampleResponsePayload) HTTP(func() { POST("/") Headers(func() { ... }) Response(func() { Code(StatusCreated) ContentType(HTTPContentTypeApplicationJSONCharsetUTF8) }) }) }) HTTP(func() { Path("/v1/example") Response(ErrorNameUnexpected, func() { Code(StatusInternalServerError) ... Each individual path must be prefixed with the /v1/ rather than setting it at the higher level. Therefore when you set up the actual metric endpoint it would look something like this package design import . "" var _ = Service("metrics", func() { Method("metrics", func() { Result(String) HTTP(func() { GET("") Response(func() { Code(StatusOK) ContentType(HTTPContentTypeApplicationJSONCharsetUTF8) }) }) }) Error(ErrorNameUnexpected, UnexpectedErrorResponsePayload) HTTP(func() { Path("/metrics") Response(ErrorNameUnexpected, func() { Code(StatusInternalServerError) ContentType(HTTPContentTypeApplicationJSONCharsetUTF8) }) }) }) The actual middleware looks very similar to the initial example given. It is then simply a case of providing all the correct endpoints within the service itself, as dictated by the goa documentation.