Skip to content
Merged
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
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/knadh/koanf/providers/env v1.1.0
github.com/knadh/koanf/v2 v2.3.4
github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/v9 v9.18.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.3.4
Expand All @@ -25,6 +26,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down Expand Up @@ -73,11 +75,15 @@ require (
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/shirou/gopsutil/v4 v4.26.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.2 // indirect
Expand All @@ -94,6 +100,7 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
Expand Down
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
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/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
Expand Down Expand Up @@ -117,6 +119,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
Expand Down Expand Up @@ -150,6 +154,8 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
Expand All @@ -161,6 +167,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
Expand Down Expand Up @@ -214,6 +228,10 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
Expand Down
34 changes: 34 additions & 0 deletions internal/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/DanielPopoola/fairqueue/internal/auth"
"github.com/DanielPopoola/fairqueue/internal/metrics"
redisstore "github.com/DanielPopoola/fairqueue/internal/store/redis"
"github.com/go-chi/chi/v5"
)

const otpTTL = 10 * time.Minute
Expand Down Expand Up @@ -107,3 +110,34 @@ func generateOTP() (string, error) {
n := (int(b[0])<<16 | int(b[1])<<8 | int(b[2])) % 1_000_000
return fmt.Sprintf("%06d", n), nil
}

func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

// Wrap ResponseWriter to capture status code
wrapped := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(wrapped, r)

duration := time.Since(start).Seconds()
status := strconv.Itoa(wrapped.status)

routePattern := chi.RouteContext(r.Context()).RoutePattern()
if routePattern == "" {
routePattern = r.URL.Path
}

metrics.HTTPRequestsTotal.WithLabelValues(r.Method, routePattern, status).Inc()
metrics.HTTPRequestDuration.WithLabelValues(r.Method, routePattern).Observe(duration)
})
}

type statusRecorder struct {
http.ResponseWriter
status int
}

func (r *statusRecorder) WriteHeader(status int) {
r.status = status
r.ResponseWriter.WriteHeader(status)
}
6 changes: 6 additions & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/DanielPopoola/fairqueue/internal/auth"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
httpSwagger "github.com/swaggo/http-swagger"
)

Expand All @@ -37,6 +38,10 @@ func NewRouter(
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(MetricsMiddleware)

// Swagger UI — served at /swagger/index.html
r.Get("/swagger/*", httpSwagger.Handler(
Expand All @@ -50,6 +55,7 @@ func NewRouter(
r.Post("/auth/customer/otp/verify", h.VerifyOTP)
r.Get("/events/{eventId}", h.GetEvent)
r.Post("/webhooks/paystack", h.PaystackWebhook)
r.Handle("/metrics", promhttp.Handler())

// ── Organizer-protected routes ────────────────────────────
r.Group(func(r chi.Router) {
Expand Down
54 changes: 54 additions & 0 deletions internal/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package metrics

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
// HTTP metrics
HTTPRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "fairqueue_http_requests_total",
Help: "Total HTTP requests by method, path, and status code",
}, []string{"method", "path", "status"})

HTTPRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "fairqueue_http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "path"})

// Queue metrics
QueueWaitingTotal = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "fairqueue_queue_waiting_total",
Help: "Number of customers currently waiting in queue per event",
}, []string{"event_id"})

QueueAdmittedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "fairqueue_queue_admitted_total",
Help: "Total customers admitted from queue",
}, []string{"event_id"})

// Claim metrics
ClaimsCreatedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "fairqueue_claims_created_total",
Help: "Total claims created",
}, []string{"event_id"})

ClaimsExpiredTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "fairqueue_claims_expired_total",
Help: "Total claims expired without payment",
}, []string{"event_id"})

// Worker metrics
WorkerTickDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "fairqueue_worker_tick_duration_seconds",
Help: "Time taken per worker tick",
Buckets: prometheus.DefBuckets,
}, []string{"worker"})

WorkerTickErrors = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "fairqueue_worker_tick_errors_total",
Help: "Total worker tick errors",
}, []string{"worker"})
)
7 changes: 7 additions & 0 deletions internal/worker/admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"context"
"fmt"
"log/slog"
"time"

"github.com/DanielPopoola/fairqueue/internal/auth"
"github.com/DanielPopoola/fairqueue/internal/config"
"github.com/DanielPopoola/fairqueue/internal/domain"
"github.com/DanielPopoola/fairqueue/internal/metrics"
"github.com/DanielPopoola/fairqueue/internal/service"
postgres "github.com/DanielPopoola/fairqueue/internal/store/postgres"
)
Expand Down Expand Up @@ -53,6 +55,10 @@ func NewAdmissionWorker(
}

func (w *AdmissionWorker) Run(ctx context.Context) error {
start := time.Now()
defer func() {
metrics.WorkerTickDuration.WithLabelValues("admission").Observe(time.Since(start).Seconds())
}()
events, err := w.events.GetByStatus(ctx, domain.EventStatusActive)
if err != nil {
return fmt.Errorf("fetching active events: %w", err)
Expand Down Expand Up @@ -128,6 +134,7 @@ func (w *AdmissionWorker) calculateBatchSize(ctx context.Context, event *domain.
// notifyAdmitted generates a signed admission token for each admitted
// customer and sends it to them.
func (w *AdmissionWorker) notifyAdmitted(ctx context.Context, eventID string, customerIDs []string) error {
metrics.QueueAdmittedTotal.WithLabelValues(eventID).Add(float64(len(customerIDs)))
for _, customerID := range customerIDs {
token, err := w.tokenizer.Generate(customerID, eventID)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/worker/expiry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/DanielPopoola/fairqueue/internal/config"
"github.com/DanielPopoola/fairqueue/internal/domain"
"github.com/DanielPopoola/fairqueue/internal/metrics"
"github.com/DanielPopoola/fairqueue/internal/service"
postgres "github.com/DanielPopoola/fairqueue/internal/store/postgres"
)
Expand Down Expand Up @@ -89,6 +90,7 @@ func (w *ExpiryWorker) releaseClaim(ctx context.Context, claim *domain.Claim) er
return fmt.Errorf("releasing claim: %w", err)
}

metrics.ClaimsExpiredTotal.WithLabelValues(claim.EventID).Inc()
// Postgres committed — now restore inventory in Redis.
// Non-fatal: reconciliation worker heals any divergence.
if err := w.inventory.Increment(ctx, claim.EventID); err != nil {
Expand Down
Loading