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"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp") 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 "goa.design/goa/v3/dsl"
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.
"goa.design/goa/v3/dsl"
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.