Metrics working
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
*.key
|
*.key
|
||||||
bin/
|
bin/
|
||||||
data.json
|
data.json
|
||||||
|
*.json
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type Config struct {
|
|||||||
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"`
|
||||||
|
|||||||
20
config.yaml
20
config.yaml
@@ -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
|
|
||||||
|
|||||||
@@ -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
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")
|
||||||
|
}
|
||||||
37
router.go
37
router.go
@@ -28,13 +28,23 @@ type Host struct {
|
|||||||
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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user