Merge pull request 'Add request_duration_seconds metric' (#72) from metrics into master

Reviewed-on: dsprenkels/rushlink#72
This commit is contained in:
mrngm 2021-05-16 21:10:48 +02:00
commit 29ee3dc6fd
2 changed files with 86 additions and 30 deletions

View File

@ -14,42 +14,59 @@ import (
const metricNamespace = "rushlink" const metricNamespace = "rushlink"
var metricRequestsTotalCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ // metricURLsTotalGauge counts the number of requests that are handled by
// the application, partitioned by status code and HTTP method.
//
// This counter is updated by the router.
var metricRequestsTotalCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace, Namespace: metricNamespace,
Subsystem: "http", Subsystem: "http",
Name: "requests_total", Name: "requests_total",
Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
}, []string{"code", "method"}) }, []string{"code", "method"})
func metricURLsTotal(database *db.Database) float64 { // metricRequestsLatencyNanoSeconds keeps track of the request latencies for
var metric float64 // each http request.
if err := database.Transaction(func(tx *db.Database) error { //
var count int64 // This historogram is updated by the router.
if err := database.Model(&db.Paste{}).Count(&count).Error; err != nil { var metricRequestsLatencyNanoSeconds = prometheus.NewHistogramVec(
return err prometheus.HistogramOpts{
} Namespace: metricNamespace,
metric = float64(count) Subsystem: "http",
return nil Name: "request_duration_seconds",
}); err != nil { Buckets: []float64{
log.Printf("error: %v", errors.Wrap(err, "fetching pastes_total metric")) float64(500e-6),
return 0 float64(1e-3),
} float64(2e-3),
return metric float64(5e-3),
} float64(10e-3),
float64(20e-3),
float64(50e-3),
},
Help: "The latency of each HTTP request, partitioned by status code and HTTP method.",
}, []string{"code", "method"})
// metricURLsTotalGauge measures the amount of pastes stored in the database,
// partitioned by type and state.
//
// Its values are computed on the fly by updateMetrics().
var metricURLsTotalGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: metricNamespace,
Subsystem: "pastes",
Name: "urls_total",
Help: "The current amount of pastes in the database, partitioned by state and type.",
}, []string{"state", "type"})
// StartMetricsServer starts sering Prometheus metrics exports on addr // StartMetricsServer starts sering Prometheus metrics exports on addr
func StartMetricsServer(addr string, database *db.Database, fs *db.FileStore) { func StartMetricsServer(addr string, database *db.Database, fs *db.FileStore) {
prometheus.MustRegister(metricRequestsTotalCounter) prometheus.MustRegister(metricRequestsTotalCounter)
prometheus.MustRegister(metricRequestsLatencyNanoSeconds)
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{ prometheus.MustRegister(metricURLsTotalGauge)
Namespace: metricNamespace,
Subsystem: "pastes",
Name: "urls_total",
Help: "The current amount of pastes in the database.",
}, func() float64 { return metricURLsTotal(database) }))
router := mux.NewRouter() router := mux.NewRouter()
router.Handle("/metrics", promhttp.Handler()).Methods("GET") router.Handle("/metrics", &MetricsHandler{database}).Methods("GET")
srv := &http.Server{ srv := &http.Server{
Handler: router, Handler: router,
Addr: addr, Addr: addr,
@ -58,3 +75,34 @@ func StartMetricsServer(addr string, database *db.Database, fs *db.FileStore) {
} }
log.Fatal(srv.ListenAndServe()) log.Fatal(srv.ListenAndServe())
} }
type MetricsHandler struct {
db *db.Database
}
func (mh *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mh.updateMetrics()
promhttp.Handler().ServeHTTP(w, r)
}
func (mh *MetricsHandler) updateMetrics() {
// Update metricURLsTotalGauge
results := make([](struct {
Type db.PasteType
State db.PasteState
Count float64
}), 0)
query := mh.db.Unscoped().Model(&db.Paste{}).Select("type", "state", "COUNT(*) as count").Group("type, state").Find(&results)
if err := query.Error; err != nil {
log.Printf("error: %v", errors.Wrap(err, "fetching pastes_total metric"))
return
}
metricURLsTotalGauge.Reset()
for _, r := range results {
labels := map[string]string{
"type": r.Type.String(),
"state": r.State.String(),
}
metricURLsTotalGauge.With(labels).Set(r.Count)
}
}

View File

@ -58,10 +58,18 @@ func (rl *rushlink) recoveryMiddleware(next http.Handler) http.Handler {
func (rl *rushlink) metricsMiddleware(next http.Handler) http.Handler { func (rl *rushlink) metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tick := time.Now()
srw := statusResponseWriter{Inner: w} srw := statusResponseWriter{Inner: w}
next.ServeHTTP(&srw, r) next.ServeHTTP(&srw, r)
tock := time.Now()
status := strconv.Itoa(srw.StatusCode) status := strconv.Itoa(srw.StatusCode)
metricRequestsTotalCounter.WithLabelValues(status, r.Method).Inc() labels := map[string]string{"code": status, "method": r.Method}
// Update requests counter metric
metricRequestsTotalCounter.With(labels).Inc()
// Update request latency metric
elapsed := tock.Sub(tick)
metricRequestsLatencyNanoSeconds.With(labels).Observe(elapsed.Seconds())
}) })
} }