From 66f2811fff508c50e14792138d2d989732dd54d1 Mon Sep 17 00:00:00 2001 From: Pablu23 Date: Wed, 1 Oct 2025 13:58:42 +0200 Subject: [PATCH] Metrics working --- .gitignore | 3 +- cmd/domain-router/main.go | 4 ++ config.go | 9 +-- config.yaml | 22 ++----- middleware/logging.go | 1 + middleware/metrics.go | 131 ++++++++++++++++++++++++++++++++++++++ router.go | 45 +++++++++++-- 7 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 middleware/metrics.go diff --git a/.gitignore b/.gitignore index aff2769..5656683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.crt *.key bin/ -data.json \ No newline at end of file +data.json +*.json diff --git a/cmd/domain-router/main.go b/cmd/domain-router/main.go index ee2c173..8cdc6b1 100644 --- a/cmd/domain-router/main.go +++ b/cmd/domain-router/main.go @@ -101,6 +101,10 @@ func configureMiddleware(config *domainrouter.Config) middleware.Middleware { middlewares = append(middlewares, middleware.RequestLogger) } + metrics := middleware.NewMetrics(512, 1*time.Minute, "tmp_metrics.json") + go metrics.Manage() + middlewares = append(middlewares, metrics.RequestMetrics) + pipeline := middleware.Pipeline(middlewares...) return pipeline } diff --git a/config.go b/config.go index 71ea914..3a4a1df 100644 --- a/config.go +++ b/config.go @@ -19,10 +19,11 @@ type Config struct { } `yaml:"ssl"` } `yaml:"server"` Hosts []struct { - Port int `yaml:"port"` - Remotes []string `yaml:"remotes"` - Domains []string `yaml:"domains"` - Secure bool `yaml:"secure"` + Port int `yaml:"port"` + Remotes []string `yaml:"remotes"` + Domains []string `yaml:"domains"` + Secure bool `yaml:"secure"` + Rewrite map[string]string `yaml:"rewrite"` } `yaml:"hosts"` RateLimit struct { Enabled bool `yaml:"enabled"` diff --git a/config.yaml b/config.yaml index 560fb58..8966442 100644 --- a/config.yaml +++ b/config.yaml @@ -51,11 +51,11 @@ hosts: - localhost - test.localhost - - remotes: + - remotes: - localhost - port: 8282 + port: 8080 domains: - - private.localhost + - api.hitstar.localhost - remotes: - localhost @@ -63,23 +63,11 @@ hosts: domains: - hitstar.localhost - hipstar.localhost + rewrite: + "/api": api.hitstar.localhost - remotes: - 127.0.0.1 port: 46009 domains: - chat.localhost - - - remotes: - - localhost - port: 8080 - domains: - - gorilla.localhost - - - remotes: - - www.google.com - port: 443 - # Uses https under the hood to communicate with the remote host - secure: true - domains: - - google.localhost diff --git a/middleware/logging.go b/middleware/logging.go index 8ead220..b580866 100644 --- a/middleware/logging.go +++ b/middleware/logging.go @@ -22,6 +22,7 @@ func RequestLogger(next http.Handler) http.Handler { Str("uri", r.RequestURI). Str("method", r.Method). Str("uuid", uuid). + Str("remote_address", r.RemoteAddr). Msg("Received Request") next.ServeHTTP(lrw, r) diff --git a/middleware/metrics.go b/middleware/metrics.go new file mode 100644 index 0000000..cf13a08 --- /dev/null +++ b/middleware/metrics.go @@ -0,0 +1,131 @@ +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") +} diff --git a/router.go b/router.go index 8e156bb..6a0e422 100644 --- a/router.go +++ b/router.go @@ -24,17 +24,27 @@ type Router struct { } type Host struct { - Port int - Remotes []string - Secure bool - Current *atomic.Uint32 + Port int + Remotes []string + Secure bool + Current *atomic.Uint32 + Rewrites map[string]*Host } func New(config *Config, client *http.Client) Router { m := make(map[string]Host) for _, host := range config.Hosts { for _, domain := range host.Domains { - m[domain] = Host{host.Port, host.Remotes, host.Secure, &atomic.Uint32{}} + curr := Host{host.Port, host.Remotes, host.Secure, &atomic.Uint32{}, make(map[string]*Host)} + m[domain] = curr + + for subUrl, rewriteHost := range host.Rewrite { + rewrite, ok := m[rewriteHost] + if !ok { + panic("WIP: Rewrite location has to be defined before rewrite") + } + curr.Rewrites[subUrl] = &rewrite + } } } @@ -87,6 +97,31 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + for subUrl, rewriteHost := range host.Rewrites { + parts := strings.Split(subUrl, "/") + requestParts := strings.Split(r.URL.Path, "/") + + for i, part := range parts { + if !strings.EqualFold(part, requestParts[i]) { + break + } + } + + slicedPath := "/" + strings.Join(requestParts[len(parts):], "/") + + log.Info(). + Str("old_host", strings.Join(host.Remotes, ", ")). + Str("new_host", strings.Join(rewriteHost.Remotes, ", ")). + Str("sub_url", subUrl). + Str("requested_path", r.URL.Path). + Str("new_path", slicedPath). + Msg("Rewriting matched url path to different remote") + + r.URL.Path = slicedPath + host = *rewriteHost + break + } + remote := host.Remotes[host.Current.Load()] go router.roundRobin(&host)