From 80241359d5d604c9ccc956fe7d7eaa0fd9b8682d Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 29 Jul 2024 14:34:45 +0200 Subject: [PATCH] * feat: New Metrics metadata is included in client registration and client metrics --- client.go | 1 + client_test.go | 10 +- config.go | 1 + example_bootstrap_from_file_test.go | 2 +- go.mod | 4 +- go.sum | 6 +- .../strategies/gradual_rollout_random_test.go | 1 + .../gradual_rollout_session_id_test.go | 1 + .../gradual_rollout_user_id_test.go | 1 + metrics.go | 40 ++++- metrics_test.go | 168 +++++++++++++++++- repository_test.go | 28 +-- spec_test.go | 2 +- unleash_test.go | 2 +- 14 files changed, 237 insertions(+), 30 deletions(-) diff --git a/client.go b/client.go index f45c385..2cb564f 100644 --- a/client.go +++ b/client.go @@ -18,6 +18,7 @@ const ( deprecatedSuffix = "/features" clientName = "unleash-client-go" clientVersion = "4.1.1" + specVersion = "4.3.1" ) var defaultStrategies = []strategy.Strategy{ diff --git a/client_test.go b/client_test.go index 64f50b8..4e3ba26 100644 --- a/client_test.go +++ b/client_test.go @@ -10,8 +10,8 @@ import ( "testing" + "github.com/h2non/gock" "github.com/stretchr/testify/assert" - "gopkg.in/h2non/gock.v1" ) func TestClientWithoutListener(t *testing.T) { @@ -1399,12 +1399,12 @@ func TestGetVariant_FallbackVariantFeatureEnabledSettingIsLeftUnchanged(t *testi client.WaitForReady() fallbackVariantFeatureEnabled := api.Variant{ - Name: "fallback-variant-feature-enabled", - FeatureEnabled: true, + Name: "fallback-variant-feature-enabled", + FeatureEnabled: true, } fallbackVariantFeatureDisabled := api.Variant{ - Name: "fallback-variant-feature-disabled", - FeatureEnabled: false, + Name: "fallback-variant-feature-disabled", + FeatureEnabled: false, } variantForEnabledFeatureNoVariants := client.GetVariant(enabledFeatureNoVariants, WithVariantFallback(&fallbackVariantFeatureDisabled)) diff --git a/config.go b/config.go index 9ed7ddc..8fb1a40 100644 --- a/config.go +++ b/config.go @@ -265,4 +265,5 @@ type metricsOptions struct { disableMetrics bool httpClient *http.Client customHeaders http.Header + started *time.Time } diff --git a/example_bootstrap_from_file_test.go b/example_bootstrap_from_file_test.go index 8dcf3f6..02a38cb 100644 --- a/example_bootstrap_from_file_test.go +++ b/example_bootstrap_from_file_test.go @@ -8,8 +8,8 @@ import ( "github.com/Unleash/unleash-client-go/v4" "github.com/Unleash/unleash-client-go/v4/context" + "github.com/h2non/gock" "github.com/stretchr/testify/assert" - "gopkg.in/h2non/gock.v1" ) func Test_bootstrapFromFile(t *testing.T) { diff --git a/go.mod b/go.mod index b4b9962..6c6deb3 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/Unleash/unleash-client-go/v4 require ( github.com/Masterminds/semver/v3 v3.1.1 github.com/davecgh/go-spew v1.1.1 // indirect - github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect + github.com/h2non/gock v1.2.0 + github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.1.1 // indirect github.com/stretchr/testify v1.2.2 github.com/twmb/murmur3 v1.1.8 - gopkg.in/h2non/gock.v1 v1.0.10 ) go 1.13 diff --git a/go.sum b/go.sum index 61d9218..f593884 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= +github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -12,5 +16,3 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -gopkg.in/h2non/gock.v1 v1.0.10 h1:D4j796HhgidcxF0LnDyFXcoEbEZWoLEWf0kRh61p22w= -gopkg.in/h2non/gock.v1 v1.0.10/go.mod h1:KHI4Z1sxDW6P4N3DfTWSEza07YpkQP7KJBfglRMEjKY= diff --git a/internal/strategies/gradual_rollout_random_test.go b/internal/strategies/gradual_rollout_random_test.go index a0e9790..1d3a78f 100644 --- a/internal/strategies/gradual_rollout_random_test.go +++ b/internal/strategies/gradual_rollout_random_test.go @@ -1,3 +1,4 @@ +//go:build norace // +build norace package strategies diff --git a/internal/strategies/gradual_rollout_session_id_test.go b/internal/strategies/gradual_rollout_session_id_test.go index 0d9ef0c..949e760 100644 --- a/internal/strategies/gradual_rollout_session_id_test.go +++ b/internal/strategies/gradual_rollout_session_id_test.go @@ -1,3 +1,4 @@ +//go:build norace // +build norace package strategies diff --git a/internal/strategies/gradual_rollout_user_id_test.go b/internal/strategies/gradual_rollout_user_id_test.go index d14d58e..c21fdc9 100644 --- a/internal/strategies/gradual_rollout_user_id_test.go +++ b/internal/strategies/gradual_rollout_user_id_test.go @@ -1,3 +1,4 @@ +//go:build norace // +build norace package strategies diff --git a/metrics.go b/metrics.go index 94eb178..3788eb8 100644 --- a/metrics.go +++ b/metrics.go @@ -8,6 +8,7 @@ import ( "math" "net/http" "net/url" + "runtime" "sync" "time" @@ -24,6 +25,21 @@ type MetricsData struct { // Bucket is the payload data sent to the server. Bucket api.Bucket `json:"bucket"` + + // The runtime version of our Platform + PlatformVersion string `json:"platformVersion"` + + // The runtime name of our Platform + PlatformName string `json:"platformName"` + + // Which version of Yggdrasil is being used + YggdrasilVersion *string `json:"yggdrasilVersion"` + + // Optional field that describes the sdk version (name:version) + SDKVersion string `json:"sdkVersion"` + + // Which version of the Unleash-Client-Spec is this SDK validated against + SpecVersion string `json:"specVersion"` } // ClientData represents the data sent to the unleash during registration. @@ -46,6 +62,15 @@ type ClientData struct { // Interval specifies the time interval (in ms) that the client is using for refreshing // feature toggles. Interval int64 `json:"interval"` + + PlatformVersion string `json:"platformVersion"` + + PlatformName string `json:"platformName"` + + YggdrasilVersion *string `json:"yggdrasilVersion"` + + // Which version of the Unleash-Client-Spec is this SDK validated against + SpecVersion string `json:"specVersion"` } type metric struct { @@ -174,9 +199,14 @@ func (m *metrics) sendMetrics() { } bucket.Stop = time.Now() payload := MetricsData{ - AppName: m.options.appName, - InstanceID: m.options.instanceId, - Bucket: bucket, + AppName: m.options.appName, + InstanceID: m.options.instanceId, + Bucket: bucket, + SDKVersion: fmt.Sprintf("%s:%s", clientName, clientVersion), + PlatformName: "go", + PlatformVersion: runtime.Version(), + YggdrasilVersion: nil, + SpecVersion: specVersion, } u, _ := m.options.url.Parse("./client/metrics") @@ -305,5 +335,9 @@ func (m *metrics) getClientData() ClientData { m.options.strategies, m.started, int64(m.options.metricsInterval.Seconds()), + runtime.Version(), + "go", + nil, + specVersion, } } diff --git a/metrics_test.go b/metrics_test.go index 82fca5d..d69298b 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -2,11 +2,13 @@ package unleash import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" "reflect" + "runtime" "strings" "sync/atomic" "testing" @@ -14,9 +16,10 @@ import ( "github.com/Unleash/unleash-client-go/v4/api" internalapi "github.com/Unleash/unleash-client-go/v4/internal/api" + "github.com/h2non/gock" + "github.com/nbio/st" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "gopkg.in/h2non/gock.v1" ) func TestMetrics_RegisterInstance(t *testing.T) { @@ -426,3 +429,166 @@ func TestMetrics_ErrorCountShouldDecreaseIfSuccessful(t *testing.T) { assert.Equal(float64(0), client.metrics.errors) assert.Nil(err, "Client should close without a problem") } + +func clientDataMatcher() func(req *http.Request, ereq *gock.Request) (bool, error) { + defaultStrategies := []string{ + "default", + "applicationHostname", + "gradualRolloutRandom", + "gradualRolloutSessionId", + "gradualRolloutUserId", + "remoteAddress", + "userWithId", + "flexibleRollout", + } + return func(req *http.Request, ereq *gock.Request) (bool, error) { + var data ClientData + err := json.NewDecoder(req.Body).Decode(&data) + if err != nil { + return false, err + } + + if data.Started.IsZero() { + return false, nil + } + + expectedData := ClientData{ + AppName: mockAppName, + InstanceID: mockInstanceId, + SDKVersion: fmt.Sprintf("%s:%s", clientName, clientVersion), + Strategies: defaultStrategies, + Interval: 0, + PlatformVersion: runtime.Version(), + PlatformName: "go", + YggdrasilVersion: nil, + SpecVersion: specVersion, + } + + return data.AppName == expectedData.AppName && + data.InstanceID == expectedData.InstanceID && + data.SDKVersion == expectedData.SDKVersion && + compareStringSlices(data.Strategies, expectedData.Strategies) && + data.Interval == expectedData.Interval && + data.PlatformVersion == expectedData.PlatformVersion && + data.PlatformName == expectedData.PlatformName && + data.YggdrasilVersion == expectedData.YggdrasilVersion && + data.SpecVersion == expectedData.SpecVersion, nil + } +} + +func compareStringSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestMetrics_ClientDataIncludesNewMetadata(t *testing.T) { + assert := assert.New(t) + defer gock.OffAll() + + gock.New(mockerServer). + Post("/client/register"). + AddMatcher(clientDataMatcher()). + Reply(200) + + client, err := NewClient( + WithUrl(mockerServer), + WithMetricsInterval(50*time.Millisecond), + WithAppName(mockAppName), + WithInstanceId(mockInstanceId), + ) + + assert.Nil(err, "Client should open without a problem") + + time.Sleep(100 * time.Millisecond) + + err = client.Close() + + assert.Nil(err, "Client should close without errors") + + st.Expect(t, gock.IsDone(), true) +} + +func TestMetrics_metricsData_includes_new_metadata(t *testing.T) { + assert := assert.New(t) + defer gock.OffAll() + + gock.Observe(gock.DumpRequest) + + gock.New(mockerServer).Post("/client/register").Reply(200) + gock.New(mockerServer). + Post("/client/metrics"). + BodyString(`.*platformVersion.*`). + Reply(200) + gock.New(mockerServer). + Get("/client/features"). + Reply(200). + JSON(api.FeatureResponse{ + Features: []api.Feature{ + { + Name: "parent", + Enabled: true, + Description: "parent toggle", + Strategies: []api.Strategy{ + { + Id: 1, + Name: "flexibleRollout", + Constraints: []api.Constraint{}, + Parameters: map[string]interface{}{ + "rollout": 100, + "stickiness": "default", + }, + }, + }, + }, + { + Name: "child", + Enabled: true, + Description: "parent toggle", + Strategies: []api.Strategy{ + { + Id: 1, + Name: "flexibleRollout", + Constraints: []api.Constraint{}, + Parameters: map[string]interface{}{ + "rollout": 100, + "stickiness": "default", + }, + }, + }, + Dependencies: &[]api.Dependency{ + { + Feature: "parent", + }, + }, + }, + }, + }) + + client, err := NewClient( + WithUrl(mockerServer), + WithMetricsInterval(50*time.Millisecond), + WithAppName(mockAppName), + WithInstanceId(mockInstanceId), + WithDisableMetrics(false), + ) + assert.Nil(err, "Client should open without a problem") + + client.WaitForReady() + client.IsEnabled("foo") + client.IsEnabled("bar") + client.IsEnabled("baz") + + time.Sleep(320 * time.Millisecond) + err = client.Close() + + assert.Nil(err, "Client should close without errors") + + st.Expect(t, gock.IsDone(), true) +} diff --git a/repository_test.go b/repository_test.go index 2854730..7f26fc9 100644 --- a/repository_test.go +++ b/repository_test.go @@ -3,7 +3,6 @@ package unleash import ( "bytes" "encoding/json" - "gopkg.in/h2non/gock.v1" "net/http" "net/http/httptest" "strings" @@ -11,6 +10,8 @@ import ( "testing" "time" + "github.com/h2non/gock" + "github.com/Unleash/unleash-client-go/v4/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -129,20 +130,19 @@ func TestRepository_ParseAPIResponse(t *testing.T) { assert.Equal(0, len(response.Segments)) } - func TestRepository_backs_off_on_http_statuses(t *testing.T) { a := assert.New(t) testCases := []struct { statusCode int errorCount float64 }{ - { 401, 10}, - { 403, 10}, - { 404, 10}, - { 429, 1}, - { 500, 1}, - { 502, 1}, - { 503, 1}, + {401, 10}, + {403, 10}, + {404, 10}, + {429, 1}, + {500, 1}, + {502, 1}, + {503, 1}, } defer gock.Off() for _, tc := range testCases { @@ -154,7 +154,7 @@ func TestRepository_backs_off_on_http_statuses(t *testing.T) { WithAppName(mockAppName), WithDisableMetrics(true), WithInstanceId(mockInstanceId), - WithRefreshInterval(time.Millisecond * 15), + WithRefreshInterval(time.Millisecond*15), ) a.Nil(err) time.Sleep(20 * time.Millisecond) @@ -167,8 +167,8 @@ func TestRepository_back_offs_are_gradually_reduced_on_success(t *testing.T) { a := assert.New(t) defer gock.Off() gock.New(mockerServer). - Get("/client/features"). - Times(4). + Get("/client/features"). + Times(4). Reply(429) gock.New(mockerServer). Get("/client/features"). @@ -179,11 +179,11 @@ func TestRepository_back_offs_are_gradually_reduced_on_success(t *testing.T) { WithAppName(mockAppName), WithDisableMetrics(true), WithInstanceId(mockInstanceId), - WithRefreshInterval(time.Millisecond * 10), + WithRefreshInterval(time.Millisecond*10), ) a.Nil(err) client.WaitForReady() err = client.Close() a.Equal(float64(3), client.repository.errors) // 4 failures, and then one success, should reduce error count to 3 a.Nil(err) -} \ No newline at end of file +} diff --git a/spec_test.go b/spec_test.go index 7fb566a..55f1078 100644 --- a/spec_test.go +++ b/spec_test.go @@ -12,10 +12,10 @@ import ( "github.com/Unleash/unleash-client-go/v4/api" "github.com/Unleash/unleash-client-go/v4/context" + "github.com/h2non/gock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" - "gopkg.in/h2non/gock.v1" ) const mockHost = "http://unleash-apu" diff --git a/unleash_test.go b/unleash_test.go index 1e96730..bf5653c 100644 --- a/unleash_test.go +++ b/unleash_test.go @@ -7,8 +7,8 @@ import ( "time" "github.com/Unleash/unleash-client-go/v4" + "github.com/h2non/gock" "github.com/stretchr/testify/assert" - "gopkg.in/h2non/gock.v1" ) func Test_withVariants(t *testing.T) {