Metrics working

This commit is contained in:
Pablu23
2025-10-01 13:58:42 +02:00
parent 018d9a9022
commit 66f2811fff
7 changed files with 188 additions and 27 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
*.key *.key
bin/ bin/
data.json data.json
*.json

View File

@@ -101,6 +101,10 @@ func configureMiddleware(config *domainrouter.Config) middleware.Middleware {
middlewares = append(middlewares, middleware.RequestLogger) 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...) pipeline := middleware.Pipeline(middlewares...)
return pipeline return pipeline
} }

View File

@@ -19,10 +19,11 @@ type Config struct {
} `yaml:"ssl"` } `yaml:"ssl"`
} `yaml:"server"` } `yaml:"server"`
Hosts []struct { Hosts []struct {
Port int `yaml:"port"` Port int `yaml:"port"`
Remotes []string `yaml:"remotes"` Remotes []string `yaml:"remotes"`
Domains []string `yaml:"domains"` Domains []string `yaml:"domains"`
Secure bool `yaml:"secure"` Secure bool `yaml:"secure"`
Rewrite map[string]string `yaml:"rewrite"`
} `yaml:"hosts"` } `yaml:"hosts"`
RateLimit struct { RateLimit struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`

View File

@@ -53,9 +53,9 @@ hosts:
- remotes: - remotes:
- localhost - localhost
port: 8282 port: 8080
domains: domains:
- private.localhost - api.hitstar.localhost
- remotes: - remotes:
- localhost - localhost
@@ -63,23 +63,11 @@ hosts:
domains: domains:
- hitstar.localhost - hitstar.localhost
- hipstar.localhost - hipstar.localhost
rewrite:
"/api": api.hitstar.localhost
- remotes: - remotes:
- 127.0.0.1 - 127.0.0.1
port: 46009 port: 46009
domains: domains:
- chat.localhost - 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

View File

@@ -22,6 +22,7 @@ func RequestLogger(next http.Handler) http.Handler {
Str("uri", r.RequestURI). Str("uri", r.RequestURI).
Str("method", r.Method). Str("method", r.Method).
Str("uuid", uuid). Str("uuid", uuid).
Str("remote_address", r.RemoteAddr).
Msg("Received Request") Msg("Received Request")
next.ServeHTTP(lrw, r) next.ServeHTTP(lrw, r)

131
middleware/metrics.go Normal file
View File

@@ -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")
}

View File

@@ -24,17 +24,27 @@ type Router struct {
} }
type Host struct { type Host struct {
Port int Port int
Remotes []string Remotes []string
Secure bool Secure bool
Current *atomic.Uint32 Current *atomic.Uint32
Rewrites map[string]*Host
} }
func New(config *Config, client *http.Client) Router { func New(config *Config, client *http.Client) Router {
m := make(map[string]Host) m := make(map[string]Host)
for _, host := range config.Hosts { for _, host := range config.Hosts {
for _, domain := range host.Domains { 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 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()] remote := host.Remotes[host.Current.Load()]
go router.roundRobin(&host) go router.roundRobin(&host)