Files
domain-router/middleware/metrics.go
2025-10-01 13:58:42 +02:00

132 lines
2.7 KiB
Go

package middleware
import (
"cmp"
"encoding/json"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/rs/zerolog/log"
)
type Metrics struct {
c chan RequestMetric
endpointMetrics []EndpointMetrics
ticker *time.Ticker
file string
}
type EndpointMetrics struct {
Host string
Endpoint string
AbsoluteDuration time.Duration
Calls uint64
}
type RequestMetric struct {
Start time.Time
Stop time.Time
Host string
Method string
Uri string
Status int
Size int
}
func NewMetrics(bufferSize int, flushTimeout time.Duration, file string) *Metrics {
return &Metrics{
c: make(chan RequestMetric, bufferSize),
ticker: time.NewTicker(flushTimeout),
file: file,
}
}
func (m *Metrics) RequestMetrics(next http.Handler) http.Handler {
log.Info().Msg("Enabling Request Metrics")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rm := RequestMetric{
Start: start,
Host: r.Host,
Method: r.Method,
Uri: r.URL.Path,
}
log.Trace().Any("request_metric", rm).Msg("RequestMetric created")
next.ServeHTTP(w, r)
rm.Stop = time.Now()
log.Trace().Any("request_metric", rm).Msg("RequestMetric finished")
m.c <- rm
})
}
func (m *Metrics) Manage() {
for {
select {
case rm := <-m.c:
m.calculateDuration(rm)
case <-m.ticker.C:
m.Flush()
}
}
}
func (m *Metrics) calculateDuration(rm RequestMetric) {
duration := rm.Stop.Sub(rm.Start)
// TODO: Replace this with a hash probably
index := slices.IndexFunc(m.endpointMetrics, func(e EndpointMetrics) bool {
if strings.EqualFold(e.Host, rm.Host) && strings.EqualFold(e.Endpoint, rm.Uri) {
return true
}
return false
})
var in EndpointMetrics
if index >= 0 {
in = m.endpointMetrics[index]
} else {
in = EndpointMetrics{
Host: rm.Host,
Endpoint: rm.Uri,
AbsoluteDuration: time.Duration(0),
Calls: 0,
}
}
in.AbsoluteDuration += duration
in.Calls += 1
if index >= 0 {
m.endpointMetrics[index] = in
} else {
m.endpointMetrics = append(m.endpointMetrics, in)
}
}
func (m *Metrics) Flush() {
file, err := os.Create(m.file)
if err != nil {
log.Error().Err(err).Str("file", m.file).Msg("Could not open file for flushing")
return
}
a := make([]EndpointMetrics, len(m.endpointMetrics))
copy(a, m.endpointMetrics)
slices.SortStableFunc(a, func(e1 EndpointMetrics, e2 EndpointMetrics) int {
return cmp.Compare(e1.Calls, e2.Calls)
})
err = json.NewEncoder(file).Encode(a)
if err != nil {
log.Error().Err(err).Str("file", m.file).Msg("Could not json Encode to file")
return
}
log.Info().Str("file", m.file).Int("count", len(a)).Msg("Completed Metrics flush")
}