Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Prometheus metrics #4138

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ server/src/coverage.txt
build
server/src/main
server/src/server
server/src/tmp

# misc
.DS_Store
Expand Down
3 changes: 3 additions & 0 deletions server/config_example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ disable-check-origin = true

# Specify the URL for the feedback webhook.
feedback-webhook-url = ""

# Enable metrics for the backend server
enable-metrics = true
20 changes: 20 additions & 0 deletions server/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@ services:
- POSTGRES_PASSWORD=supersecret
- POSTGRES_DB=scrumlr
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C

prometheus:
image: prom/prometheus:v2.52.0
ports:
- 9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command: --config.file=/etc/prometheus/prometheus.yml
extra_hosts:
- "host.docker.internal:host-gateway"

grafana:
image: grafana/grafana:10.4.2
ports:
- 3001:3000
environment:
- GF_SECURITY_ADMIN_USER=test
- GF_SECURITY_ADMIN_PASSWORD=test
volumes:
- ./grafana.yml:/etc/grafana/provisioning/datasources/datasources.yml
6 changes: 6 additions & 0 deletions server/grafana.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: 1
datasources:
- name: Main
type: prometheus
url: http://prometheus:9090
isDefault: true
15 changes: 15 additions & 0 deletions server/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
global:
scrape_interval: 15s # By default, scrape targets every 15 seconds.

scrape_configs:
- job_name: 'scrumlr-api'

# Override the global default and scrape targets from this job every 5 seconds.
scrape_interval: 5s
scrape_timeout: 5s
metrics_path: /metrics
scheme: http
follow_redirects: true

static_configs:
- targets: ['host.docker.internal:8080']
79 changes: 79 additions & 0 deletions server/src/api/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package api

import (
"context"
"net/http"
"strconv"
"strings"
"time"

"github.com/go-chi/chi/middleware"
"github.com/prometheus/client_golang/prometheus"
"scrumlr.io/server/logger"
)

var (
endpointLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request latency per endpoint.",
Buckets: []float64{0.1, 0.2, 0.5, 1, 3, 5, 10},
},
[]string{"method", "route", "status"},
)
requestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_request_counter",
Help: "Counter of HTTP requests",
}, []string{"method", "route", "status"},
)
)

func (s *Server) initMetrics() {
log := logger.FromContext(context.Background())
err := s.customMetrics.RegisterCounterVec(requestCounter)
if err != nil {
log.Errorw("unable to register counter for custom metrics", err)
}
err = s.customMetrics.RegisterHistogramVec(endpointLatency)
if err != nil {
log.Errorw("unable to register histogram for custom metrics", err)
}
}

func metricCounterMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
defer func() {
urlPath := generalizeURLPath(r.URL.Path)
requestCounter.WithLabelValues(r.Method, urlPath, strconv.Itoa(ww.Status())).Inc()
}()
next.ServeHTTP(ww, r)

})
}

func metricMeasureLatencyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
defer func() {
duration := time.Since(start).Seconds()
urlPath := generalizeURLPath(r.URL.Path)
endpointLatency.WithLabelValues(r.Method, urlPath, strconv.Itoa(ww.Status())).Observe(duration)
}()
next.ServeHTTP(ww, r)

})
}

// Replaces UUID-like segments with a wildcard placeholder
func generalizeURLPath(path string) string {
segments := strings.Split(path, "/")
for i, segment := range segments {
if len(segment) == 36 && strings.Contains(segment, "-") {
segments[i] = "*"
}
}
return strings.Join(segments, "/")
}
3 changes: 2 additions & 1 deletion server/src/api/notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package api

