Metrics working
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
*.key
|
||||
bin/
|
||||
data.json
|
||||
*.json
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
20
config.yaml
20
config.yaml
@@ -53,9 +53,9 @@ hosts:
|
||||
|
||||
- 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
131
middleware/metrics.go
Normal file
131
middleware/metrics.go
Normal 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")
|
||||
}
|
||||
45
router.go
45
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user