From 97997fffef44acccee3ec0161bbeb72f5411acfd Mon Sep 17 00:00:00 2001 From: Yasir Folio3 <39988750+yasirfolio3@users.noreply.github.com> Date: Tue, 6 Jun 2023 10:03:25 -0400 Subject: [PATCH 1/6] [FSSDK-8707] feat(metrics): Adds support for prometheus (#348) * Added support for prometheus. * Added unit tests. * Added unit tests. * updated headers. * fixes. * cleanup. * Adding support for labels in prometheus and expvar. * Refactor code * fix readme * Fix copyright year --------- Co-authored-by: Mirza Sohail Hussain Co-authored-by: Pulak Bhowmick --- cmd/optimizely/main.go | 3 +- cmd/optimizely/main_test.go | 3 + cmd/optimizely/testdata/default.yaml | 1 + config.yaml | 4 + config/config.go | 8 +- config/config_test.go | 1 + go.mod | 11 ++ go.sum | 89 ++++++++++- pkg/handlers/admin_entities.go | 7 +- pkg/metrics/metrics.go | 161 +++++++++++++------ pkg/metrics/metrics_prometheus_test.go | 204 +++++++++++++++++++++++++ pkg/metrics/metrics_test.go | 45 ++++-- pkg/middleware/metrics_test.go | 2 +- pkg/optimizely/cache_test.go | 4 +- pkg/optimizely/metrics_test.go | 4 +- pkg/routers/api_test.go | 2 +- 16 files changed, 478 insertions(+), 71 deletions(-) create mode 100644 pkg/metrics/metrics_prometheus_test.go diff --git a/cmd/optimizely/main.go b/cmd/optimizely/main.go index 7d5958e5..7a6d97b9 100644 --- a/cmd/optimizely/main.go +++ b/cmd/optimizely/main.go @@ -137,7 +137,8 @@ func main() { setRuntimeEnvironment(conf.Runtime) - agentMetricsRegistry := metrics.NewRegistry() + // Set metrics type to be used + agentMetricsRegistry := metrics.NewRegistry(conf.Admin.MetricsType) sdkMetricsRegistry := optimizely.NewRegistry(agentMetricsRegistry) ctx, cancel := context.WithCancel(context.Background()) // Create default service context diff --git a/cmd/optimizely/main_test.go b/cmd/optimizely/main_test.go index 6d7f9828..d99a47b0 100644 --- a/cmd/optimizely/main_test.go +++ b/cmd/optimizely/main_test.go @@ -98,6 +98,7 @@ func assertLog(t *testing.T, actual config.LogConfig) { func assertAdmin(t *testing.T, actual config.AdminConfig) { assert.Equal(t, "3002", actual.Port) + assert.Equal(t, "prometheus", actual.MetricsType) } func assertAdminAuth(t *testing.T, actual config.ServiceAuthConfig) { @@ -232,6 +233,7 @@ func TestViperProps(t *testing.T) { v.Set("log.level", "debug") v.Set("admin.port", "3002") + v.Set("admin.metricsType", "prometheus") v.Set("admin.auth.ttl", "30m") v.Set("admin.auth.hmacSecrets", "efgh,ijkl") v.Set("admin.auth.jwksURL", "admin_jwks_url") @@ -319,6 +321,7 @@ func TestViperEnv(t *testing.T) { _ = os.Setenv("OPTIMIZELY_LOG_LEVEL", "debug") _ = os.Setenv("OPTIMIZELY_ADMIN_PORT", "3002") + _ = os.Setenv("OPTIMIZELY_ADMIN_METRICSTYPE", "prometheus") _ = os.Setenv("OPTIMIZELY_API_MAXCONNS", "100") _ = os.Setenv("OPTIMIZELY_API_PORT", "3000") diff --git a/cmd/optimizely/testdata/default.yaml b/cmd/optimizely/testdata/default.yaml index 8abb6ad6..972189de 100644 --- a/cmd/optimizely/testdata/default.yaml +++ b/cmd/optimizely/testdata/default.yaml @@ -52,6 +52,7 @@ client: admin: port: "3002" + metricsType: "prometheus" auth: ttl: 30m hmacSecrets: diff --git a/config.yaml b/config.yaml index 6943496f..4d1f4b95 100644 --- a/config.yaml +++ b/config.yaml @@ -95,6 +95,10 @@ api: admin: ## http listener port port: "8088" + ## metrics package to use + ## supported packages are expvar and prometheus + ## default is expvar + metricsType: "" ## ## webhook service receives update notifications to your Optimizely project. Receipt of the webhook will ## trigger an immediate download of the datafile from the CDN diff --git a/config/config.go b/config/config.go index ae2a49df..6f84ca04 100644 --- a/config/config.go +++ b/config/config.go @@ -39,7 +39,8 @@ func NewDefaultConfig() *AgentConfig { JwksURL: "", JwksUpdateInterval: 0, }, - Port: "8088", + Port: "8088", + MetricsType: "expvar", }, API: APIConfig{ Auth: ServiceAuthConfig{ @@ -227,8 +228,9 @@ type CORSConfig struct { // AdminConfig holds the configuration for the admin web interface type AdminConfig struct { - Auth ServiceAuthConfig `json:"-"` - Port string `json:"port"` + Auth ServiceAuthConfig `json:"-"` + Port string `json:"port"` + MetricsType string `json:"metricsType"` } // WebhookConfig holds configuration for Optimizely Webhooks diff --git a/config/config_test.go b/config/config_test.go index 4cc52bd2..464991d5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -50,6 +50,7 @@ func TestDefaultConfig(t *testing.T) { assert.Equal(t, "info", conf.Log.Level) assert.Equal(t, "8088", conf.Admin.Port) + assert.Equal(t, "expvar", conf.Admin.MetricsType) assert.Equal(t, make([]OAuthClientCredentials, 0), conf.Admin.Auth.Clients) assert.Equal(t, make([]string, 0), conf.Admin.Auth.HMACSecrets) assert.Equal(t, time.Duration(0), conf.Admin.Auth.TTL) diff --git a/go.mod b/go.mod index 57fff9f0..811b42a9 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/lestrrat-go/jwx v0.9.0 github.com/optimizely/go-sdk v1.8.4-0.20230308225321-5a0d450ca834 github.com/orcaman/concurrent-map v1.0.0 + github.com/prometheus/client_golang v1.11.0 github.com/rakyll/statik v0.1.7 github.com/rs/zerolog v1.29.0 github.com/spf13/viper v1.15.0 @@ -23,6 +24,16 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.30.0 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + google.golang.org/protobuf v1.28.1 // indirect +) + require ( github.com/VividCortex/gohistogram v1.0.0 // indirect github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index 4bc5c6a4..e90cd79c 100644 --- a/go.sum +++ b/go.sum @@ -42,7 +42,17 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -81,11 +91,19 @@ github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIo github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -113,6 +131,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -124,6 +145,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -159,12 +181,21 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -181,13 +212,19 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= @@ -197,6 +234,7 @@ github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HD github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -204,7 +242,27 @@ github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug= +github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -214,6 +272,9 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs= github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= @@ -225,6 +286,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -253,6 +315,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -297,6 +360,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -304,6 +368,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -326,6 +391,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -336,6 +402,7 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -349,9 +416,12 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -360,6 +430,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -372,6 +443,8 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -379,8 +452,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -396,6 +472,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -540,14 +617,24 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/handlers/admin_entities.go b/pkg/handlers/admin_entities.go index 9b555fd5..1c6e9abc 100644 --- a/pkg/handlers/admin_entities.go +++ b/pkg/handlers/admin_entities.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2020, Optimizely, Inc. and contributors * + * Copyright 2019-2020,2023 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -18,14 +18,13 @@ package handlers import ( - "expvar" "net/http" "os" "time" "github.com/go-chi/render" - "github.com/optimizely/agent/config" + "github.com/optimizely/agent/pkg/metrics" ) var startTime = time.Now() @@ -87,5 +86,5 @@ func (a Admin) AppInfoHeader(next http.Handler) http.Handler { // Metrics returns expvar info func (a Admin) Metrics(w http.ResponseWriter, r *http.Request) { - expvar.Handler().ServeHTTP(w, r) + metrics.GetHandler(a.Config.Admin.MetricsType).ServeHTTP(w, r) } diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index cd8379ce..09c15ea3 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -18,11 +18,17 @@ package metrics import ( + "expvar" + "net/http" + "regexp" + "strings" "sync" go_kit_metrics "github.com/go-kit/kit/metrics" - go_kit_expvar "github.com/go-kit/kit/metrics/expvar" + go_kit_prometheus "github.com/go-kit/kit/metrics/prometheus" + stdprometheus "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog/log" ) @@ -33,12 +39,42 @@ const ( TimerPrefix = "timer" ) +const ( + prometheusPackage = "prometheus" +) + +// GetHandler returns request handler for provided metrics package type +func GetHandler(packageType string) http.Handler { + switch packageType { + case prometheusPackage: + return promhttp.Handler() + default: + // expvar + return expvar.Handler() + } +} + +// Timer is the collection of some timers +type Timer struct { + hits go_kit_metrics.Counter + totalTime go_kit_metrics.Counter + histogram go_kit_metrics.Histogram +} + +// Update timer components +func (t *Timer) Update(delta float64) { + t.hits.Add(1) + t.totalTime.Add(delta) + t.histogram.Observe(delta) +} + // Registry initializes expvar metrics registry type Registry struct { metricsCounterVars map[string]go_kit_metrics.Counter metricsGaugeVars map[string]go_kit_metrics.Gauge metricsHistogramVars map[string]go_kit_metrics.Histogram metricsTimerVars map[string]*Timer + metricsType string gaugeLock sync.RWMutex counterLock sync.RWMutex @@ -46,24 +82,6 @@ type Registry struct { timerLock sync.RWMutex } -// NewRegistry initializes metrics registry -func NewRegistry() *Registry { - - return &Registry{ - metricsCounterVars: map[string]go_kit_metrics.Counter{}, - metricsGaugeVars: map[string]go_kit_metrics.Gauge{}, - metricsHistogramVars: map[string]go_kit_metrics.Histogram{}, - metricsTimerVars: map[string]*Timer{}, - } -} - -// Timer is the collection of some timers -type Timer struct { - hits go_kit_metrics.Counter - totalTime go_kit_metrics.Counter - histogram go_kit_metrics.Histogram -} - // NewTimer constructs Timer func (m *Registry) NewTimer(key string) *Timer { if key == "" { @@ -81,16 +99,22 @@ func (m *Registry) NewTimer(key string) *Timer { return m.createTimer(combinedKey) } -// Update timer components -func (t *Timer) Update(delta float64) { - t.hits.Add(1) - t.totalTime.Add(delta) - t.histogram.Observe(delta) +// GetCounter gets go-kit expvar Counter +// NewRegistry initializes metrics registry +func NewRegistry(metricsType string) *Registry { + + registry := &Registry{ + metricsCounterVars: map[string]go_kit_metrics.Counter{}, + metricsGaugeVars: map[string]go_kit_metrics.Gauge{}, + metricsHistogramVars: map[string]go_kit_metrics.Histogram{}, + metricsTimerVars: map[string]*Timer{}, + metricsType: metricsType, + } + return registry } -// GetCounter gets go-kit expvar Counter +// GetCounter gets go-kit Counter func (m *Registry) GetCounter(key string) go_kit_metrics.Counter { - if key == "" { log.Warn().Msg("metrics counter key is empty") return nil @@ -103,13 +127,11 @@ func (m *Registry) GetCounter(key string) go_kit_metrics.Counter { if val, ok := m.metricsCounterVars[combinedKey]; ok { return val } - return m.createCounter(combinedKey) } -// GetGauge gets go-kit expvar Gauge +// GetGauge gets go-kit Gauge func (m *Registry) GetGauge(key string) go_kit_metrics.Gauge { - if key == "" { log.Warn().Msg("metrics gauge key is empty") return nil @@ -127,9 +149,8 @@ func (m *Registry) GetGauge(key string) go_kit_metrics.Gauge { // GetHistogram gets go-kit Histogram func (m *Registry) GetHistogram(key string) go_kit_metrics.Histogram { - if key == "" { - log.Warn().Msg("metrics gauge key is empty") + log.Warn().Msg("metrics histogram key is empty") return nil } @@ -141,25 +162,52 @@ func (m *Registry) GetHistogram(key string) go_kit_metrics.Histogram { return m.createHistogram(key) } -func (m *Registry) createGauge(key string) *go_kit_expvar.Gauge { - gaugeVar := go_kit_expvar.NewGauge(key) +func (m *Registry) createGauge(key string) (gaugeVar go_kit_metrics.Gauge) { + // This is required since naming convention for every package differs + name := m.getPackageSupportedName(key) + switch m.metricsType { + case prometheusPackage: + gaugeVar = go_kit_prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Name: name, + }, []string{}) + default: + // Default expvar + gaugeVar = go_kit_expvar.NewGauge(name) + } m.metricsGaugeVars[key] = gaugeVar - return gaugeVar - + return } -func (m *Registry) createCounter(key string) *go_kit_expvar.Counter { - counterVar := go_kit_expvar.NewCounter(key) +func (m *Registry) createCounter(key string) (counterVar go_kit_metrics.Counter) { + // This is required since naming convention for every package differs + name := m.getPackageSupportedName(key) + switch m.metricsType { + case prometheusPackage: + counterVar = go_kit_prometheus.NewCounterFrom(stdprometheus.CounterOpts{ + Name: name, + }, []string{}) + default: + // Default expvar + counterVar = go_kit_expvar.NewCounter(name) + } m.metricsCounterVars[key] = counterVar - return counterVar - + return } -func (m *Registry) createHistogram(key string) *go_kit_expvar.Histogram { - histogramVar := go_kit_expvar.NewHistogram(key, 50) +func (m *Registry) createHistogram(key string) (histogramVar go_kit_metrics.Histogram) { + // This is required since naming convention for every package differs + name := m.getPackageSupportedName(key) + switch m.metricsType { + case prometheusPackage: + histogramVar = go_kit_prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{ + Name: name, + }, []string{}) + default: + // Default expvar + histogramVar = go_kit_expvar.NewHistogram(name, 50) + } m.metricsHistogramVars[key] = histogramVar - return histogramVar - + return } func (m *Registry) createTimer(key string) *Timer { @@ -168,8 +216,33 @@ func (m *Registry) createTimer(key string) *Timer { totalTime: m.createCounter(key + ".responseTime"), histogram: m.createHistogram(key + ".responseTimeHist"), } - m.metricsTimerVars[key] = timerVar return timerVar +} +// getPackageSupportedName converts name to package supported type +func (m *Registry) getPackageSupportedName(name string) string { + switch m.metricsType { + case prometheusPackage: + // https://prometheus.io/docs/practices/naming/ + return toSnakeCase(name) + default: + // Default expvar + return name + } +} + +func toSnakeCase(name string) string { + v := strings.Replace(name, "-", "_", -1) + strArray := strings.Split(v, ".") + var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") + var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + convertedArray := []string{} + + for _, v := range strArray { + snake := matchFirstCap.ReplaceAllString(v, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + convertedArray = append(convertedArray, strings.ToLower(snake)) + } + return strings.Join(convertedArray, "_") } diff --git a/pkg/metrics/metrics_prometheus_test.go b/pkg/metrics/metrics_prometheus_test.go new file mode 100644 index 00000000..a8a4cc10 --- /dev/null +++ b/pkg/metrics/metrics_prometheus_test.go @@ -0,0 +1,204 @@ +/**************************************************************************** + * Copyright 2023, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package optimizely // +package metrics + +import ( + "expvar" + "io/ioutil" + "net/http/httptest" + "strings" + "testing" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stretchr/testify/assert" +) + +func TestPrometheusCounterValid(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + counter := metricsRegistry.GetCounter("metrics") + counter.Add(12) + counter.Add(23) + + expvar.Handler().ServeHTTP(rec, req) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "counter_metrics 35") +} + +func TestPrometheusCounterMultipleRetrievals(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + counterKey := "next_counter_metrics" + counter := metricsRegistry.GetCounter(counterKey) + counter.Add(12) + + nextCounter := metricsRegistry.GetCounter(counterKey) + nextCounter.Add(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "counter_"+counterKey+"35") +} + +func TestPrometheusCounterEmptyKey(t *testing.T) { + + metricsRegistry := NewRegistry(prometheusPackage) + counter := metricsRegistry.GetCounter("") + assert.Nil(t, counter) +} + +func TestPrometheusGaugeValid(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + gauge := metricsRegistry.GetGauge("metrics") + gauge.Set(12) + gauge.Set(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "gauge_metrics") +} + +func TestPrometheusGaugeMultipleRetrievals(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + guageKey := "next_gauge_metrics" + gauge := metricsRegistry.GetGauge(guageKey) + gauge.Set(12) + nextGauge := metricsRegistry.GetGauge(guageKey) + nextGauge.Set(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "gauge_"+guageKey+" 23") +} + +func TestPrometheusGaugeEmptyKey(t *testing.T) { + + metricsRegistry := NewRegistry(prometheusPackage) + gauge := metricsRegistry.GetGauge("") + assert.Nil(t, gauge) +} + +func TestPrometheusHistogramValid(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + histogram := metricsRegistry.GetHistogram("metrics") + histogram.Observe(12) + histogram.Observe(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "timer_metrics_response_time_hist_sum 35") + strings.Contains(strResponse, "timer_metrics_response_time_hist_count 2") +} + +func TestPrometheusHistogramMultipleRetrievals(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + histogramKey := "next_histogram_metrics" + histogram := metricsRegistry.GetHistogram(histogramKey) + histogram.Observe(12) + nextGauge := metricsRegistry.GetHistogram(histogramKey) + nextGauge.Observe(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "timer_metrics_response_time_hist_sum 35") + strings.Contains(strResponse, "timer_metrics_response_time_hist_count 2") +} + +func TestPrometheusHistogramEmptyKey(t *testing.T) { + + metricsRegistry := NewRegistry(prometheusPackage) + histogram := metricsRegistry.GetHistogram("") + + assert.Nil(t, histogram) +} + +func TestPrometheusTimerValid(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + timer := metricsRegistry.NewTimer("metrics") + timer.Update(12) + timer.Update(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "timer_metrics_response_time_hist_sum 35") + strings.Contains(strResponse, "timer_metrics_response_time_hist_count 2") + strings.Contains(strResponse, `timer_metrics_response_time_hist_bucket{le="+Inf"} 2`) +} + +func TestPrometheusTimerMultipleRetrievals(t *testing.T) { + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + + metricsRegistry := NewRegistry(prometheusPackage) + timerKey := "next_timer_metrics" + timer := metricsRegistry.NewTimer(timerKey) + timer.Update(12) + nextTimer := metricsRegistry.NewTimer(timerKey) + nextTimer.Update(23) + + promhttp.Handler().ServeHTTP(rec, req) + resp, err := ioutil.ReadAll(rec.Body) + assert.Nil(t, err) + strResponse := string(resp) + strings.Contains(strResponse, "timer_metrics_response_time_hist_sum 35") + strings.Contains(strResponse, "timer_metrics_response_time_hist_count 2") + strings.Contains(strResponse, `timer_metrics_response_time_hist_bucket{le="+Inf"} 2`) +} diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 9b108798..1eadda22 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * + * Copyright 2019,2023 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -28,12 +28,18 @@ import ( type JSON map[string]interface{} +func TestGetHandler(t *testing.T) { + assert.NotNil(t, GetHandler("")) + assert.NotNil(t, GetHandler("expvar")) + assert.NotNil(t, GetHandler("123131231")) +} + func TestCounterValid(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") counter := metricsRegistry.GetCounter("metrics") counter.Add(12) counter.Add(23) @@ -52,7 +58,7 @@ func TestCounterMultipleRetrievals(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") counterKey := "next_counter_metrics" counter := metricsRegistry.GetCounter(counterKey) counter.Add(12) @@ -70,7 +76,7 @@ func TestCounterMultipleRetrievals(t *testing.T) { func TestCounterEmptyKey(t *testing.T) { - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") counter := metricsRegistry.GetCounter("") assert.Nil(t, counter) @@ -82,7 +88,7 @@ func TestGaugeValid(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") gauge := metricsRegistry.GetGauge("metrics") gauge.Set(12) gauge.Set(23) @@ -101,7 +107,7 @@ func TestGaugeMultipleRetrievals(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") guageKey := "next_gauge_metrics" gauge := metricsRegistry.GetGauge(guageKey) gauge.Set(12) @@ -119,7 +125,7 @@ func TestGaugeMultipleRetrievals(t *testing.T) { func TestGaugeEmptyKey(t *testing.T) { - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") gauge := metricsRegistry.GetGauge("") assert.Nil(t, gauge) @@ -131,7 +137,7 @@ func TestHistorgramValid(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") histogram := metricsRegistry.GetHistogram("metrics") histogram.Observe(12) histogram.Observe(23) @@ -151,7 +157,7 @@ func TestHistogramMultipleRetrievals(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") histogramKey := "next_histogram_metrics" histogram := metricsRegistry.GetHistogram(histogramKey) histogram.Observe(12) @@ -170,7 +176,7 @@ func TestHistogramMultipleRetrievals(t *testing.T) { func TestHistogramEmptyKey(t *testing.T) { - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") histogram := metricsRegistry.GetHistogram("") assert.Nil(t, histogram) @@ -181,7 +187,7 @@ func TestTimerValid(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") timer := metricsRegistry.NewTimer("metrics") timer.Update(12) timer.Update(23) @@ -202,7 +208,7 @@ func TestTimerMultipleRetrievals(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := NewRegistry() + metricsRegistry := NewRegistry("") timerKey := "next_timer_metrics" timer := metricsRegistry.NewTimer(timerKey) timer.Update(12) @@ -219,3 +225,18 @@ func TestTimerMultipleRetrievals(t *testing.T) { assert.Equal(t, 23.0, expVarMap["timer.next_timer_metrics.responseTimeHist.p99"]) } + +func TestToSnakeCase(t *testing.T) { + assert.Equal(t, "", toSnakeCase("")) + assert.Equal(t, "abc", toSnakeCase("abc")) + assert.Equal(t, "abc_123", toSnakeCase("abc_123")) + assert.Equal(t, "abc_efg", toSnakeCase("abcEfg")) + assert.Equal(t, "timer_activate_response_time", toSnakeCase("timer.activate.responseTime")) + assert.Equal(t, "timer_activate_response_time_hist_p95", toSnakeCase("timer.activate.responseTimeHist.p95")) + assert.Equal(t, "timer_create_api_access_token_response_time_hist_p50", toSnakeCase("timer.create-api-access-token.responseTimeHist.p50")) + assert.Equal(t, "timer_get_config_response_time_hist_p50", toSnakeCase("timer.get-config.responseTimeHist.p50")) + assert.Equal(t, "timer_track_event_response_time_hist_p50", toSnakeCase("timer.track-event.responseTimeHist.p50")) + assert.Equal(t, "counter_dispatcher_success_flush", toSnakeCase("counter.dispatcher.successFlush")) + assert.Equal(t, "timer_get_config_response_time", toSnakeCase("timer.get-config.responseTime")) + assert.Equal(t, "timer_get_config_hits", toSnakeCase("timer.get-config.hits")) +} diff --git a/pkg/middleware/metrics_test.go b/pkg/middleware/metrics_test.go index 6148a4e8..5d35a3e2 100644 --- a/pkg/middleware/metrics_test.go +++ b/pkg/middleware/metrics_test.go @@ -53,7 +53,7 @@ func (rm *RequestMetrics) SetupRoute(key string) { r := httptest.NewRequest("GET", "/", nil) rm.req = r.WithContext(context.WithValue(r.Context(), responseTime, time.Now())) - rm.handler = http.Handler(Metricize(key, metrics.NewRegistry())(getTestMetrics())) + rm.handler = http.Handler(Metricize(key, metrics.NewRegistry(""))(getTestMetrics())) } diff --git a/pkg/optimizely/cache_test.go b/pkg/optimizely/cache_test.go index 398d644c..be7af2e1 100644 --- a/pkg/optimizely/cache_test.go +++ b/pkg/optimizely/cache_test.go @@ -104,7 +104,7 @@ func (suite *CacheTestSuite) TestUpdateConfigs() { } func (suite *CacheTestSuite) TestNewCache() { - agentMetricsRegistry := metrics.NewRegistry() + agentMetricsRegistry := metrics.NewRegistry("") sdkMetricsRegistry := NewRegistry(agentMetricsRegistry) // To improve coverage @@ -252,7 +252,7 @@ type DefaultLoaderTestSuite struct { func (s *DefaultLoaderTestSuite) SetupTest() { // Need the registry to be created only once since it panics if we create gauges with the same name again and again doOnce.Do(func() { - s.registry = &MetricsRegistry{metrics.NewRegistry()} + s.registry = &MetricsRegistry{metrics.NewRegistry("")} }) s.upsMap = cmap.New() s.bpFactory = func(options ...event.BPOptionConfig) *event.BatchEventProcessor { diff --git a/pkg/optimizely/metrics_test.go b/pkg/optimizely/metrics_test.go index 76ce9c8b..b64ed154 100644 --- a/pkg/optimizely/metrics_test.go +++ b/pkg/optimizely/metrics_test.go @@ -35,7 +35,7 @@ func TestCounterValid(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := metrics.NewRegistry() + metricsRegistry := metrics.NewRegistry("") metricsSDKRegistry := NewRegistry(metricsRegistry) counter := metricsSDKRegistry.GetCounter("metrics") @@ -56,7 +56,7 @@ func TestGaugeValid(t *testing.T) { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", "/", nil) - metricsRegistry := metrics.NewRegistry() + metricsRegistry := metrics.NewRegistry("") metricsSDKRegistry := NewRegistry(metricsRegistry) gauge := metricsSDKRegistry.GetGauge("metrics") diff --git a/pkg/routers/api_test.go b/pkg/routers/api_test.go index 2f0fb84f..dae294bb 100644 --- a/pkg/routers/api_test.go +++ b/pkg/routers/api_test.go @@ -33,7 +33,7 @@ import ( "github.com/optimizely/agent/pkg/optimizely/optimizelytest" ) -var metricsRegistry = metrics.NewRegistry() +var metricsRegistry = metrics.NewRegistry("") const methodHeaderKey = "X-Method-Header" const clientHeaderKey = "X-Client-Header" From 369fe722c9d89b23f20a5487065c84302532056d Mon Sep 17 00:00:00 2001 From: pulak-opti <129880418+pulak-opti@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:04:16 +0600 Subject: [PATCH 2/6] [FSSDK-9705] feat: add OpenTelemetry tracing (#400) * add a basic tracing pipeline * add tracing config * update tracing config * use context * update handler with middleware * support both http & grpc protocal * refactor config * add unit test * update unit test * refactor code * make tracing disabled by default * add config doc * fix typo * collect more attributes * add common http attributes * collect http status code attribute * collect sdk key attribute * support distributed tracing * add unit test for trace id generator * update unit test * fix typo * fix typo in config file * update config.yaml * make trace_id configurable for distributed tracing --- cmd/optimizely/main.go | 120 ++++++++++++++++++++++++++++++++- cmd/optimizely/main_test.go | 89 ++++++++++++++++++++++++ config.yaml | 39 +++++++++++ config/config.go | 46 +++++++++++++ go.mod | 31 +++++++-- go.sum | 73 +++++++++++++++----- pkg/handlers/activate.go | 4 +- pkg/handlers/override.go | 4 +- pkg/handlers/track.go | 2 +- pkg/middleware/trace.go | 123 ++++++++++++++++++++++++++++++++++ pkg/middleware/trace_test.go | 96 ++++++++++++++++++++++++++ pkg/optimizely/client.go | 46 +++++++++++-- pkg/optimizely/client_test.go | 17 ++--- pkg/routers/api.go | 46 ++++++++----- pkg/routers/api_test.go | 13 ++-- 15 files changed, 681 insertions(+), 68 deletions(-) create mode 100644 pkg/middleware/trace.go create mode 100644 pkg/middleware/trace_test.go diff --git a/cmd/optimizely/main.go b/cmd/optimizely/main.go index 7a6d97b9..7586e7dd 100644 --- a/cmd/optimizely/main.go +++ b/cmd/optimizely/main.go @@ -18,6 +18,8 @@ package main import ( "bytes" "context" + "errors" + "fmt" "os" "os/signal" "runtime" @@ -32,6 +34,7 @@ import ( "github.com/optimizely/agent/config" "github.com/optimizely/agent/pkg/metrics" + "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/agent/pkg/optimizely" "github.com/optimizely/agent/pkg/routers" "github.com/optimizely/agent/pkg/server" @@ -42,6 +45,14 @@ import ( // Initiate the loading of the userprofileservice plugins _ "github.com/optimizely/agent/plugins/userprofileservice/all" "github.com/optimizely/go-sdk/pkg/logging" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" ) // Version holds the admin version @@ -112,6 +123,97 @@ func initLogging(conf config.LogConfig) { } } +func getStdOutTraceProvider(conf config.OTELTracingConfig) (*sdktrace.TracerProvider, error) { + f, err := os.Create(conf.Services.StdOut.Filename) + if err != nil { + return nil, fmt.Errorf("failed to create the trace file, error: %s", err.Error()) + } + + exp, err := stdouttrace.New( + stdouttrace.WithPrettyPrint(), + stdouttrace.WithWriter(f), + ) + if err != nil { + return nil, fmt.Errorf("failed to create the collector exporter, error: %s", err.Error()) + } + + res, err := resource.New( + context.Background(), + resource.WithAttributes( + semconv.ServiceNameKey.String(conf.ServiceName), + semconv.DeploymentEnvironmentKey.String(conf.Env), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create the otel resource, error: %s", err.Error()) + } + + return sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(res), + sdktrace.WithIDGenerator(middleware.NewTraceIDGenerator(conf.TraceIDHeaderKey)), + ), nil +} + +func getOTELTraceClient(conf config.OTELTracingConfig) (otlptrace.Client, error) { + switch conf.Services.Remote.Protocol { + case config.TracingRemoteProtocolHTTP: + return otlptracehttp.NewClient( + otlptracehttp.WithInsecure(), + otlptracehttp.WithEndpoint(conf.Services.Remote.Endpoint), + ), nil + case config.TracingRemoteProtocolGRPC: + return otlptracegrpc.NewClient( + otlptracegrpc.WithInsecure(), + otlptracegrpc.WithEndpoint(conf.Services.Remote.Endpoint), + ), nil + default: + return nil, errors.New("unknown remote tracing protocal") + } +} + +func getRemoteTraceProvider(conf config.OTELTracingConfig) (*sdktrace.TracerProvider, error) { + res, err := resource.New( + context.Background(), + resource.WithAttributes( + semconv.ServiceNameKey.String(conf.ServiceName), + semconv.DeploymentEnvironmentKey.String(conf.Env), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create the otel resource, error: %s", err.Error()) + } + + traceClient, err := getOTELTraceClient(conf) + if err != nil { + return nil, fmt.Errorf("failed to create the remote trace client, error: %s", err.Error()) + } + + traceExporter, err := otlptrace.New(context.Background(), traceClient) + if err != nil { + return nil, fmt.Errorf("failed to create the remote trace exporter, error: %s", err.Error()) + } + + bsp := sdktrace.NewBatchSpanProcessor(traceExporter) + return sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(conf.Services.Remote.SampleRate))), + sdktrace.WithResource(res), + sdktrace.WithSpanProcessor(bsp), + sdktrace.WithIDGenerator(middleware.NewTraceIDGenerator(conf.TraceIDHeaderKey)), + ), nil +} + +func initTracing(conf config.OTELTracingConfig) (*sdktrace.TracerProvider, error) { + switch conf.Default { + case config.TracingServiceTypeRemote: + return getRemoteTraceProvider(conf) + case config.TracingServiceTypeStdOut: + return getStdOutTraceProvider(conf) + default: + return nil, errors.New("unknown tracing service type") + } +} + func setRuntimeEnvironment(conf config.RuntimeConfig) { if conf.BlockProfileRate != 0 { log.Warn().Msgf("Setting non-zero blockProfileRate is NOT recommended for production") @@ -133,6 +235,22 @@ func main() { conf := loadConfig(v) initLogging(conf.Log) + if conf.Tracing.Enabled { + tp, err := initTracing(conf.Tracing.OpenTelemetry) + if err != nil { + log.Panic().Err(err).Msg("Unable to initialize tracing") + } + defer func() { + if err := tp.Shutdown(context.Background()); err != nil { + log.Error().Err(err).Msg("Failed to shutdown tracing") + } + }() + otel.SetTracerProvider(tp) + log.Info().Msg(fmt.Sprintf("Tracing enabled with service %q", conf.Tracing.OpenTelemetry.Default)) + } else { + log.Info().Msg("Tracing disabled") + } + conf.LogConfigWarnings() setRuntimeEnvironment(conf.Runtime) @@ -157,7 +275,7 @@ func main() { cancel() }() - apiRouter := routers.NewDefaultAPIRouter(optlyCache, conf.API, agentMetricsRegistry) + apiRouter := routers.NewDefaultAPIRouter(optlyCache, *conf, agentMetricsRegistry) adminRouter := routers.NewAdminRouter(*conf) log.Info().Str("version", conf.Version).Msg("Starting services.") diff --git a/cmd/optimizely/main_test.go b/cmd/optimizely/main_test.go index d99a47b0..d23e989a 100644 --- a/cmd/optimizely/main_test.go +++ b/cmd/optimizely/main_test.go @@ -362,3 +362,92 @@ func TestLoggingWithIncludeSdkKey(t *testing.T) { }) assert.False(t, optimizely.ShouldIncludeSDKKey) } + +func Test_initTracing(t *testing.T) { + type args struct { + conf config.OTELTracingConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "should return error when exporter type is not supported", + args: args{ + conf: config.OTELTracingConfig{ + Default: "unsupported", + }, + }, + wantErr: true, + }, + { + name: "should return no error stdout tracing exporter", + args: args{ + conf: config.OTELTracingConfig{ + Default: "stdout", + Services: config.TracingServiceConfig{ + StdOut: config.TracingStdOutConfig{ + Filename: "trace.out", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "should return no error for remote tracing exporter with http protocal", + args: args{ + conf: config.OTELTracingConfig{ + Default: "remote", + Services: config.TracingServiceConfig{ + Remote: config.TracingRemoteConfig{ + Endpoint: "localhost:1234", + Protocol: "http", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "should return no error for remote tracing exporter with grpc protocal", + args: args{ + conf: config.OTELTracingConfig{ + Default: "remote", + Services: config.TracingServiceConfig{ + Remote: config.TracingRemoteConfig{ + Endpoint: "localhost:1234", + Protocol: "grpc", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "should return no error for remote tracing exporter with invalid protocal", + args: args{ + conf: config.OTELTracingConfig{ + Default: "remote", + Services: config.TracingServiceConfig{ + Remote: config.TracingRemoteConfig{ + Endpoint: "localhost:1234", + Protocol: "udp/invalid", + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := initTracing(tt.args.conf) + if (err != nil) != tt.wantErr { + t.Errorf("initTracing() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/config.yaml b/config.yaml index 4d1f4b95..56f8db7c 100644 --- a/config.yaml +++ b/config.yaml @@ -26,6 +26,45 @@ log: ## to set whether or not the SDK key is included in the logging output. includeSdkKey: true +## +## tracing: tracing configuration +## +tracing: + ## bydefault tracing is disabled + ## to enable tracing set enabled to true + enabled: false + # opentelemetry tracing configuration + opentelemetry: + ## bydefault stdout exporter is enabled + ## to enable remote exporter set default as "remote" + default: "stdout" + ## tracing service name + serviceName: "optimizely-agent" + ## tracing environment name + ## example: for production environment env can be set as "prod" + env: "dev" + ## HTTP Header Key for TraceID in Distributed Tracing + ## The value set in HTTP Header must be a hex compliant with the W3C trace-context specification. + ## See more at https://www.w3.org/TR/trace-context/#trace-id + traceIDHeaderKey: "X-Optimizely-Trace-ID" + ## tracing service configuration + services: + ## stdout exporter configuration + stdout: + ## for stdout tracing data is saved in the specified file + filename: "trace.out" + ## remote exporter configuration + remote: + ## remote collector endpoint + endpoint: "localhost:4317" + ## supported protocols are "http" and "grpc" + protocol: "grpc" + ## "sampleRate" refers to the rate at which traces are collected and recorded. + ## sampleRate >= 1 will always sample. + ## sampleRate < 0 are treated as zero i.e. never sample. + sampleRate: 1.0 + + ## ## http server configuration ## diff --git a/config/config.go b/config/config.go index 6f84ca04..b0412dc5 100644 --- a/config/config.go +++ b/config/config.go @@ -71,6 +71,9 @@ func NewDefaultConfig() *AgentConfig { IncludeSDKKey: true, Level: "info", }, + Tracing: TracingConfig{ + Enabled: false, + }, Client: ClientConfig{ PollingInterval: 1 * time.Minute, BatchSize: 10, @@ -124,6 +127,7 @@ type AgentConfig struct { Admin AdminConfig `json:"admin"` API APIConfig `json:"api"` Log LogConfig `json:"log"` + Tracing TracingConfig `json:"tracing"` Client ClientConfig `json:"client"` Runtime RuntimeConfig `json:"runtime"` Server ServerConfig `json:"server"` @@ -173,6 +177,48 @@ type LogConfig struct { Level string `json:"level"` } +type TracingConfig struct { + Enabled bool `json:"enabled"` + OpenTelemetry OTELTracingConfig `json:"opentelemetry"` +} + +type TracingServiceType string + +const ( + TracingServiceTypeStdOut TracingServiceType = "stdout" + TracingServiceTypeRemote TracingServiceType = "remote" +) + +type TracingRemoteProtocol string + +const ( + TracingRemoteProtocolGRPC TracingRemoteProtocol = "grpc" + TracingRemoteProtocolHTTP TracingRemoteProtocol = "http" +) + +type OTELTracingConfig struct { + Default TracingServiceType `json:"default"` + ServiceName string `json:"serviceName"` + Env string `json:"env"` + TraceIDHeaderKey string `json:"traceIDHeaderKey"` + Services TracingServiceConfig `json:"services"` +} + +type TracingServiceConfig struct { + StdOut TracingStdOutConfig `json:"stdout"` + Remote TracingRemoteConfig `json:"remote"` +} + +type TracingStdOutConfig struct { + Filename string `json:"filename"` +} + +type TracingRemoteConfig struct { + Endpoint string `json:"endpoint"` + Protocol TracingRemoteProtocol `json:"protocol"` + SampleRate float64 `json:"sampleRate"` +} + // PluginConfigs defines the generic mapping of middleware plugins type PluginConfigs map[string]interface{} diff --git a/go.mod b/go.mod index 811b42a9..1d199067 100644 --- a/go.mod +++ b/go.mod @@ -18,20 +18,37 @@ require ( github.com/rakyll/statik v0.1.7 github.com/rs/zerolog v1.29.0 github.com/spf13/viper v1.15.0 - github.com/stretchr/testify v1.8.2 - golang.org/x/crypto v0.6.0 - golang.org/x/sync v0.1.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 + go.opentelemetry.io/otel/sdk v1.19.0 + go.opentelemetry.io/otel/trace v1.19.0 + golang.org/x/crypto v0.11.0 + golang.org/x/sync v0.3.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.30.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect - google.golang.org/protobuf v1.28.1 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + golang.org/x/net v0.12.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/grpc v1.58.2 // indirect + google.golang.org/protobuf v1.31.0 // indirect ) require ( @@ -62,8 +79,8 @@ require ( github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/twmb/murmur3 v1.1.6 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.11.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e90cd79c..b1e14991 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= @@ -99,6 +101,11 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -107,6 +114,7 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -132,8 +140,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -169,6 +177,8 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -197,7 +207,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -266,7 +276,7 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= @@ -298,8 +308,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/twmb/murmur3 v1.0.0/go.mod h1:5Y5m8Y8WIyucaICVP+Aep5C8ydggjEuRQHDq1icoOYo= @@ -315,6 +325,25 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -323,8 +352,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -392,7 +421,8 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -413,8 +443,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -463,8 +493,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -473,8 +503,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -591,6 +621,11 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -607,6 +642,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= +google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -619,13 +656,13 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/pkg/handlers/activate.go b/pkg/handlers/activate.go index 0a35d662..4a48f90a 100644 --- a/pkg/handlers/activate.go +++ b/pkg/handlers/activate.go @@ -76,10 +76,10 @@ func Activate(w http.ResponseWriter, r *http.Request) { switch value { case "experiment": logger.Debug().Str("experimentKey", key).Msg("fetching experiment decision") - d, err = optlyClient.ActivateExperiment(key, uc, disableTracking) + d, err = optlyClient.ActivateExperiment(r.Context(), key, uc, disableTracking) case "feature": logger.Debug().Str("featureKey", key).Msg("fetching feature decision") - d, err = optlyClient.ActivateFeature(key, uc, disableTracking) + d, err = optlyClient.ActivateFeature(r.Context(), key, uc, disableTracking) case "experimentKey-not-found": logger.Debug().Str("experimentKey", key).Msg("experimentKey not found") d = &optimizely.Decision{ diff --git a/pkg/handlers/override.go b/pkg/handlers/override.go index 749bbf4b..dff9aa9e 100644 --- a/pkg/handlers/override.go +++ b/pkg/handlers/override.go @@ -61,7 +61,7 @@ func Override(w http.ResponseWriter, r *http.Request) { // Empty variation means remove if body.VariationKey == "" { - if override, err := optlyClient.RemoveForcedVariation(experimentKey, body.UserID); err != nil { + if override, err := optlyClient.RemoveForcedVariation(r.Context(), experimentKey, body.UserID); err != nil { RenderError(err, http.StatusInternalServerError, w, r) } else { render.JSON(w, r, override) @@ -70,7 +70,7 @@ func Override(w http.ResponseWriter, r *http.Request) { } logger.Debug().Str("experimentKey", experimentKey).Str("variationKey", body.VariationKey).Msg("setting override") - if override, err := optlyClient.SetForcedVariation(experimentKey, body.UserID, body.VariationKey); err != nil { + if override, err := optlyClient.SetForcedVariation(r.Context(), experimentKey, body.UserID, body.VariationKey); err != nil { RenderError(err, http.StatusInternalServerError, w, r) } else { render.JSON(w, r, override) diff --git a/pkg/handlers/track.go b/pkg/handlers/track.go index 8359ee42..61e40c7b 100644 --- a/pkg/handlers/track.go +++ b/pkg/handlers/track.go @@ -60,7 +60,7 @@ func TrackEvent(w http.ResponseWriter, r *http.Request) { Attributes: body.UserAttributes, } - track, err := optlyClient.TrackEvent(eventKey, uc, body.EventTags) + track, err := optlyClient.TrackEvent(r.Context(), eventKey, uc, body.EventTags) if err != nil { RenderError(err, http.StatusInternalServerError, w, r) return diff --git a/pkg/middleware/trace.go b/pkg/middleware/trace.go new file mode 100644 index 00000000..9b11768f --- /dev/null +++ b/pkg/middleware/trace.go @@ -0,0 +1,123 @@ +/**************************************************************************** + * Copyright 2023 Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package middleware // +package middleware + +import ( + "context" + crand "crypto/rand" + "encoding/binary" + "math/rand" + "net/http" + "sync" + + "github.com/optimizely/agent/config" + "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" +) + +type traceIDGenerator struct { + sync.Mutex + randSource *rand.Rand + traceIDHeaderKey string +} + +func NewTraceIDGenerator(traceIDHeaderKey string) *traceIDGenerator { + var rngSeed int64 + _ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed) + return &traceIDGenerator{ + randSource: rand.New(rand.NewSource(rngSeed)), + traceIDHeaderKey: traceIDHeaderKey, + } +} + +func (gen *traceIDGenerator) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID { + gen.Lock() + defer gen.Unlock() + sid := trace.SpanID{} + _, _ = gen.randSource.Read(sid[:]) + return sid +} + +func (gen *traceIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) { + gen.Lock() + defer gen.Unlock() + tid := trace.TraceID{} + _, _ = gen.randSource.Read(tid[:]) + sid := trace.SpanID{} + _, _ = gen.randSource.Read(sid[:]) + + // read trace id from header if provided + traceIDHeader := ctx.Value(gen.traceIDHeaderKey) + if val, ok := traceIDHeader.(string); ok { + if val != "" { + headerTraceId, err := trace.TraceIDFromHex(val) + if err == nil { + tid = headerTraceId + } else { + log.Error().Err(err).Msg("failed to parse trace id from header, invalid trace id") + } + } + } + + return tid, sid +} + +type statusRecorder struct { + http.ResponseWriter + statusCode int +} + +func (r *statusRecorder) WriteHeader(code int) { + r.statusCode = code + r.ResponseWriter.WriteHeader(code) +} + +func AddTracing(conf config.TracingConfig, tracerName, spanName string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + pctx := context.WithValue(r.Context(), conf.OpenTelemetry.TraceIDHeaderKey, r.Header.Get(conf.OpenTelemetry.TraceIDHeaderKey)) + + ctx, span := otel.Tracer(tracerName).Start(pctx, spanName) + defer span.End() + + span.SetAttributes( + semconv.HTTPMethodKey.String(r.Method), + semconv.HTTPRouteKey.String(r.URL.Path), + semconv.HTTPURLKey.String(r.URL.String()), + semconv.HTTPHostKey.String(r.Host), + semconv.HTTPSchemeKey.String(r.URL.Scheme), + attribute.String(OptlySDKHeader, r.Header.Get(OptlySDKHeader)), + ) + + rec := &statusRecorder{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + next.ServeHTTP(rec, r.WithContext(ctx)) + + span.SetAttributes( + semconv.HTTPStatusCodeKey.Int(rec.statusCode), + ) + } + return http.HandlerFunc(fn) + } +} diff --git a/pkg/middleware/trace_test.go b/pkg/middleware/trace_test.go new file mode 100644 index 00000000..7c747947 --- /dev/null +++ b/pkg/middleware/trace_test.go @@ -0,0 +1,96 @@ +/**************************************************************************** + * Copyright 2023 Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package middleware // +package middleware + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/optimizely/agent/config" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/trace" +) + +func TestAddTracing(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/text") + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + req := httptest.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + middleware := http.Handler(AddTracing(config.TracingConfig{}, "test-tracer", "test-span")(handler)) + + // Serve the request through the middleware + middleware.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("Expected status code %v, but got %v", http.StatusOK, status) + } + + if body := rr.Body.String(); body != "OK" { + t.Errorf("Expected response body %v, but got %v", "OK", body) + } + + if typeHeader := rr.Header().Get("Content-Type"); typeHeader != "application/text" { + t.Errorf("Expected Content-Type header %v, but got %v", "application/text", typeHeader) + } +} + +func TestNewIDs(t *testing.T) { + gen := NewTraceIDGenerator("") + n := 1000 + + for i := 0; i < n; i++ { + traceID, spanID := gen.NewIDs(context.Background()) + assert.Truef(t, traceID.IsValid(), "trace id: %s", traceID.String()) + assert.Truef(t, spanID.IsValid(), "span id: %s", spanID.String()) + } +} + +func TestNewSpanID(t *testing.T) { + gen := NewTraceIDGenerator("") + testTraceID := [16]byte{123, 123} + n := 1000 + + for i := 0; i < n; i++ { + spanID := gen.NewSpanID(context.Background(), testTraceID) + assert.Truef(t, spanID.IsValid(), "span id: %s", spanID.String()) + } +} + +func TestNewSpanIDWithInvalidTraceID(t *testing.T) { + gen := NewTraceIDGenerator("") + spanID := gen.NewSpanID(context.Background(), trace.TraceID{}) + assert.Truef(t, spanID.IsValid(), "span id: %s", spanID.String()) +} + +func TestTraceIDWithGivenHeaderValue(t *testing.T) { + traceHeader := "X-Trace-ID" + traceID := "9b8eac67e332c6f8baf1e013de6891bb" + + gen := NewTraceIDGenerator(traceHeader) + + ctx := context.WithValue(context.Background(), traceHeader, traceID) + genTraceID, _ := gen.NewIDs(ctx) + assert.Truef(t, genTraceID.IsValid(), "trace id: %s", genTraceID.String()) + assert.Equal(t, traceID, genTraceID.String()) +} diff --git a/pkg/optimizely/client.go b/pkg/optimizely/client.go index 5e84e6db..3a3593bd 100644 --- a/pkg/optimizely/client.go +++ b/pkg/optimizely/client.go @@ -18,11 +18,14 @@ package optimizely import ( + "context" "errors" optimizelyclient "github.com/optimizely/go-sdk/pkg/client" "github.com/optimizely/go-sdk/pkg/decision" "github.com/optimizely/go-sdk/pkg/entities" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" ) // ErrEntityNotFound is returned when no entity exists with a given key @@ -81,7 +84,11 @@ func (c *OptlyClient) UpdateConfig() { } // TrackEvent checks for the existence of the event before calling the OptimizelyClient Track method -func (c *OptlyClient) TrackEvent(eventKey string, uc entities.UserContext, eventTags map[string]interface{}) (*Track, error) { +func (c *OptlyClient) TrackEvent(ctx context.Context, eventKey string, uc entities.UserContext, eventTags map[string]interface{}) (*Track, error) { + _, span := otel.Tracer("trackHandler").Start(ctx, "TrackEvent") + defer span.End() + span.SetAttributes(attribute.String("trackEventKey", eventKey)) + tr := &Track{ UserID: uc.ID, EventKey: eventKey, @@ -104,7 +111,10 @@ func (c *OptlyClient) TrackEvent(eventKey string, uc entities.UserContext, event // SetForcedVariation sets a forced variation for the argument experiment key and user ID // Returns false if the same forced variation was already set for the argument experiment and user, true otherwise // Returns an error when forced variations are not available on this OptlyClient instance -func (c *OptlyClient) SetForcedVariation(experimentKey, userID, variationKey string) (*Override, error) { +func (c *OptlyClient) SetForcedVariation(ctx context.Context, experimentKey, userID, variationKey string) (*Override, error) { + _, span := otel.Tracer("overrideHandler").Start(ctx, "SetForcedVariation") + defer span.End() + if c.ForcedVariations == nil { return &Override{}, ErrForcedVariationsUninitialized } @@ -139,12 +149,18 @@ func (c *OptlyClient) SetForcedVariation(experimentKey, userID, variationKey str override.Messages = messages } + span.SetAttributes(attribute.String("variationKey", override.VariationKey)) + span.SetAttributes(attribute.String("experimentKey", override.ExperimentKey)) + c.ForcedVariations.SetVariation(forcedVariationKey, variationKey) return &override, nil } // RemoveForcedVariation removes any forced variation that was previously set for the argument experiment key and user ID -func (c *OptlyClient) RemoveForcedVariation(experimentKey, userID string) (*Override, error) { +func (c *OptlyClient) RemoveForcedVariation(ctx context.Context, experimentKey, userID string) (*Override, error) { + _, span := otel.Tracer("overrideHandler").Start(ctx, "RemoveForcedVariation") + defer span.End() + if c.ForcedVariations == nil { return &Override{}, ErrForcedVariationsUninitialized } @@ -171,12 +187,18 @@ func (c *OptlyClient) RemoveForcedVariation(experimentKey, userID string) (*Over override.Messages = messages c.ForcedVariations.RemoveVariation(forcedVariationKey) + span.SetAttributes(attribute.String("variationKey", override.VariationKey)) + span.SetAttributes(attribute.String("experimentKey", override.ExperimentKey)) + return &override, nil } // ActivateFeature activates a feature for a given user by getting the feature enabled status and all // associated variables -func (c *OptlyClient) ActivateFeature(key string, uc entities.UserContext, disableTracking bool) (*Decision, error) { +func (c *OptlyClient) ActivateFeature(ctx context.Context, key string, uc entities.UserContext, disableTracking bool) (*Decision, error) { + _, span := otel.Tracer("activateHandler").Start(ctx, "ActivateFeature") + defer span.End() + unsafeDecisionInfo, err := c.GetDetailedFeatureDecisionUnsafe(key, uc, disableTracking) if err != nil { return &Decision{}, err @@ -192,11 +214,20 @@ func (c *OptlyClient) ActivateFeature(key string, uc entities.UserContext, disab VariationKey: unsafeDecisionInfo.VariationKey, } + span.SetAttributes(attribute.String("variationKey", dec.VariationKey)) + span.SetAttributes(attribute.String("experimentKey", dec.ExperimentKey)) + span.SetAttributes(attribute.String("featureKey", dec.FeatureKey)) + span.SetAttributes(attribute.Bool("enabled", dec.Enabled)) + span.SetAttributes(attribute.String("type", dec.Type)) + return dec, nil } // ActivateExperiment activates an experiment -func (c *OptlyClient) ActivateExperiment(key string, uc entities.UserContext, disableTracking bool) (*Decision, error) { +func (c *OptlyClient) ActivateExperiment(ctx context.Context, key string, uc entities.UserContext, disableTracking bool) (*Decision, error) { + _, span := otel.Tracer("activateHandler").Start(ctx, "ActivateExperiment") + defer span.End() + var variation string var err error @@ -218,5 +249,10 @@ func (c *OptlyClient) ActivateExperiment(key string, uc entities.UserContext, di Variables: map[string]interface{}{}, } + span.SetAttributes(attribute.String("variationKey", dec.VariationKey)) + span.SetAttributes(attribute.String("experimentKey", dec.ExperimentKey)) + span.SetAttributes(attribute.Bool("enabled", dec.Enabled)) + span.SetAttributes(attribute.String("type", dec.Type)) + return dec, nil } diff --git a/pkg/optimizely/client_test.go b/pkg/optimizely/client_test.go index 0bb347fe..090241f7 100644 --- a/pkg/optimizely/client_test.go +++ b/pkg/optimizely/client_test.go @@ -18,6 +18,7 @@ package optimizely import ( + "context" "fmt" "testing" @@ -65,7 +66,7 @@ func (suite *ClientTestSuite) TestTrackEvent() { eventKey := "eventKey" suite.testClient.AddEvent(entities.Event{Key: eventKey}) tags := map[string]interface{}{"tag": "value"} - actual, err := suite.optlyClient.TrackEvent(eventKey, suite.userContext, tags) + actual, err := suite.optlyClient.TrackEvent(context.Background(), eventKey, suite.userContext, tags) suite.NoError(err) expected := &Track{ @@ -122,7 +123,7 @@ func (suite *ClientTestSuite) TestValidSetForcedVariations() { userID := "testUser" for _, scenario := range scenarios { - actual, err := suite.optlyClient.SetForcedVariation(scenario.experimentKey, userID, scenario.variationKey) + actual, err := suite.optlyClient.SetForcedVariation(context.Background(), scenario.experimentKey, userID, scenario.variationKey) suite.NoError(err) expected := &Override{ @@ -161,10 +162,10 @@ func (suite *ClientTestSuite) TestRemoveForcedVariation() { } userID := "testUser" - _, _ = suite.optlyClient.SetForcedVariation(suite.featureExp.Key, userID, "enabled_var") + _, _ = suite.optlyClient.SetForcedVariation(context.Background(), suite.featureExp.Key, userID, "enabled_var") for _, scenario := range scenarios { - actual, err := suite.optlyClient.RemoveForcedVariation(suite.featureExp.Key, userID) + actual, err := suite.optlyClient.RemoveForcedVariation(context.Background(), suite.featureExp.Key, userID) suite.NoError(err) expected := &Override{ @@ -211,7 +212,7 @@ func (suite *ClientTestSuite) TestActivateFeature() { // Response should be the same regardless of the flag for _, flag := range []bool{true, false} { - actual, err := suite.optlyClient.ActivateFeature(feature.Key, entities.UserContext{ID: "testUser"}, flag) + actual, err := suite.optlyClient.ActivateFeature(context.Background(), feature.Key, entities.UserContext{ID: "testUser"}, flag) suite.NoError(err) suite.Equal(expected, actual) } @@ -237,7 +238,7 @@ func (suite *ClientTestSuite) TestActivateExperiment() { // Response should be the same regardless of the flag for _, flag := range []bool{true, false} { - actual, err := suite.optlyClient.ActivateExperiment(experiment.Key, entities.UserContext{ID: "testUser"}, flag) + actual, err := suite.optlyClient.ActivateExperiment(context.Background(), experiment.Key, entities.UserContext{ID: "testUser"}, flag) suite.NoError(err) suite.Equal(expected, actual) } @@ -314,7 +315,7 @@ func TestTrackErrorConfigManager(t *testing.T) { } uc := entities.UserContext{ID: "userId"} - actual, err := optlyClient.TrackEvent("something", uc, map[string]interface{}{}) + actual, err := optlyClient.TrackEvent(context.Background(), "something", uc, map[string]interface{}{}) assert.EqualError(t, err, "config error") expected := &Track{} @@ -341,7 +342,7 @@ func TestTrackErrorClient(t *testing.T) { } uc := entities.UserContext{ID: "userId"} - actual, err := optlyClient.TrackEvent("something", uc, map[string]interface{}{}) + actual, err := optlyClient.TrackEvent(context.Background(), "something", uc, map[string]interface{}{}) assert.NoError(t, err) expected := &Track{ diff --git a/pkg/routers/api.go b/pkg/routers/api.go index c2e513c8..88897df6 100644 --- a/pkg/routers/api.go +++ b/pkg/routers/api.go @@ -63,8 +63,8 @@ func forbiddenHandler(message string) http.HandlerFunc { } // NewDefaultAPIRouter creates a new router with the default backing optimizely.Cache -func NewDefaultAPIRouter(optlyCache optimizely.Cache, conf config.APIConfig, metricsRegistry *metrics.Registry) http.Handler { - +func NewDefaultAPIRouter(optlyCache optimizely.Cache, agentConf config.AgentConfig, metricsRegistry *metrics.Registry) http.Handler { + conf := agentConf.API authProvider := middleware.NewAuth(&conf.Auth) if authProvider == nil { log.Error().Msg("unable to initialize api auth middleware.") @@ -109,19 +109,19 @@ func NewDefaultAPIRouter(optlyCache optimizely.Cache, conf config.APIConfig, met corsHandler: corsHandler, } - return NewAPIRouter(spec) + return NewAPIRouter(spec, agentConf.Tracing) } // NewAPIRouter returns HTTP API router backed by an optimizely.Cache implementation -func NewAPIRouter(opt *APIOptions) *chi.Mux { +func NewAPIRouter(opt *APIOptions, traceConf config.TracingConfig) *chi.Mux { r := chi.NewRouter() - WithAPIRouter(opt, r) + WithAPIRouter(opt, r, traceConf) return r } // WithAPIRouter appends routes and middleware to the given router. // See https://godoc.org/github.com/go-chi/chi/v5#Mux.Group for usage -func WithAPIRouter(opt *APIOptions, r chi.Router) { +func WithAPIRouter(opt *APIOptions, r chi.Router, traceConf config.TracingConfig) { getConfigTimer := middleware.Metricize("get-config", opt.metricsRegistry) getDatafileTimer := middleware.Metricize("get-datafile", opt.metricsRegistry) activateTimer := middleware.Metricize("activate", opt.metricsRegistry) @@ -134,6 +134,18 @@ func WithAPIRouter(opt *APIOptions, r chi.Router) { createAccesstokenTimer := middleware.Metricize("create-api-access-token", opt.metricsRegistry) contentTypeMiddleware := chimw.AllowContentType("application/json") + configTracer := middleware.AddTracing(traceConf, "configHandler", "OptimizelyConfig") + datafileTracer := middleware.AddTracing(traceConf, "datafileHandler", "OptimizelyDatafile") + activateTracer := middleware.AddTracing(traceConf, "activateHandler", "Activate") + decideTracer := middleware.AddTracing(traceConf, "decideHandler", "Decide") + trackTracer := middleware.AddTracing(traceConf, "trackHandler", "Track") + overrideTracer := middleware.AddTracing(traceConf, "overrideHandler", "Override") + lookupTracer := middleware.AddTracing(traceConf, "lookupHandler", "Lookup") + saveTracer := middleware.AddTracing(traceConf, "saveHandler", "Save") + sendOdpEventTracer := middleware.AddTracing(traceConf, "sendOdpEventHandler", "SendOdpEvent") + nStreamTracer := middleware.AddTracing(traceConf, "notificationHandler", "SendNotificationEvent") + authTracer := middleware.AddTracing(traceConf, "authHandler", "AuthToken") + if opt.maxConns > 0 { // Note this is NOT a rate limiter, but a concurrency threshold r.Use(chimw.Throttle(opt.maxConns)) @@ -144,19 +156,19 @@ func WithAPIRouter(opt *APIOptions, r chi.Router) { r.Route("/v1", func(r chi.Router) { r.Use(opt.corsHandler, opt.sdkMiddleware) - r.With(getConfigTimer, opt.oAuthMiddleware).Get("/config", opt.configHandler) - r.With(getDatafileTimer, opt.oAuthMiddleware).Get("/datafile", opt.datafileHandler) - r.With(activateTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/activate", opt.activateHandler) - r.With(decideTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/decide", opt.decideHandler) - r.With(trackTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/track", opt.trackHandler) - r.With(overrideTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/override", opt.overrideHandler) - r.With(lookupTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/lookup", opt.lookupHandler) - r.With(saveTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/save", opt.saveHandler) - r.With(sendOdpEventTimer, opt.oAuthMiddleware, contentTypeMiddleware).Post("/send-odp-event", opt.sendOdpEventHandler) - r.With(opt.oAuthMiddleware).Get("/notifications/event-stream", opt.nStreamHandler) + r.With(getConfigTimer, opt.oAuthMiddleware, configTracer).Get("/config", opt.configHandler) + r.With(getDatafileTimer, opt.oAuthMiddleware, datafileTracer).Get("/datafile", opt.datafileHandler) + r.With(activateTimer, opt.oAuthMiddleware, contentTypeMiddleware, activateTracer).Post("/activate", opt.activateHandler) + r.With(decideTimer, opt.oAuthMiddleware, contentTypeMiddleware, decideTracer).Post("/decide", opt.decideHandler) + r.With(trackTimer, opt.oAuthMiddleware, contentTypeMiddleware, trackTracer).Post("/track", opt.trackHandler) + r.With(overrideTimer, opt.oAuthMiddleware, contentTypeMiddleware, overrideTracer).Post("/override", opt.overrideHandler) + r.With(lookupTimer, opt.oAuthMiddleware, contentTypeMiddleware, lookupTracer).Post("/lookup", opt.lookupHandler) + r.With(saveTimer, opt.oAuthMiddleware, contentTypeMiddleware, saveTracer).Post("/save", opt.saveHandler) + r.With(sendOdpEventTimer, opt.oAuthMiddleware, contentTypeMiddleware, sendOdpEventTracer).Post("/send-odp-event", opt.sendOdpEventHandler) + r.With(opt.oAuthMiddleware, nStreamTracer).Get("/notifications/event-stream", opt.nStreamHandler) }) - r.With(createAccesstokenTimer).Post("/oauth/token", opt.oAuthHandler) + r.With(createAccesstokenTimer, authTracer).Post("/oauth/token", opt.oAuthHandler) statikFS, err := fs.New() if err != nil { diff --git a/pkg/routers/api_test.go b/pkg/routers/api_test.go index dae294bb..9196d880 100644 --- a/pkg/routers/api_test.go +++ b/pkg/routers/api_test.go @@ -126,7 +126,7 @@ func (suite *APIV1TestSuite) SetupTest() { corsHandler: testCorsHandler, } - suite.mux = NewAPIRouter(opts) + suite.mux = NewAPIRouter(opts, config.TracingConfig{}) } func (suite *APIV1TestSuite) TestValidRoutes() { @@ -138,7 +138,7 @@ func (suite *APIV1TestSuite) TestValidRoutes() { } return http.HandlerFunc(fn) } - suite.mux = NewAPIRouter(opts) + suite.mux = NewAPIRouter(opts, config.TracingConfig{}) routes := []struct { method string @@ -328,7 +328,7 @@ func TestAPIV1TestSuite(t *testing.T) { } func TestNewDefaultAPIV1Router(t *testing.T) { - client := NewDefaultAPIRouter(MockCache{}, config.APIConfig{}, metricsRegistry) + client := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{}, metricsRegistry) assert.NotNil(t, client) } @@ -353,7 +353,7 @@ func TestNewDefaultAPIV1RouterInvalidHandlerConfig(t *testing.T) { EnableNotifications: false, EnableOverrides: false, } - client := NewDefaultAPIRouter(MockCache{}, invalidAPIConfig, metricsRegistry) + client := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{API: invalidAPIConfig}, metricsRegistry) assert.Nil(t, client) } @@ -368,13 +368,12 @@ func TestNewDefaultClientRouterInvalidMiddlewareConfig(t *testing.T) { EnableNotifications: false, EnableOverrides: false, } - client := NewDefaultAPIRouter(MockCache{}, invalidAPIConfig, metricsRegistry) + client := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{API: invalidAPIConfig}, metricsRegistry) assert.Nil(t, client) } func TestForbiddenRoutes(t *testing.T) { - conf := config.APIConfig{} - mux := NewDefaultAPIRouter(MockCache{}, conf, metricsRegistry) + mux := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{}, metricsRegistry) routes := []struct { method string From ddb2e15549aa3c5969245d15ba4e81c2ab884f8e Mon Sep 17 00:00:00 2001 From: pulak-opti <129880418+pulak-opti@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:46:19 +0600 Subject: [PATCH 3/6] fix: fix trace middleware http writer (#401) * improve trace middleware writer * fix failing acceptance test * fix bug --- pkg/middleware/trace.go | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pkg/middleware/trace.go b/pkg/middleware/trace.go index 9b11768f..6caf33d3 100644 --- a/pkg/middleware/trace.go +++ b/pkg/middleware/trace.go @@ -25,6 +25,7 @@ import ( "net/http" "sync" + "github.com/go-chi/chi/v5/middleware" "github.com/optimizely/agent/config" "github.com/rs/zerolog/log" "go.opentelemetry.io/otel" @@ -80,16 +81,6 @@ func (gen *traceIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.S return tid, sid } -type statusRecorder struct { - http.ResponseWriter - statusCode int -} - -func (r *statusRecorder) WriteHeader(code int) { - r.statusCode = code - r.ResponseWriter.WriteHeader(code) -} - func AddTracing(conf config.TracingConfig, tracerName, spanName string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { @@ -107,15 +98,12 @@ func AddTracing(conf config.TracingConfig, tracerName, spanName string) func(htt attribute.String(OptlySDKHeader, r.Header.Get(OptlySDKHeader)), ) - rec := &statusRecorder{ - ResponseWriter: w, - statusCode: http.StatusOK, - } + respWriter := middleware.NewWrapResponseWriter(w, r.ProtoMajor) - next.ServeHTTP(rec, r.WithContext(ctx)) + next.ServeHTTP(respWriter, r.WithContext(ctx)) span.SetAttributes( - semconv.HTTPStatusCodeKey.Int(rec.statusCode), + semconv.HTTPStatusCodeKey.Int(respWriter.Status()), ) } return http.HandlerFunc(fn) From a221e9daa254deb1c68cd552db4f9a2c436adaed Mon Sep 17 00:00:00 2001 From: pulak-opti <129880418+pulak-opti@users.noreply.github.com> Date: Wed, 1 Nov 2023 22:28:24 +0600 Subject: [PATCH 4/6] fix: fix context propagation in distributed tracing (#402) --- cmd/optimizely/main.go | 7 ++-- config.yaml | 7 ++-- config/config.go | 9 +++--- pkg/middleware/trace.go | 63 +++--------------------------------- pkg/middleware/trace_test.go | 47 +-------------------------- pkg/routers/api.go | 33 +++++++++---------- pkg/routers/api_test.go | 12 +++---- 7 files changed, 38 insertions(+), 140 deletions(-) diff --git a/cmd/optimizely/main.go b/cmd/optimizely/main.go index 7586e7dd..1e64396d 100644 --- a/cmd/optimizely/main.go +++ b/cmd/optimizely/main.go @@ -34,7 +34,6 @@ import ( "github.com/optimizely/agent/config" "github.com/optimizely/agent/pkg/metrics" - "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/agent/pkg/optimizely" "github.com/optimizely/agent/pkg/routers" "github.com/optimizely/agent/pkg/server" @@ -50,6 +49,7 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" @@ -151,7 +151,6 @@ func getStdOutTraceProvider(conf config.OTELTracingConfig) (*sdktrace.TracerProv return sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp), sdktrace.WithResource(res), - sdktrace.WithIDGenerator(middleware.NewTraceIDGenerator(conf.TraceIDHeaderKey)), ), nil } @@ -199,7 +198,6 @@ func getRemoteTraceProvider(conf config.OTELTracingConfig) (*sdktrace.TracerProv sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(conf.Services.Remote.SampleRate))), sdktrace.WithResource(res), sdktrace.WithSpanProcessor(bsp), - sdktrace.WithIDGenerator(middleware.NewTraceIDGenerator(conf.TraceIDHeaderKey)), ), nil } @@ -246,6 +244,7 @@ func main() { } }() otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.TraceContext{}) log.Info().Msg(fmt.Sprintf("Tracing enabled with service %q", conf.Tracing.OpenTelemetry.Default)) } else { log.Info().Msg("Tracing disabled") @@ -275,7 +274,7 @@ func main() { cancel() }() - apiRouter := routers.NewDefaultAPIRouter(optlyCache, *conf, agentMetricsRegistry) + apiRouter := routers.NewDefaultAPIRouter(optlyCache, conf.API, agentMetricsRegistry) adminRouter := routers.NewAdminRouter(*conf) log.Info().Str("version", conf.Version).Msg("Starting services.") diff --git a/config.yaml b/config.yaml index 56f8db7c..b8b85779 100644 --- a/config.yaml +++ b/config.yaml @@ -29,6 +29,9 @@ log: ## ## tracing: tracing configuration ## +## For distributed tracing, trace context should be sent on "traceparent" header +## The value set in HTTP Header must be a hex compliant with the W3C trace-context specification. +## See more at https://www.w3.org/TR/trace-context/#trace-id tracing: ## bydefault tracing is disabled ## to enable tracing set enabled to true @@ -43,10 +46,6 @@ tracing: ## tracing environment name ## example: for production environment env can be set as "prod" env: "dev" - ## HTTP Header Key for TraceID in Distributed Tracing - ## The value set in HTTP Header must be a hex compliant with the W3C trace-context specification. - ## See more at https://www.w3.org/TR/trace-context/#trace-id - traceIDHeaderKey: "X-Optimizely-Trace-ID" ## tracing service configuration services: ## stdout exporter configuration diff --git a/config/config.go b/config/config.go index b0412dc5..4155b3ef 100644 --- a/config/config.go +++ b/config/config.go @@ -197,11 +197,10 @@ const ( ) type OTELTracingConfig struct { - Default TracingServiceType `json:"default"` - ServiceName string `json:"serviceName"` - Env string `json:"env"` - TraceIDHeaderKey string `json:"traceIDHeaderKey"` - Services TracingServiceConfig `json:"services"` + Default TracingServiceType `json:"default"` + ServiceName string `json:"serviceName"` + Env string `json:"env"` + Services TracingServiceConfig `json:"services"` } type TracingServiceConfig struct { diff --git a/pkg/middleware/trace.go b/pkg/middleware/trace.go index 6caf33d3..e5270230 100644 --- a/pkg/middleware/trace.go +++ b/pkg/middleware/trace.go @@ -18,75 +18,22 @@ package middleware import ( - "context" - crand "crypto/rand" - "encoding/binary" - "math/rand" "net/http" - "sync" "github.com/go-chi/chi/v5/middleware" - "github.com/optimizely/agent/config" - "github.com/rs/zerolog/log" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" - "go.opentelemetry.io/otel/trace" ) -type traceIDGenerator struct { - sync.Mutex - randSource *rand.Rand - traceIDHeaderKey string -} - -func NewTraceIDGenerator(traceIDHeaderKey string) *traceIDGenerator { - var rngSeed int64 - _ = binary.Read(crand.Reader, binary.LittleEndian, &rngSeed) - return &traceIDGenerator{ - randSource: rand.New(rand.NewSource(rngSeed)), - traceIDHeaderKey: traceIDHeaderKey, - } -} - -func (gen *traceIDGenerator) NewSpanID(ctx context.Context, traceID trace.TraceID) trace.SpanID { - gen.Lock() - defer gen.Unlock() - sid := trace.SpanID{} - _, _ = gen.randSource.Read(sid[:]) - return sid -} - -func (gen *traceIDGenerator) NewIDs(ctx context.Context) (trace.TraceID, trace.SpanID) { - gen.Lock() - defer gen.Unlock() - tid := trace.TraceID{} - _, _ = gen.randSource.Read(tid[:]) - sid := trace.SpanID{} - _, _ = gen.randSource.Read(sid[:]) - - // read trace id from header if provided - traceIDHeader := ctx.Value(gen.traceIDHeaderKey) - if val, ok := traceIDHeader.(string); ok { - if val != "" { - headerTraceId, err := trace.TraceIDFromHex(val) - if err == nil { - tid = headerTraceId - } else { - log.Error().Err(err).Msg("failed to parse trace id from header, invalid trace id") - } - } - } - - return tid, sid -} - -func AddTracing(conf config.TracingConfig, tracerName, spanName string) func(http.Handler) http.Handler { +func AddTracing(tracerName, spanName string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - pctx := context.WithValue(r.Context(), conf.OpenTelemetry.TraceIDHeaderKey, r.Header.Get(conf.OpenTelemetry.TraceIDHeaderKey)) + prop := otel.GetTextMapPropagator() + propCtx := prop.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) - ctx, span := otel.Tracer(tracerName).Start(pctx, spanName) + ctx, span := otel.Tracer(tracerName).Start(propCtx, spanName) defer span.End() span.SetAttributes( diff --git a/pkg/middleware/trace_test.go b/pkg/middleware/trace_test.go index 7c747947..91e15e6d 100644 --- a/pkg/middleware/trace_test.go +++ b/pkg/middleware/trace_test.go @@ -18,14 +18,9 @@ package middleware import ( - "context" "net/http" "net/http/httptest" "testing" - - "github.com/optimizely/agent/config" - "github.com/stretchr/testify/assert" - "go.opentelemetry.io/otel/trace" ) func TestAddTracing(t *testing.T) { @@ -37,7 +32,7 @@ func TestAddTracing(t *testing.T) { req := httptest.NewRequest("GET", "/", nil) rr := httptest.NewRecorder() - middleware := http.Handler(AddTracing(config.TracingConfig{}, "test-tracer", "test-span")(handler)) + middleware := http.Handler(AddTracing("test-tracer", "test-span")(handler)) // Serve the request through the middleware middleware.ServeHTTP(rr, req) @@ -54,43 +49,3 @@ func TestAddTracing(t *testing.T) { t.Errorf("Expected Content-Type header %v, but got %v", "application/text", typeHeader) } } - -func TestNewIDs(t *testing.T) { - gen := NewTraceIDGenerator("") - n := 1000 - - for i := 0; i < n; i++ { - traceID, spanID := gen.NewIDs(context.Background()) - assert.Truef(t, traceID.IsValid(), "trace id: %s", traceID.String()) - assert.Truef(t, spanID.IsValid(), "span id: %s", spanID.String()) - } -} - -func TestNewSpanID(t *testing.T) { - gen := NewTraceIDGenerator("") - testTraceID := [16]byte{123, 123} - n := 1000 - - for i := 0; i < n; i++ { - spanID := gen.NewSpanID(context.Background(), testTraceID) - assert.Truef(t, spanID.IsValid(), "span id: %s", spanID.String()) - } -} - -func TestNewSpanIDWithInvalidTraceID(t *testing.T) { - gen := NewTraceIDGenerator("") - spanID := gen.NewSpanID(context.Background(), trace.TraceID{}) - assert.Truef(t, spanID.IsValid(), "span id: %s", spanID.String()) -} - -func TestTraceIDWithGivenHeaderValue(t *testing.T) { - traceHeader := "X-Trace-ID" - traceID := "9b8eac67e332c6f8baf1e013de6891bb" - - gen := NewTraceIDGenerator(traceHeader) - - ctx := context.WithValue(context.Background(), traceHeader, traceID) - genTraceID, _ := gen.NewIDs(ctx) - assert.Truef(t, genTraceID.IsValid(), "trace id: %s", genTraceID.String()) - assert.Equal(t, traceID, genTraceID.String()) -} diff --git a/pkg/routers/api.go b/pkg/routers/api.go index 88897df6..b7d2aac5 100644 --- a/pkg/routers/api.go +++ b/pkg/routers/api.go @@ -63,8 +63,7 @@ func forbiddenHandler(message string) http.HandlerFunc { } // NewDefaultAPIRouter creates a new router with the default backing optimizely.Cache -func NewDefaultAPIRouter(optlyCache optimizely.Cache, agentConf config.AgentConfig, metricsRegistry *metrics.Registry) http.Handler { - conf := agentConf.API +func NewDefaultAPIRouter(optlyCache optimizely.Cache, conf config.APIConfig, metricsRegistry *metrics.Registry) http.Handler { authProvider := middleware.NewAuth(&conf.Auth) if authProvider == nil { log.Error().Msg("unable to initialize api auth middleware.") @@ -109,19 +108,19 @@ func NewDefaultAPIRouter(optlyCache optimizely.Cache, agentConf config.AgentConf corsHandler: corsHandler, } - return NewAPIRouter(spec, agentConf.Tracing) + return NewAPIRouter(spec) } // NewAPIRouter returns HTTP API router backed by an optimizely.Cache implementation -func NewAPIRouter(opt *APIOptions, traceConf config.TracingConfig) *chi.Mux { +func NewAPIRouter(opt *APIOptions) *chi.Mux { r := chi.NewRouter() - WithAPIRouter(opt, r, traceConf) + WithAPIRouter(opt, r) return r } // WithAPIRouter appends routes and middleware to the given router. // See https://godoc.org/github.com/go-chi/chi/v5#Mux.Group for usage -func WithAPIRouter(opt *APIOptions, r chi.Router, traceConf config.TracingConfig) { +func WithAPIRouter(opt *APIOptions, r chi.Router) { getConfigTimer := middleware.Metricize("get-config", opt.metricsRegistry) getDatafileTimer := middleware.Metricize("get-datafile", opt.metricsRegistry) activateTimer := middleware.Metricize("activate", opt.metricsRegistry) @@ -134,17 +133,17 @@ func WithAPIRouter(opt *APIOptions, r chi.Router, traceConf config.TracingConfig createAccesstokenTimer := middleware.Metricize("create-api-access-token", opt.metricsRegistry) contentTypeMiddleware := chimw.AllowContentType("application/json") - configTracer := middleware.AddTracing(traceConf, "configHandler", "OptimizelyConfig") - datafileTracer := middleware.AddTracing(traceConf, "datafileHandler", "OptimizelyDatafile") - activateTracer := middleware.AddTracing(traceConf, "activateHandler", "Activate") - decideTracer := middleware.AddTracing(traceConf, "decideHandler", "Decide") - trackTracer := middleware.AddTracing(traceConf, "trackHandler", "Track") - overrideTracer := middleware.AddTracing(traceConf, "overrideHandler", "Override") - lookupTracer := middleware.AddTracing(traceConf, "lookupHandler", "Lookup") - saveTracer := middleware.AddTracing(traceConf, "saveHandler", "Save") - sendOdpEventTracer := middleware.AddTracing(traceConf, "sendOdpEventHandler", "SendOdpEvent") - nStreamTracer := middleware.AddTracing(traceConf, "notificationHandler", "SendNotificationEvent") - authTracer := middleware.AddTracing(traceConf, "authHandler", "AuthToken") + configTracer := middleware.AddTracing("configHandler", "OptimizelyConfig") + datafileTracer := middleware.AddTracing("datafileHandler", "OptimizelyDatafile") + activateTracer := middleware.AddTracing("activateHandler", "Activate") + decideTracer := middleware.AddTracing("decideHandler", "Decide") + trackTracer := middleware.AddTracing("trackHandler", "Track") + overrideTracer := middleware.AddTracing("overrideHandler", "Override") + lookupTracer := middleware.AddTracing("lookupHandler", "Lookup") + saveTracer := middleware.AddTracing("saveHandler", "Save") + sendOdpEventTracer := middleware.AddTracing("sendOdpEventHandler", "SendOdpEvent") + nStreamTracer := middleware.AddTracing("notificationHandler", "SendNotificationEvent") + authTracer := middleware.AddTracing("authHandler", "AuthToken") if opt.maxConns > 0 { // Note this is NOT a rate limiter, but a concurrency threshold diff --git a/pkg/routers/api_test.go b/pkg/routers/api_test.go index 9196d880..bc7776e9 100644 --- a/pkg/routers/api_test.go +++ b/pkg/routers/api_test.go @@ -126,7 +126,7 @@ func (suite *APIV1TestSuite) SetupTest() { corsHandler: testCorsHandler, } - suite.mux = NewAPIRouter(opts, config.TracingConfig{}) + suite.mux = NewAPIRouter(opts) } func (suite *APIV1TestSuite) TestValidRoutes() { @@ -138,7 +138,7 @@ func (suite *APIV1TestSuite) TestValidRoutes() { } return http.HandlerFunc(fn) } - suite.mux = NewAPIRouter(opts, config.TracingConfig{}) + suite.mux = NewAPIRouter(opts) routes := []struct { method string @@ -328,7 +328,7 @@ func TestAPIV1TestSuite(t *testing.T) { } func TestNewDefaultAPIV1Router(t *testing.T) { - client := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{}, metricsRegistry) + client := NewDefaultAPIRouter(MockCache{}, config.APIConfig{}, metricsRegistry) assert.NotNil(t, client) } @@ -353,7 +353,7 @@ func TestNewDefaultAPIV1RouterInvalidHandlerConfig(t *testing.T) { EnableNotifications: false, EnableOverrides: false, } - client := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{API: invalidAPIConfig}, metricsRegistry) + client := NewDefaultAPIRouter(MockCache{}, invalidAPIConfig, metricsRegistry) assert.Nil(t, client) } @@ -368,12 +368,12 @@ func TestNewDefaultClientRouterInvalidMiddlewareConfig(t *testing.T) { EnableNotifications: false, EnableOverrides: false, } - client := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{API: invalidAPIConfig}, metricsRegistry) + client := NewDefaultAPIRouter(MockCache{}, invalidAPIConfig, metricsRegistry) assert.Nil(t, client) } func TestForbiddenRoutes(t *testing.T) { - mux := NewDefaultAPIRouter(MockCache{}, config.AgentConfig{}, metricsRegistry) + mux := NewDefaultAPIRouter(MockCache{}, config.APIConfig{}, metricsRegistry) routes := []struct { method string From 054b50df99ae99360d30b5f2eb50a5b333605ef0 Mon Sep 17 00:00:00 2001 From: pulak-opti <129880418+pulak-opti@users.noreply.github.com> Date: Tue, 12 Dec 2023 22:45:39 +0600 Subject: [PATCH 5/6] [FSSDK-9840] chore: add traceID & spanID to logs (#407) * add logging for decide api * log traceid for trace api * log traceid for override api * add traceid for error log * add traceid into logs * clean up * clean up * update unit test --- pkg/handlers/activate.go | 9 +++++---- pkg/handlers/decide.go | 7 ++++--- pkg/handlers/decide_test.go | 9 ++++----- pkg/handlers/get_datafile.go | 4 ++++ pkg/handlers/lookup.go | 7 +++++-- pkg/handlers/optimizely_config.go | 3 +++ pkg/handlers/override.go | 2 +- pkg/handlers/save.go | 7 +++++-- pkg/handlers/send_odp_event.go | 5 ++++- pkg/handlers/track.go | 5 +++-- pkg/handlers/utils.go | 10 ++++++---- pkg/middleware/utils.go | 14 ++++++++++---- pkg/middleware/utils_test.go | 20 ++++++++++++++++++-- 13 files changed, 72 insertions(+), 30 deletions(-) diff --git a/pkg/handlers/activate.go b/pkg/handlers/activate.go index 4a48f90a..2768f919 100644 --- a/pkg/handlers/activate.go +++ b/pkg/handlers/activate.go @@ -22,13 +22,12 @@ import ( "fmt" "net/http" + "github.com/go-chi/render" + "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/agent/pkg/optimizely" - "github.com/optimizely/go-sdk/pkg/config" "github.com/optimizely/go-sdk/pkg/entities" - - "github.com/go-chi/render" ) type keyMap map[string]string @@ -42,12 +41,13 @@ type ActivateBody struct { // Activate makes feature and experiment decisions for the selected query parameters. func Activate(w http.ResponseWriter, r *http.Request) { optlyClient, err := middleware.GetOptlyClient(r) - logger := middleware.GetLogger(r) if err != nil { RenderError(err, http.StatusInternalServerError, w, r) return } + logger := middleware.GetLogger(r) + uc, err := getUserContext(r) if err != nil { RenderError(err, http.StatusBadRequest, w, r) @@ -108,6 +108,7 @@ func Activate(w http.ResponseWriter, r *http.Request) { } decisions = filterDecisions(r, decisions) + logger.Info().Msgf("Made activate decisions for user %s", uc.ID) render.JSON(w, r, decisions) } diff --git a/pkg/handlers/decide.go b/pkg/handlers/decide.go index 15331b70..021645d2 100644 --- a/pkg/handlers/decide.go +++ b/pkg/handlers/decide.go @@ -21,14 +21,13 @@ import ( "errors" "net/http" - "github.com/optimizely/agent/pkg/middleware" + "github.com/go-chi/render" + "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/go-sdk/pkg/client" "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" "github.com/optimizely/go-sdk/pkg/odp/segment" - - "github.com/go-chi/render" ) // DecideBody defines the request body for decide API @@ -108,6 +107,7 @@ func Decide(w http.ResponseWriter, r *http.Request) { key := keys[0] logger.Debug().Str("featureKey", key).Msg("fetching feature decision") d := optimizelyUserContext.Decide(key, decideOptions) + logger.Info().Msgf("Feature %q is enabled for user %s? %t", d.FlagKey, d.UserContext.UserID, d.Enabled) decideOut := DecideOut{d, d.Variables.ToMap()} render.JSON(w, r, decideOut) return @@ -120,6 +120,7 @@ func Decide(w http.ResponseWriter, r *http.Request) { for _, d := range decides { decideOut := DecideOut{d, d.Variables.ToMap()} decideOuts = append(decideOuts, decideOut) + logger.Info().Msgf("Feature %q is enabled for user %s? %t", d.FlagKey, d.UserContext.UserID, d.Enabled) } render.JSON(w, r, decideOuts) } diff --git a/pkg/handlers/decide_test.go b/pkg/handlers/decide_test.go index c047da4d..7eef55c2 100644 --- a/pkg/handlers/decide_test.go +++ b/pkg/handlers/decide_test.go @@ -26,18 +26,17 @@ import ( "net/http/httptest" "testing" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/agent/pkg/optimizely" "github.com/optimizely/agent/pkg/optimizely/optimizelytest" - "github.com/optimizely/go-sdk/pkg/client" "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/odp/segment" - - "github.com/go-chi/chi/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" ) type DecideTestSuite struct { diff --git a/pkg/handlers/get_datafile.go b/pkg/handlers/get_datafile.go index 368374ba..d458cb1b 100644 --- a/pkg/handlers/get_datafile.go +++ b/pkg/handlers/get_datafile.go @@ -34,11 +34,15 @@ func GetDatafile(w http.ResponseWriter, r *http.Request) { return } + logger := middleware.GetLogger(r) + datafile := optlyClient.GetOptimizelyConfig().GetDatafile() var raw map[string]interface{} if err = json.Unmarshal([]byte(datafile), &raw); err != nil { RenderError(err, http.StatusInternalServerError, w, r) return } + + logger.Info().Msg("Successfully returned datafile") render.JSON(w, r, raw) } diff --git a/pkg/handlers/lookup.go b/pkg/handlers/lookup.go index 0682d298..30298d2c 100644 --- a/pkg/handlers/lookup.go +++ b/pkg/handlers/lookup.go @@ -21,9 +21,9 @@ import ( "errors" "net/http" - "github.com/optimizely/agent/pkg/middleware" - "github.com/go-chi/render" + + "github.com/optimizely/agent/pkg/middleware" ) type lookupBody struct { @@ -47,6 +47,8 @@ func Lookup(w http.ResponseWriter, r *http.Request) { return } + logger := middleware.GetLogger(r) + if optlyClient.UserProfileService == nil { RenderError(ErrNoUPS, http.StatusInternalServerError, w, r) return @@ -75,5 +77,6 @@ func Lookup(w http.ResponseWriter, r *http.Request) { experimentBucketMap[k.ExperimentID] = map[string]interface{}{k.Field: v} } lookupResponse.ExperimentBucketMap = experimentBucketMap + logger.Info().Msgf("Looked up user profile for user %s", body.UserID) render.JSON(w, r, lookupResponse) } diff --git a/pkg/handlers/optimizely_config.go b/pkg/handlers/optimizely_config.go index 1659cbf5..5ad19a4d 100644 --- a/pkg/handlers/optimizely_config.go +++ b/pkg/handlers/optimizely_config.go @@ -33,6 +33,9 @@ func OptimizelyConfig(w http.ResponseWriter, r *http.Request) { return } + logger := middleware.GetLogger(r) + conf := optlyClient.GetOptimizelyConfig() + logger.Info().Msg("Successfully returned OptimizelyConfig") render.JSON(w, r, conf) } diff --git a/pkg/handlers/override.go b/pkg/handlers/override.go index dff9aa9e..36cb3ec4 100644 --- a/pkg/handlers/override.go +++ b/pkg/handlers/override.go @@ -69,10 +69,10 @@ func Override(w http.ResponseWriter, r *http.Request) { return } - logger.Debug().Str("experimentKey", experimentKey).Str("variationKey", body.VariationKey).Msg("setting override") if override, err := optlyClient.SetForcedVariation(r.Context(), experimentKey, body.UserID, body.VariationKey); err != nil { RenderError(err, http.StatusInternalServerError, w, r) } else { + logger.Info().Str("experimentKey", experimentKey).Str("variationKey", body.VariationKey).Msg("Successfully set override") render.JSON(w, r, override) } } diff --git a/pkg/handlers/save.go b/pkg/handlers/save.go index 7d7903ce..2281aab7 100644 --- a/pkg/handlers/save.go +++ b/pkg/handlers/save.go @@ -20,10 +20,10 @@ package handlers import ( "net/http" + "github.com/go-chi/render" + "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/go-sdk/pkg/decision" - - "github.com/go-chi/render" ) type saveBody struct { @@ -38,6 +38,8 @@ func Save(w http.ResponseWriter, r *http.Request) { return } + logger := middleware.GetLogger(r) + if optlyClient.UserProfileService == nil { RenderError(ErrNoUPS, http.StatusInternalServerError, w, r) return @@ -58,6 +60,7 @@ func Save(w http.ResponseWriter, r *http.Request) { convertedProfile := convertToUserProfile(body) optlyClient.UserProfileService.Save(convertedProfile) + logger.Info().Msgf("Saved user profile for user %s", body.UserID) render.Status(r, http.StatusOK) } diff --git a/pkg/handlers/send_odp_event.go b/pkg/handlers/send_odp_event.go index d9fcfae3..91fdeb68 100644 --- a/pkg/handlers/send_odp_event.go +++ b/pkg/handlers/send_odp_event.go @@ -22,10 +22,10 @@ import ( "net/http" "github.com/go-chi/render" - "github.com/optimizely/go-sdk/pkg/odp/event" "github.com/optimizely/agent/pkg/middleware" "github.com/optimizely/agent/pkg/optimizely" + "github.com/optimizely/go-sdk/pkg/odp/event" ) // SendOdpEvent sends event to ODP platform @@ -36,6 +36,8 @@ func SendOdpEvent(w http.ResponseWriter, r *http.Request) { return } + logger := middleware.GetLogger(r) + body, err := getRequestOdpEvent(r) if err != nil { RenderError(err, http.StatusBadRequest, w, r) @@ -52,6 +54,7 @@ func SendOdpEvent(w http.ResponseWriter, r *http.Request) { Success: true, } + logger.Info().Msg("Successfully sent event to ODP platform") render.JSON(w, r, returnResult) } diff --git a/pkg/handlers/track.go b/pkg/handlers/track.go index 61e40c7b..010431df 100644 --- a/pkg/handlers/track.go +++ b/pkg/handlers/track.go @@ -22,9 +22,9 @@ import ( "net/http" "github.com/go-chi/render" - "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/agent/pkg/middleware" + "github.com/optimizely/go-sdk/pkg/entities" ) type trackBody struct { @@ -40,6 +40,7 @@ func TrackEvent(w http.ResponseWriter, r *http.Request) { RenderError(err, http.StatusInternalServerError, w, r) return } + logger := middleware.GetLogger(r) var body trackBody err = ParseRequestBody(r, &body) @@ -66,6 +67,6 @@ func TrackEvent(w http.ResponseWriter, r *http.Request) { return } - middleware.GetLogger(r).Debug().Str("eventKey", eventKey).Msg("tracking event") + logger.Info().Str("eventKey", eventKey).Msg("tracked event") render.JSON(w, r, track) } diff --git a/pkg/handlers/utils.go b/pkg/handlers/utils.go index 82605cc7..e632fb1c 100644 --- a/pkg/handlers/utils.go +++ b/pkg/handlers/utils.go @@ -35,7 +35,7 @@ type ErrorResponse struct { // RenderError sets the request status and renders the error message. func RenderError(err error, status int, w http.ResponseWriter, r *http.Request) { - middleware.GetLogger(r).Info().Err(err).Int("status", status).Msg("render error") + middleware.GetLogger(r).Err(err).Int("status", status).Msg("render error") render.Status(r, status) render.JSON(w, r, ErrorResponse{Error: err.Error()}) } @@ -44,22 +44,24 @@ func RenderError(err error, status int, w http.ResponseWriter, r *http.Request) // into the provided interface. Note that we're sanitizing the returned error // so that it is not leaked back to the requestor. func ParseRequestBody(r *http.Request, v interface{}) error { + logger := middleware.GetLogger(r) + body, err := io.ReadAll(r.Body) if err != nil { msg := "error reading request body" - middleware.GetLogger(r).Error().Err(err).Msg(msg) + logger.Err(err).Msg(msg) return fmt.Errorf("%s", msg) } if len(body) == 0 { - middleware.GetLogger(r).Debug().Msg("body was empty skip JSON unmarshal") + logger.Info().Msg("body was empty skip JSON unmarshal") return nil } err = json.Unmarshal(body, &v) if err != nil { msg := "error parsing request body" - middleware.GetLogger(r).Error().Err(err).Msg(msg) + logger.Err(err).Msg(msg) return fmt.Errorf("%s", msg) } diff --git a/pkg/middleware/utils.go b/pkg/middleware/utils.go index 24940c83..cf45b041 100644 --- a/pkg/middleware/utils.go +++ b/pkg/middleware/utils.go @@ -23,13 +23,13 @@ import ( "strconv" "strings" - "github.com/optimizely/agent/pkg/optimizely" - - "github.com/optimizely/go-sdk/pkg/config" - "github.com/go-chi/render" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel/trace" + + "github.com/optimizely/agent/pkg/optimizely" + "github.com/optimizely/go-sdk/pkg/config" ) // ErrorResponse Model @@ -52,6 +52,12 @@ func GetLogger(r *http.Request) *zerolog.Logger { reqID := r.Header.Get(OptlyRequestHeader) logger := log.With().Str("requestId", reqID).Logger() + span := trace.SpanFromContext(r.Context()) + if span.SpanContext().TraceID().IsValid() { + logger = logger.With().Str("traceId", span.SpanContext().TraceID().String()).Logger() + logger = logger.With().Str("spanId", span.SpanContext().SpanID().String()).Logger() + } + if optimizely.ShouldIncludeSDKKey { sdkKey := r.Header.Get(OptlySDKHeader) sdkKeySplit := strings.Split(sdkKey, ":") diff --git a/pkg/middleware/utils_test.go b/pkg/middleware/utils_test.go index 8418e4a2..799477a3 100644 --- a/pkg/middleware/utils_test.go +++ b/pkg/middleware/utils_test.go @@ -21,13 +21,15 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" "github.com/optimizely/agent/pkg/optimizely" - "github.com/optimizely/go-sdk/pkg/config" ) @@ -54,15 +56,26 @@ func TestGetLogger(t *testing.T) { out := &bytes.Buffer{} req := httptest.NewRequest("GET", "/", nil) + traceId := "0af7651916cd43dd8448eb211c80319c" + spanId := "b9c7c989f97918e1" + req.Header.Set(OptlyRequestHeader, "12345") req.Header.Set(OptlySDKHeader, "some_key") - logger := GetLogger(req) + req.Header.Set("traceparent", fmt.Sprintf("00-%s-%s-01", traceId, spanId)) + + otel.SetTextMapPropagator(propagation.TraceContext{}) + ctx := otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header)) + + logger := GetLogger(req.WithContext(ctx)) newLogger := logger.Output(out) newLogger.Info().Msg("some_message") assert.Contains(t, out.String(), `"requestId":"12345"`) assert.Contains(t, out.String(), `"sdkKey":"some_key"`) + assert.Contains(t, out.String(), `"traceId":"`+traceId+`"`) + assert.Contains(t, out.String(), `"spanId":"`+spanId+`"`) + optimizely.ShouldIncludeSDKKey = false out = &bytes.Buffer{} logger = GetLogger(req) @@ -70,6 +83,9 @@ func TestGetLogger(t *testing.T) { newLogger.Info().Msg("some_message") assert.Contains(t, out.String(), `"requestId":"12345"`) assert.NotContains(t, out.String(), `"sdkKey":"some_key"`) + + assert.NotContains(t, out.String(), `"traceId":"`+traceId+`"`) + assert.NotContains(t, out.String(), `"spanId":"`+spanId+`"`) } func TestGetFeature(t *testing.T) { From ba168d15aa0e2c3532628bb7056389113434e0fd Mon Sep 17 00:00:00 2001 From: pulak-opti <129880418+pulak-opti@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:24:21 +0600 Subject: [PATCH 6/6] [FSSDK-9840] chore: prepare for 3.2.0 release (#409) --- CHANGELOG.md | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d0218eb..f429b7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,51 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [3.0.1] - March 14, 2023 +## [3.2.0] - December 13, 2023 + +### New Features + +- Added support for including `traceId` and `spanId` into the logs. ([#407](https://github.com/optimizely/agent/pull/407)) + +## [3.1.0] - November 3, 2023 + +### New Features + +- Added support for Prometheus-based metrics alongside expvar metrics. This can be configured from the config.yaml file. ([#348](https://github.com/optimizely/agent/pull/348)) + +- Added support for OpenTelemetry tracing. Distributed tracing is also supported according to [W3C TraceContext](https://www.w3.org/TR/trace-context/). ([#400](https://github.com/optimizely/agent/pull/400), [#401](https://github.com/optimizely/agent/pull/401), [#402](https://github.com/optimizely/agent/pull/402)) + +## [4.0.0-beta] - May 11, 2023 + +### New Features + +The 4.0.0-beta release introduces a new primary feature, [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) ([#356](https://github.com/optimizely/agent/pull/356), [#364](https://github.com/optimizely/agent/pull/364), [#365](https://github.com/optimizely/agent/pull/365), [#366](https://github.com/optimizely/agent/pull/366)). + +You can use ODP, a high-performance [Customer Data Platform (CDP)](https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool. + +With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager. + +This version includes the following changes: + +- `FetchQualifiedSegments()` API has been added to the `/decide` endpoint. This API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. Fetched data will be stored in the local cache to avoid repeated network delays. + +- `SendOdpEvent()` API has been added with the `/send-opd-event` endpoint. Customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. + +For details, refer to our documentation pages: + +* [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) + +* [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-for-server-side-sdks) + +* [Use Optimizely Agent](https://docs.developers.optimizely.com/feature-experimentation/docs/use-optimizely-agent) + +* [Configure Optimizely Agent](https://docs.developers.optimizely.com/feature-experimentation/docs/configure-optimizely-agent) + +### Breaking Changes + +- ODPManager in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most ODPManager functions will be ignored. If needed, ODPManager can be disabled when OptimizelyClient is instantiated. From Agent, it can be switched off from config.yaml or env variables. + +## [3.0.1] - March 16, 2023 - Update README.md and other non-functional code to reflect that this SDK supports both Optimizely Feature Experimentation and Optimizely Full Stack. ([#369](https://github.com/optimizely/agent/pull/369)).