import (
"fmt"
"net/http"

"github.com/go-chi/render"
"github.com/google/uuid"
"net/http"
"scrumlr.io/server/common"
"scrumlr.io/server/common/dto"
"scrumlr.io/server/identifiers"
Expand Down
24 changes: 22 additions & 2 deletions server/src/api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus/promhttp"

"scrumlr.io/server/auth"
"scrumlr.io/server/logger"
Expand All @@ -36,6 +37,8 @@ type Server struct {
feedback services.Feedback
boardReactions services.BoardReactions
boardTemplates services.BoardTemplates
customMetrics services.CustomMetrics


upgrader websocket.Upgrader

Expand All @@ -61,7 +64,7 @@ func New(
feedback services.Feedback,
boardReactions services.BoardReactions,
boardTemplates services.BoardTemplates,

customMetrics services.CustomMetrics,
verbose bool,
checkOrigin bool,
anonymousLoginDisabled bool,
Expand All @@ -71,6 +74,8 @@ func New(
r.Use(middleware.RequestID)
r.Use(logger.RequestIDMiddleware)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Use(metricCounterMiddleware)
r.Use(metricMeasureLatencyMiddleware)

if !checkOrigin {
r.Use(cors.Handler(cors.Options{
Expand Down Expand Up @@ -105,7 +110,7 @@ func New(
feedback: feedback,
boardReactions: boardReactions,
boardTemplates: boardTemplates,

customMetrics: customMetrics,
anonymousLoginDisabled: anonymousLoginDisabled,
}

Expand All @@ -115,6 +120,11 @@ func New(
WriteBufferSize: 1024,
}

// Registers needed metrics for prometheus
if s.customMetrics != nil {
s.initMetrics()
}

if checkOrigin {
s.upgrader.CheckOrigin = nil
} else {
Expand Down Expand Up @@ -148,6 +158,7 @@ func (s *Server) publicRoutes(r chi.Router) chi.Router {
r.Get("/callback", s.verifyAuthProviderCallback)
})
})
s.initCustomMetricResources(r)
})
}

Expand Down Expand Up @@ -332,3 +343,12 @@ func (s *Server) initBoardReactionResources(r chi.Router) {
r.Post("/", s.createBoardReaction)
})
}

func (s *Server) initCustomMetricResources(r chi.Router) {
if s.customMetrics != nil {
r.Handle("/metrics", promhttp.HandlerFor(
s.customMetrics.Registry(),
promhttp.HandlerOpts{Registry: s.customMetrics.Registry()},
))
}
}
6 changes: 6 additions & 0 deletions server/src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go 1.22
replace github.com/opencontainers/runc v1.1.0 => github.com/opencontainers/runc v1.1.2

require (
github.com/go-chi/chi v1.5.5
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/cors v1.2.1
github.com/go-chi/httprate v0.12.0
Expand All @@ -20,6 +21,7 @@ require (
github.com/nats-io/nats.go v1.36.0
github.com/ory/dockertest/v3 v3.10.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.19.1
github.com/stretchr/testify v1.9.0
github.com/uptrace/bun v1.1.17
github.com/uptrace/bun/dbfixture v1.1.17
Expand All @@ -39,6 +41,7 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/continuity v0.3.0 // indirect
Expand Down Expand Up @@ -83,6 +86,9 @@ require (
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect
Expand Down
12 changes: 12 additions & 0 deletions server/src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
Expand Down Expand Up @@ -52,6 +54,8 @@ github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4Nij
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
Expand Down Expand Up @@ -180,6 +184,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
Expand Down
17 changes: 16 additions & 1 deletion server/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"strings"

"scrumlr.io/server/auth"
"scrumlr.io/server/services"
"scrumlr.io/server/services/custom_metrics"
"scrumlr.io/server/services/health"

"scrumlr.io/server/api"
Expand Down Expand Up @@ -206,6 +208,13 @@ func main() {
Usage: "TOML `filepath` to be loaded ",
Required: false,
},
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "enable-metrics",
Aliases: []string{"m"},
EnvVars: []string{"ENABLE_SCRUMLR_METRICS"},
Usage: "enables metrics and its endpoint",
Required: false,
}),
},
}
app.Before = altsrc.InitInputSourceWithContext(app.Flags, altsrc.NewTomlSourceFromFlagFunc("config"))
Expand Down Expand Up @@ -296,6 +305,12 @@ func run(c *cli.Context) error {
RedirectUri: fmt.Sprintf("%s%s/login/apple/callback", strings.TrimSuffix(c.String("auth-callback-host"), "/"), strings.TrimSuffix(basePath, "/")),
}
}
var customMetricService services.CustomMetrics
if c.IsSet("enable-metrics") {
customMetricService = custom_metrics.NewCustomMetricService(true)
} else {
customMetricService = nil
}

dbConnection := database.New(db, c.Bool("verbose"))

Expand Down Expand Up @@ -329,7 +344,7 @@ func run(c *cli.Context) error {
feedbackService,
boardReactionService,
boardTemplateService,

customMetricService,
c.Bool("verbose"),
!c.Bool("disable-check-origin"),
c.Bool("disable-anonymous-login"),
Expand Down
34 changes: 34 additions & 0 deletions server/src/services/custom_metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package custom_metrics

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"scrumlr.io/server/services"
)

type CustomMetricService struct {
registry prometheus.Registry
}

func NewCustomMetricService(enabled bool) services.CustomMetrics {
if enabled {
m := &CustomMetricService{}
m.registry = *prometheus.NewRegistry()
m.registry.Unregister(collectors.NewGoCollector()) // Unregister the default bloat of metrics
return m
}

return nil
}

func (cms *CustomMetricService) RegisterHistogramVec(histo *prometheus.HistogramVec) error {
return cms.registry.Register(histo)
}

func (cms *CustomMetricService) RegisterCounterVec(ctr *prometheus.CounterVec) error {
return cms.registry.Register(ctr)
}

func (cms *CustomMetricService) Registry() *prometheus.Registry {
return &cms.registry
}
7 changes: 7 additions & 0 deletions server/src/services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"scrumlr.io/server/common/dto"
"scrumlr.io/server/common/filter"
)
Expand Down Expand Up @@ -114,3 +115,9 @@ type BoardTemplates interface {
UpdateColumnTemplate(ctx context.Context, body dto.ColumnTemplateUpdateRequest) (*dto.ColumnTemplate, error)
DeleteColumnTemplate(ctx context.Context, boar, column, user uuid.UUID) error
}

type CustomMetrics interface {
RegisterHistogramVec(hist *prometheus.HistogramVec) error
RegisterCounterVec(ctr *prometheus.CounterVec) error
Registry() *prometheus.Registry
}