From a3e06b6314a615c81396fb912b910b0c7814624c Mon Sep 17 00:00:00 2001 From: Andrew Dunstall Date: Wed, 29 May 2024 07:38:48 +0100 Subject: [PATCH] server: add usage reporting Adds usage reporting that periodically reports anonymous usage statistics to report.pikoproxy.com. Each report includes the servers uptime, requests processed, upstreams registered and host OS/architecture. This can be disabled with '--usage.disable'. --- go.mod | 1 + go.sum | 2 + server/config/config.go | 18 +++++++ server/proxy/proxy.go | 24 ++++++++- server/server.go | 14 ++++++ server/usage/reporter.go | 105 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 server/usage/reporter.go diff --git a/go.mod b/go.mod index b42838b..f7a5db8 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/goccy/go-yaml v1.11.3 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.1.2 github.com/gorilla/websocket v1.5.1 github.com/hashicorp/go-sockaddr v1.0.6 github.com/oklog/run v1.1.0 diff --git a/go.sum b/go.sum index 75f2cd1..b52725a 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I= diff --git a/server/config/config.go b/server/config/config.go index 7087cbf..ad3975f 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -61,6 +61,11 @@ func (c *ClusterConfig) Validate() error { return nil } +type UsageConfig struct { + // Disable indicates whether to disable anonymous usage collection. + Disable bool `json:"disable" yaml:"disable"` +} + type Config struct { Proxy ProxyConfig `json:"proxy" yaml:"proxy"` Upstream UpstreamConfig `json:"upstream" yaml:"upstream"` @@ -68,6 +73,7 @@ type Config struct { Gossip gossip.Config `json:"gossip" yaml:"gossip"` Cluster ClusterConfig `json:"cluster" yaml:"cluster"` Auth auth.Config `json:"auth" yaml:"auth"` + Usage UsageConfig `json:"usage" yaml:"usage"` Log log.Config `json:"log" yaml:"log"` // GracePeriod is the duration to gracefully shutdown the server. During @@ -212,6 +218,18 @@ node to join (excluding itself) but fails to join any members.`, c.Gossip.RegisterFlags(fs) + fs.BoolVar( + &c.Usage.Disable, + "usage.disable", + false, + ` +Whether to disable anonymous usage tracking. + +The Piko server periodically sends an anonymous report to help understand how +Piko is being used. This report includes the Piko version, host OS, host +architecture, requests processed and upstreams registered.`, + ) + c.Log.RegisterFlags(fs) fs.DurationVar( diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index b2ad3c8..620304b 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -12,6 +12,7 @@ import ( "github.com/andydunstall/piko/pkg/log" "github.com/andydunstall/piko/server/cluster" + "go.uber.org/atomic" "go.uber.org/zap" ) @@ -19,11 +20,18 @@ var ( errEndpointNotFound = errors.New("not endpoint found") ) +type Usage struct { + Requests *atomic.Uint64 + Upstreams *atomic.Uint64 +} + // Proxy is responsible for forwarding requests to upstream endpoints. type Proxy struct { local *localProxy remote *remoteProxy + usage *Usage + metrics *Metrics logger log.Logger @@ -38,8 +46,12 @@ func NewProxy(clusterState *cluster.State, opts ...Option) *Proxy { metrics := NewMetrics() logger := options.logger.WithSubsystem("proxy") return &Proxy{ - local: newLocalProxy(metrics, logger), - remote: newRemoteProxy(clusterState, options.forwarder, metrics, logger), + local: newLocalProxy(metrics, logger), + remote: newRemoteProxy(clusterState, options.forwarder, metrics, logger), + usage: &Usage{ + Requests: atomic.NewUint64(0), + Upstreams: atomic.NewUint64(0), + }, metrics: metrics, logger: logger, } @@ -65,6 +77,7 @@ func (p *Proxy) Request( zap.String("path", r.URL.Path), zap.Bool("forwarded", forwarded), ) + p.usage.Requests.Inc() endpointID := endpointIDFromRequest(r) if endpointID == "" { @@ -152,6 +165,9 @@ func (p *Proxy) AddConn(conn Conn) { zap.String("endpoint-id", conn.EndpointID()), zap.String("addr", conn.Addr()), ) + + p.usage.Upstreams.Inc() + p.local.AddConn(conn) p.remote.AddConn(conn) } @@ -173,6 +189,10 @@ func (p *Proxy) ConnAddrs() map[string][]string { return p.local.ConnAddrs() } +func (p *Proxy) Usage() *Usage { + return p.usage +} + func (p *Proxy) Metrics() *Metrics { return p.metrics } diff --git a/server/server.go b/server/server.go index 011ae20..b245c0f 100644 --- a/server/server.go +++ b/server/server.go @@ -16,6 +16,7 @@ import ( adminserver "github.com/andydunstall/piko/server/server/admin" proxyserver "github.com/andydunstall/piko/server/server/proxy" upstreamserver "github.com/andydunstall/piko/server/server/upstream" + "github.com/andydunstall/piko/server/usage" "github.com/golang-jwt/jwt/v5" "github.com/hashicorp/go-sockaddr" rungroup "github.com/oklog/run" @@ -221,6 +222,8 @@ func (s *Server) Run(ctx context.Context) error { s.logger, ) + reporter := usage.NewReporter(p, s.logger) + var group rungroup.Group // Termination handler. @@ -335,6 +338,17 @@ func (s *Server) Run(ctx context.Context) error { gossipCancel() }) + if !s.conf.Usage.Disable { + // Usage. + usageCtx, usageCancel := context.WithCancel(ctx) + group.Add(func() error { + reporter.Run(usageCtx) + return nil + }, func(error) { + usageCancel() + }) + } + if err := group.Run(); err != nil { return err } diff --git a/server/usage/reporter.go b/server/usage/reporter.go new file mode 100644 index 0000000..7a5dea5 --- /dev/null +++ b/server/usage/reporter.go @@ -0,0 +1,105 @@ +package usage + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "runtime" + "time" + + "github.com/andydunstall/piko/pkg/log" + "github.com/andydunstall/piko/server/proxy" + "github.com/google/uuid" + "go.uber.org/zap" +) + +const ( + reportInterval = time.Hour +) + +type Report struct { + ID string `json:"id"` + OS string `json:"os"` + Arch string `json:"arch"` + Uptime int64 `json:"uptime"` + Requests uint64 `json:"requests"` + Upstreams uint64 `json:"upstreams"` +} + +// Reporter sends a periodic usage report. +type Reporter struct { + id string + start time.Time + proxy *proxy.Proxy + logger log.Logger +} + +func NewReporter(proxy *proxy.Proxy, logger log.Logger) *Reporter { + return &Reporter{ + id: uuid.New().String(), + start: time.Now(), + proxy: proxy, + logger: logger.WithSubsystem("reporter"), + } +} + +func (r *Reporter) Run(ctx context.Context) { + // Report on startup. + r.report() + + ticker := time.NewTicker(reportInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + // Report on shutdown. + r.report() + return + case <-ticker.C: + // Report on interval. + r.report() + } + } +} + +func (r *Reporter) report() { + report := &Report{ + ID: r.id, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + Uptime: int64(time.Since(r.start).Seconds()), + Requests: r.proxy.Usage().Requests.Load(), + Upstreams: r.proxy.Usage().Upstreams.Load(), + } + if err := r.send(report); err != nil { + // Debug only as theres no user impact. + r.logger.Debug("failed to send usage report", zap.Error(err)) + } +} + +func (r *Reporter) send(report *Report) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + body, err := json.Marshal(report) + if err != nil { + return fmt.Errorf("marshal: %w", err) + } + req, err := http.NewRequestWithContext( + ctx, http.MethodPost, "http://report.pikoproxy.com/v1", bytes.NewBuffer(body), + ) + if err != nil { + return fmt.Errorf("request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("request: %w", err) + } + defer resp.Body.Close() + + return nil +}