feat: add prometheus metrics
This commit is contained in:
parent
b4c331daa0
commit
5c8c6eed63
7 changed files with 110 additions and 9 deletions
|
@ -7,6 +7,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/log"
|
||||
"emperror.dev/errors"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var sq = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
|
||||
|
@ -32,19 +34,24 @@ type DB struct {
|
|||
minio *minio.Client
|
||||
minioBucket string
|
||||
baseURL *url.URL
|
||||
|
||||
TotalRequests prometheus.Counter
|
||||
}
|
||||
|
||||
func New() (*DB, error) {
|
||||
log.Debug("creating postgres client")
|
||||
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating postgres client")
|
||||
}
|
||||
|
||||
log.Debug("creating redis client")
|
||||
redis, err := (&radix.PoolConfig{}).New(context.Background(), "tcp", os.Getenv("REDIS"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating redis client")
|
||||
}
|
||||
|
||||
log.Debug("creating minio client")
|
||||
minioClient, err := minio.New(os.Getenv("MINIO_ENDPOINT"), &minio.Options{
|
||||
Creds: credentials.NewStaticV4(os.Getenv("MINIO_ACCESS_KEY_ID"), os.Getenv("MINIO_ACCESS_KEY_SECRET"), ""),
|
||||
Secure: os.Getenv("MINIO_SSL") == "true",
|
||||
|
@ -67,6 +74,12 @@ func New() (*DB, error) {
|
|||
baseURL: baseURL,
|
||||
}
|
||||
|
||||
log.Debug("initializing metrics")
|
||||
err = db.initMetrics()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "initializing metrics")
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
|
|
63
backend/db/metrics.go
Normal file
63
backend/db/metrics.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/log"
|
||||
"emperror.dev/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
func (db *DB) initMetrics() (err error) {
|
||||
err = prometheus.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "pronouns_users_total",
|
||||
Help: "The total number of registered users",
|
||||
}, func() float64 {
|
||||
count, err := db.TotalUserCount(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("getting user count for metrics: %v", err)
|
||||
}
|
||||
return float64(count)
|
||||
}))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "registering user count gauge")
|
||||
}
|
||||
|
||||
err = prometheus.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "pronouns_members_total",
|
||||
Help: "The total number of registered members",
|
||||
}, func() float64 {
|
||||
count, err := db.TotalMemberCount(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("getting member count for metrics: %v", err)
|
||||
}
|
||||
return float64(count)
|
||||
}))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "registering member count gauge")
|
||||
}
|
||||
|
||||
db.TotalRequests = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "pronouns_api_requests_total",
|
||||
Help: "The total number of API requests since the last restart",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) TotalUserCount(ctx context.Context) (numUsers int64, err error) {
|
||||
err = db.QueryRow(ctx, "SELECT COUNT(*) FROM users WHERE deleted_at IS NULL").Scan(&numUsers)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "querying user count")
|
||||
}
|
||||
return numUsers, nil
|
||||
}
|
||||
|
||||
func (db *DB) TotalMemberCount(ctx context.Context) (numMembers int64, err error) {
|
||||
err = db.QueryRow(ctx, "SELECT COUNT(*) FROM members WHERE unlisted = false AND user_id = ANY(SELECT id FROM users WHERE deleted_at IS NULL)").Scan(&numMembers)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "querying member count")
|
||||
}
|
||||
return numMembers, nil
|
||||
}
|
|
@ -31,13 +31,12 @@ type MetaResponse struct {
|
|||
func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := r.Context()
|
||||
|
||||
var numUsers, numMembers int64
|
||||
err := s.DB.QueryRow(ctx, "SELECT COUNT(*) FROM users WHERE deleted_at IS NULL").Scan(&numUsers)
|
||||
numUsers, err := s.DB.TotalUserCount(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "querying user count")
|
||||
}
|
||||
|
||||
err = s.DB.QueryRow(ctx, "SELECT COUNT(*) FROM members WHERE unlisted = false AND user_id = ANY(SELECT id FROM users WHERE deleted_at IS NULL)").Scan(&numMembers)
|
||||
numMembers, err := s.DB.TotalMemberCount(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "querying user count")
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
@ -23,6 +24,8 @@ func Mount(srv *server.Server, r chi.Router) {
|
|||
r.Patch("/reports/{id}", server.WrapHandler(s.resolveReport))
|
||||
})
|
||||
|
||||
r.With(MustAdmin).Handle("/metrics", promhttp.Handler())
|
||||
|
||||
r.With(server.MustAuth).Post("/users/{id}/reports", server.WrapHandler(s.createUserReport))
|
||||
r.With(server.MustAuth).Post("/members/{id}/reports", server.WrapHandler(s.createMemberReport))
|
||||
|
||||
|
|
|
@ -107,12 +107,17 @@ func New() (*Server, error) {
|
|||
rateLimiter.Scope("*", "/auth/invites", 10)
|
||||
rateLimiter.Scope("POST", "/auth/discord/*", 10)
|
||||
|
||||
// rate limit handling
|
||||
// - 120 req/minute (2/s)
|
||||
// - keyed by Authorization header if valid token is provided, otherwise by IP
|
||||
// - returns rate limit reset info in error
|
||||
s.Router.Use(rateLimiter.Handler())
|
||||
|
||||
// increment the total requests counter whenever a request is made
|
||||
s.Router.Use(func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
s.DB.TotalRequests.Inc()
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
})
|
||||
|
||||
// return an API error for not found + method not allowed
|
||||
s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
render.Status(r, errCodeStatuses[ErrNotFound])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue