package rushlink import ( "log" "net/http" "time" "gitea.hashru.nl/dsprenkels/rushlink/internal/db" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) const metricNamespace = "rushlink" // 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, Subsystem: "http", Name: "requests_total", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", }, []string{"code", "method"}) // metricRequestsLatencyNanoSeconds keeps track of the request latencies for // each http request. // // This historogram is updated by the router. var metricRequestsLatencyNanoSeconds = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: metricNamespace, Subsystem: "http", Name: "request_duration_seconds", Buckets: []float64{ float64(500e-6), float64(1e-3), float64(2e-3), 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 func StartMetricsServer(addr string, database *db.Database, fs *db.FileStore) { prometheus.MustRegister(metricRequestsTotalCounter) prometheus.MustRegister(metricRequestsLatencyNanoSeconds) prometheus.MustRegister(metricURLsTotalGauge) router := mux.NewRouter() router.Handle("/metrics", &MetricsHandler{database}).Methods("GET") srv := &http.Server{ Handler: router, Addr: addr, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } 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) } }