diff --git a/CHANGELOG.md b/CHANGELOG.md index a24047d..e9fac96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v0.0.10 + +* feat: add gzip size of sent data stat +* feat: singleton broker list [CIRC-9594] + # v0.0.9 * chore: add RefreshCheckBundle tests diff --git a/broker.go b/broker.go index 69c4279..ac4e7e0 100644 --- a/broker.go +++ b/broker.go @@ -35,14 +35,15 @@ func (tc *TrapCheck) fetchBroker(cid, checkType string) error { if checkType == "" { return fmt.Errorf("invalid check type (empty)") } - broker, err := tc.client.FetchBroker(apiclient.CIDType(&cid)) + broker, err := tc.brokerList.GetBroker(cid) + // broker, err := tc.client.FetchBroker(apiclient.CIDType(&cid)) if err != nil { return fmt.Errorf("retrieving broker (%s): %w", cid, err) } - if valid, err := tc.isValidBroker(broker, checkType); !valid { + if valid, err := tc.isValidBroker(&broker, checkType); !valid { return fmt.Errorf("%s (%s) is an invalid broker for check type %s: %w", tc.broker.Name, tc.checkConfig.Brokers[0], checkType, err) } - tc.broker = broker + tc.broker = &broker return nil } @@ -57,33 +58,33 @@ func (tc *TrapCheck) getBroker(checkType string) error { // // otherwise, select an applicable broker // - var brokerList *[]apiclient.Broker + var list *[]apiclient.Broker if len(tc.brokerSelectTags) > 0 { - filter := apiclient.SearchFilterType{ - "f__tags_has": tc.brokerSelectTags, - } - bl, err := tc.client.SearchBrokers(nil, &filter) + // filter := apiclient.SearchFilterType{ + // "f__tags_has": tc.brokerSelectTags, + // } + bl, err := tc.brokerList.SearchBrokerList(tc.brokerSelectTags) // tc.client.SearchBrokers(nil, &filter) if err != nil { return fmt.Errorf("search brokers: %w", err) } - brokerList = bl + list = bl } else { - bl, err := tc.client.FetchBrokers() + bl, err := tc.brokerList.GetBrokerList() // tc.client.FetchBrokers() if err != nil { return fmt.Errorf("fetch brokers: %w", err) } - brokerList = bl + list = bl } - if len(*brokerList) == 0 { + if len(*list) == 0 { return fmt.Errorf("zero brokers found") } validBrokers := make(map[string]apiclient.Broker) haveEnterprise := false - for _, broker := range *brokerList { + for _, broker := range *list { broker := broker valid, err := tc.isValidBroker(&broker, checkType) if err != nil { @@ -109,7 +110,7 @@ func (tc *TrapCheck) getBroker(checkType string) error { } if len(validBrokers) == 0 { - return fmt.Errorf("found %d broker(s), zero are valid", len(*brokerList)) + return fmt.Errorf("found %d broker(s), zero are valid", len(*list)) } validBrokerKeys := reflect.ValueOf(validBrokers).MapKeys() diff --git a/broker_test.go b/broker_test.go index 0bd662a..f80c3f1 100644 --- a/broker_test.go +++ b/broker_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/circonus-labs/go-apiclient" + brokerList "github.com/circonus-labs/go-trapcheck/internal/broker_list" ) func TestTrapCheck_brokerSupportsCheckType(t *testing.T) { @@ -354,6 +355,133 @@ func TestTrapCheck_getBroker(t *testing.T) { } brokerPort := uint16(bp) + noBrokersFoundClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return nil, fmt.Errorf("API 404 - broker not found") + }, + } + + emptyBrokerListClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{}, nil + }, + } + + noActiveBrokersClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: "foo", + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + { + CID: "/broker/456", + Name: "bar", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: "bar", + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + }, nil + }, + } + + basicEnterpriseClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"foo:bar"}, + }, + { + CID: "/broker/456", + Name: "bar", + Type: enterpriseType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + }, nil + }, + } + + basicClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"foo:bar"}, + }, + { + CID: "/broker/456", + Name: "bar", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + { + CID: "/broker/789", + Name: "baz", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"ack:nak", "wing:ding"}, + }, + }, nil + }, + } + tests := []struct { checkConfig *apiclient.CheckBundle checkBundle *apiclient.CheckBundle @@ -367,203 +495,68 @@ func TestTrapCheck_getBroker(t *testing.T) { checkBrokerType bool }{ { - name: "invalid (non-existent) broker in passed config", - client: &APIMock{ - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return nil, fmt.Errorf("API 404 - broker not found") - }, - }, - checkConfig: &apiclient.CheckBundle{Brokers: []string{"/broker/123"}}, + name: "invalid (non-existent) broker in passed config", + client: basicClient, + checkConfig: &apiclient.CheckBundle{Brokers: []string{"/broker/999"}}, checkType: "httptrap", wantErr: true, }, { - name: "valid broker in passed config", - client: &APIMock{ - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return &apiclient.Broker{ - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, nil - }, - }, + name: "valid broker in passed config", + client: basicClient, checkConfig: &apiclient.CheckBundle{Brokers: []string{"/broker/123"}}, checkType: "httptrap", wantErr: false, }, { - name: "invalid search broker w/select tag - not found", - client: &APIMock{ - SearchBrokersFunc: func(searchCriteria *apiclient.SearchQueryType, filterCriteria *apiclient.SearchFilterType) (*[]apiclient.Broker, error) { - return nil, fmt.Errorf("API 404 - broker not found") - }, - }, - brokerSelectTags: apiclient.TagType{"foo:bar"}, + name: "invalid search broker w/select tag - not found", + client: basicClient, + brokerSelectTags: apiclient.TagType{"bar:baz"}, checkType: "httptrap", wantErr: true, }, { - name: "valid search broker w/select tag", - client: &APIMock{ - SearchBrokersFunc: func(searchCriteria *apiclient.SearchQueryType, filterCriteria *apiclient.SearchFilterType) (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, - }, + name: "valid search broker w/select tag", + client: basicClient, brokerSelectTags: apiclient.TagType{"foo:bar"}, checkType: "httptrap", wantErr: false, }, { - name: "invalid no brokers found", - client: &APIMock{ - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return nil, fmt.Errorf("API 404 - broker not found") - }, - }, + name: "valid search broker w/select tags", + client: basicClient, + brokerSelectTags: apiclient.TagType{"wing:ding", "ack:nak"}, + checkType: "httptrap", + wantErr: false, + }, + { + name: "invalid no brokers found", + client: noBrokersFoundClient, checkType: "httptrap", wantErr: true, }, { - name: "invalid empty broker list", - client: &APIMock{ - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{}, nil - }, - }, + name: "invalid empty broker list", + client: emptyBrokerListClient, checkType: "httptrap", wantErr: true, }, { - name: "invalid no active brokers", - client: &APIMock{ - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: "foo", - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - { - CID: "/broker/456", - Name: "bar", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: "bar", - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, - }, + name: "invalid no active brokers", + client: noActiveBrokersClient, checkType: "httptrap", wantErr: true, }, { - name: "valid active brokers", - client: &APIMock{ - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - { - CID: "/broker/456", - Name: "bar", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, - }, - checkType: "httptrap", - wantErr: false, + name: "valid active brokers (circ type)", + client: basicClient, + checkType: "httptrap", + wantErr: false, + wantBrokerType: circonusType, }, { - name: "valid active brokers", - client: &APIMock{ - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - { - CID: "/broker/456", - Name: "bar", - Type: enterpriseType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, - }, + name: "valid active brokers (enterprise)", + client: basicEnterpriseClient, checkType: "httptrap", wantErr: false, checkBrokerType: true, @@ -573,7 +566,15 @@ func TestTrapCheck_getBroker(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - tc.client = tt.client + if err := brokerList.Init(tt.client, tc.Log); err != nil { + t.Errorf("initializing broker list: %s", err) + } + if bl, err := brokerList.GetInstance(); err != nil { + t.Errorf("getting broker list instance: %s", err) + } else { + tc.brokerList = bl + } + // tc.client = tt.client tc.checkConfig = tt.checkConfig tc.checkBundle = tt.checkBundle tc.brokerSelectTags = tt.brokerSelectTags diff --git a/check_test.go b/check_test.go index bd209a8..a8b4f74 100644 --- a/check_test.go +++ b/check_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/circonus-labs/go-apiclient" + brokerList "github.com/circonus-labs/go-trapcheck/internal/broker_list" ) func TestTrapCheck_applyCheckBundleDefaults(t *testing.T) { @@ -25,27 +26,10 @@ func TestTrapCheck_applyCheckBundleDefaults(t *testing.T) { Debug: false, } - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "beep boop") - })) - defer ts.Close() - - tsURL, err := url.Parse(ts.URL) - if err != nil { - t.Fatalf("creating test broker: %s", err) - } - brokerIP := tsURL.Hostname() - bp, err := strconv.Atoi(tsURL.Port()) - if err != nil { - t.Fatalf("parsing test broker port: %s", err) - } - brokerPort := uint16(bp) - type args struct { cfg *apiclient.CheckBundle } tests := []struct { - client API args args name string wantErr bool @@ -54,23 +38,6 @@ func TestTrapCheck_applyCheckBundleDefaults(t *testing.T) { name: "basic", args: args{cfg: &apiclient.CheckBundle{Brokers: []string{"/broker/123"}}}, wantErr: false, - client: &APIMock{ - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return &apiclient.Broker{ - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, nil - }, - }, }, } for _, tt := range tests { @@ -106,11 +73,60 @@ func TestTrapCheck_fetchCheckBundle(t *testing.T) { } brokerPort := uint16(bp) + basicBrokerClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"foo:bar"}, + }, + { + CID: "/broker/456", + Name: "bar", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + { + CID: "/broker/789", + Name: "baz", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"ack:nak", "wing:ding"}, + }, + }, nil + }, + } + tests := []struct { - checkConfig *apiclient.CheckBundle - client API - name string - wantErr bool + checkConfig *apiclient.CheckBundle + client API + brokerClient API + name string + wantErr bool }{ { name: "invalid, not found", @@ -121,6 +137,7 @@ func TestTrapCheck_fetchCheckBundle(t *testing.T) { return nil, fmt.Errorf("API 404 - not found") }, }, + brokerClient: basicBrokerClient, }, { name: "invalid, no submission_url", @@ -131,6 +148,7 @@ func TestTrapCheck_fetchCheckBundle(t *testing.T) { return &apiclient.CheckBundle{CID: "/check_bundle/123"}, nil }, }, + brokerClient: basicBrokerClient, }, { name: "invalid, broker not found", @@ -144,10 +162,8 @@ func TestTrapCheck_fetchCheckBundle(t *testing.T) { Brokers: []string{"/broker/123"}, }, nil }, - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return nil, fmt.Errorf("API 404 - not found") - }, }, + brokerClient: basicBrokerClient, }, { name: "invalid, check not active", @@ -163,22 +179,8 @@ func TestTrapCheck_fetchCheckBundle(t *testing.T) { Status: "invalid", }, nil }, - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return &apiclient.Broker{ - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, { name: "valid", @@ -194,27 +196,21 @@ func TestTrapCheck_fetchCheckBundle(t *testing.T) { Status: statusActive, }, nil }, - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return &apiclient.Broker{ - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + if err := brokerList.Init(tt.brokerClient, tc.Log); err != nil { + t.Errorf("initializing broker list: %s", err) + } + if bl, err := brokerList.GetInstance(); err != nil { + t.Errorf("getting broker list instance: %s", err) + } else { + tc.brokerList = bl + } tc.client = tt.client tc.checkConfig = tt.checkConfig if err := tc.fetchCheckBundle(); (err != nil) != tt.wantErr { @@ -247,69 +243,84 @@ func TestTrapCheck_createCheckBundle(t *testing.T) { } brokerPort := uint16(bp) + basicBrokerClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"foo:bar"}, + }, + { + CID: "/broker/456", + Name: "bar", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + { + CID: "/broker/789", + Name: "baz", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"ack:nak", "wing:ding"}, + }, + }, nil + }, + } + tests := []struct { - client API - cfg *apiclient.CheckBundle - name string - wantErr bool + client API + brokerClient API + cfg *apiclient.CheckBundle + name string + wantErr bool }{ { name: "invalid, nil config", wantErr: true, }, { - name: "invalid config", - cfg: &apiclient.CheckBundle{}, + name: "invalid config", + cfg: &apiclient.CheckBundle{}, + brokerClient: basicBrokerClient, client: &APIMock{ CreateCheckBundleFunc: func(cfg *apiclient.CheckBundle) (*apiclient.CheckBundle, error) { return nil, fmt.Errorf("API 500 - failure") }, - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - CN: "testbroker.example.com", - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, }, wantErr: true, }, { - name: "valid", - cfg: &apiclient.CheckBundle{}, + name: "valid", + cfg: &apiclient.CheckBundle{}, + brokerClient: basicBrokerClient, client: &APIMock{ CreateCheckBundleFunc: func(cfg *apiclient.CheckBundle) (*apiclient.CheckBundle, error) { return &apiclient.CheckBundle{CID: "/check_bundle/123"}, nil }, - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - CN: "testbroker.example.com", - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, }, wantErr: false, }, @@ -323,6 +334,14 @@ func TestTrapCheck_createCheckBundle(t *testing.T) { } } tc.client = tt.client + if err := brokerList.Init(tt.brokerClient, tc.Log); err != nil { + t.Errorf("initializing broker list: %s", err) + } + if bl, err := brokerList.GetInstance(); err != nil { + t.Errorf("getting broker list instance: %s", err) + } else { + tc.brokerList = bl + } if err := tc.createCheckBundle(tt.cfg); (err != nil) != tt.wantErr { t.Errorf("TrapCheck.createCheckBundle() error = %v, wantErr %v", err, tt.wantErr) } @@ -487,8 +506,57 @@ func TestTrapCheck_initCheckBundle(t *testing.T) { } brokerPort := uint16(bp) + basicBrokerClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"foo:bar"}, + }, + { + CID: "/broker/456", + Name: "bar", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + { + CID: "/broker/789", + Name: "baz", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"ack:nak", "wing:ding"}, + }, + }, nil + }, + } + tests := []struct { client API + brokerClient API cfg *apiclient.CheckBundle name string checkSearchTags apiclient.TagType @@ -504,6 +572,7 @@ func TestTrapCheck_initCheckBundle(t *testing.T) { return nil, fmt.Errorf("API 404 - not found") }, }, + brokerClient: basicBrokerClient, }, { name: "success: search found", @@ -517,6 +586,7 @@ func TestTrapCheck_initCheckBundle(t *testing.T) { }, nil }, }, + brokerClient: basicBrokerClient, }, { name: "search not found, create error", @@ -530,25 +600,8 @@ func TestTrapCheck_initCheckBundle(t *testing.T) { SearchCheckBundlesFunc: func(searchCriteria *apiclient.SearchQueryType, filterCriteria *apiclient.SearchFilterType) (*[]apiclient.CheckBundle, error) { return &[]apiclient.CheckBundle{}, nil }, - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - CN: "testbroker.example.com", - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, { name: "success: search not found, create", @@ -562,31 +615,22 @@ func TestTrapCheck_initCheckBundle(t *testing.T) { SearchCheckBundlesFunc: func(searchCriteria *apiclient.SearchQueryType, filterCriteria *apiclient.SearchFilterType) (*[]apiclient.CheckBundle, error) { return &[]apiclient.CheckBundle{}, nil }, - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - CN: "testbroker.example.com", - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { tc.client = tt.client + if err := brokerList.Init(tt.brokerClient, tc.Log); err != nil { + t.Errorf("initializing broker list: %s", err) + } + if bl, err := brokerList.GetInstance(); err != nil { + t.Errorf("getting broker list instance: %s", err) + } else { + tc.brokerList = bl + } tc.checkSearchTags = tt.checkSearchTags if err := tc.initCheckBundle(tt.cfg); (err != nil) != tt.wantErr { t.Errorf("TrapCheck.initCheckBundle() error = %v, wantErr %v", err, tt.wantErr) @@ -618,8 +662,57 @@ func TestTrapCheck_initializeCheck(t *testing.T) { } brokerPort := uint16(bp) + basicBrokerClient := &APIMock{ + FetchBrokersFunc: func() (*[]apiclient.Broker, error) { + return &[]apiclient.Broker{ + { + CID: "/broker/123", + Name: "foo", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"foo:bar"}, + }, + { + CID: "/broker/456", + Name: "bar", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + }, + { + CID: "/broker/789", + Name: "baz", + Type: circonusType, + Details: []apiclient.BrokerDetail{ + { + Status: statusActive, + Modules: []string{"httptrap"}, + IP: &brokerIP, + Port: &brokerPort, + }, + }, + Tags: []string{"ack:nak", "wing:ding"}, + }, + }, nil + }, + } + tests := []struct { client API + brokerClient API checkConfig *apiclient.CheckBundle name string checkSearchTags apiclient.TagType @@ -639,6 +732,7 @@ func TestTrapCheck_initializeCheck(t *testing.T) { return nil, fmt.Errorf("API 404 - not found") }, }, + brokerClient: basicBrokerClient, }, { name: "cfg w/cid - broker - api error", @@ -653,10 +747,8 @@ func TestTrapCheck_initializeCheck(t *testing.T) { FetchCheckBundleFunc: func(cid apiclient.CIDType) (*apiclient.CheckBundle, error) { return &apiclient.CheckBundle{CID: "/check_bundle/123", Brokers: []string{"/broker/123"}}, nil }, - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return nil, fmt.Errorf("API 404 - not found") - }, }, + brokerClient: basicBrokerClient, }, { name: "success: cfg w/cid", @@ -672,22 +764,8 @@ func TestTrapCheck_initializeCheck(t *testing.T) { Status: statusActive, }, nil }, - FetchBrokerFunc: func(cid apiclient.CIDType) (*apiclient.Broker, error) { - return &apiclient.Broker{ - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, { name: "success: search", @@ -705,24 +783,8 @@ func TestTrapCheck_initializeCheck(t *testing.T) { }, }, nil }, - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, { name: "success: create", @@ -741,30 +803,22 @@ func TestTrapCheck_initializeCheck(t *testing.T) { Status: statusActive, }, nil }, - FetchBrokersFunc: func() (*[]apiclient.Broker, error) { - return &[]apiclient.Broker{ - { - CID: "/broker/123", - Name: "foo", - Type: circonusType, - Details: []apiclient.BrokerDetail{ - { - Status: statusActive, - Modules: []string{"httptrap"}, - IP: &brokerIP, - Port: &brokerPort, - }, - }, - }, - }, nil - }, }, + brokerClient: basicBrokerClient, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { tc.client = tt.client + if err := brokerList.Init(tt.brokerClient, tc.Log); err != nil { + t.Errorf("initializing broker list: %s", err) + } + if bl, err := brokerList.GetInstance(); err != nil { + t.Errorf("getting broker list instance: %s", err) + } else { + tc.brokerList = bl + } tc.checkConfig = tt.checkConfig tc.checkSearchTags = tt.checkSearchTags if err := tc.initializeCheck(); (err != nil) != tt.wantErr { diff --git a/go.mod b/go.mod index 3917d02..90ae7d2 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/circonus-labs/go-trapcheck go 1.17 require ( - github.com/circonus-labs/go-apiclient v0.7.18 + github.com/circonus-labs/go-apiclient v0.7.22 github.com/google/uuid v1.3.0 - github.com/hashicorp/go-retryablehttp v0.7.1 + github.com/hashicorp/go-retryablehttp v0.7.2 ) require ( - github.com/hashicorp/go-cleanhttp v0.5.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/pkg/errors v0.9.1 // indirect ) diff --git a/go.sum b/go.sum index a19402a..5f0ce57 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,18 @@ -github.com/circonus-labs/go-apiclient v0.7.18 h1:OrRHUwoFroEUN+3KylQkdQOaTN1s3SCFh9E6D7Eb9PQ= -github.com/circonus-labs/go-apiclient v0.7.18/go.mod h1:NCu3kJ282ZGzr7vxzFH/S28y1nW9DZ37f8L7veDENFc= +github.com/circonus-labs/go-apiclient v0.7.22 h1:d7C1Ys/EAcn/jhDUXl7Nqn5nNXR5bkr7/Cyfgq/BuoA= +github.com/circonus-labs/go-apiclient v0.7.22/go.mod h1:NCu3kJ282ZGzr7vxzFH/S28y1nW9DZ37f8L7veDENFc= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/internal/broker_list/api.go b/internal/broker_list/api.go new file mode 100644 index 0000000..f6c9023 --- /dev/null +++ b/internal/broker_list/api.go @@ -0,0 +1,12 @@ +package brokerlist + +//go:generate moq -out api_moq_test.go . API + +import "github.com/circonus-labs/go-apiclient" + +type API interface { + // broker methods + FetchBroker(cid apiclient.CIDType) (*apiclient.Broker, error) + FetchBrokers() (*[]apiclient.Broker, error) + SearchBrokers(searchCriteria *apiclient.SearchQueryType, filterCriteria *apiclient.SearchFilterType) (*[]apiclient.Broker, error) +} diff --git a/internal/broker_list/broker_list.go b/internal/broker_list/broker_list.go new file mode 100644 index 0000000..92d6e55 --- /dev/null +++ b/internal/broker_list/broker_list.go @@ -0,0 +1,161 @@ +package brokerlist + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/circonus-labs/go-apiclient" +) + +// var once sync.Once + +type BrokerList interface { + RefreshBrokers() error + FetchBrokers() error + GetBrokerList() (*[]apiclient.Broker, error) + GetBroker(cid string) (apiclient.Broker, error) + SearchBrokerList(searchTags apiclient.TagType) (*[]apiclient.Broker, error) +} + +type brokerList struct { + lastRefresh time.Time + logger Logger + client API + brokers *[]apiclient.Broker + sync.Mutex +} + +var brokerListInstance *brokerList + +func Init(client API, logger Logger) error { + if client == nil { + return fmt.Errorf("invalid init call, client is nil") + } + + if logger == nil { + return fmt.Errorf("invalid init call, logger is nil") + } + + if brokerListInstance != nil { + return nil + } + + brokerListInstance = &brokerList{ + client: client, + logger: logger, + } + return brokerListInstance.FetchBrokers() +} + +func GetInstance() (BrokerList, error) { //nolint:revive + if brokerListInstance == nil { + return nil, fmt.Errorf("broker list not initialized") + } + return brokerListInstance, nil +} + +func (bl *brokerList) RefreshBrokers() error { + // only refresh if it's been at least five minutes since last refresh + // to prevent API request storms. + if time.Since(bl.lastRefresh) > 5*time.Minute { + return bl.FetchBrokers() + } + return nil +} + +func (bl *brokerList) FetchBrokers() error { + bl.Lock() + defer bl.Unlock() + + bl.logger.Infof("fetching broker list") + list, err := bl.client.FetchBrokers() + if err != nil { + return fmt.Errorf("error fetching broker list: %w", err) + } + + bl.brokers = list + + return nil +} + +func (bl *brokerList) GetBrokerList() (*[]apiclient.Broker, error) { + bl.Lock() + defer bl.Unlock() + + if bl.brokers == nil { + return nil, fmt.Errorf("invalid state, broker list is nil") + } + + if len(*bl.brokers) == 0 { + return nil, fmt.Errorf("invalid state, empty broker list") + } + + list := *bl.brokers + + return &list, nil +} + +func (bl *brokerList) GetBroker(cid string) (apiclient.Broker, error) { + if cid == "" { + return apiclient.Broker{}, fmt.Errorf("invalid cid (empty)") + } + + bl.Lock() + defer bl.Unlock() + + if bl.brokers == nil { + return apiclient.Broker{}, fmt.Errorf("invalid state, broker list is nil") + } + + if len(*bl.brokers) == 0 { + if err := bl.FetchBrokers(); err != nil { + return apiclient.Broker{}, fmt.Errorf("invalid state, broker list len is 0, unable to fetch broker list: %w", err) + } + if len(*bl.brokers) == 0 { + return apiclient.Broker{}, fmt.Errorf("invalid state, no brokers in list") + } + } + + for _, b := range *bl.brokers { + if b.CID == cid { + bl.logger.Infof("using cached broker %s", b.CID) + return b, nil + } + } + + return apiclient.Broker{}, fmt.Errorf("no broker with CID (%s) found", cid) +} + +func (bl *brokerList) SearchBrokerList(searchTags apiclient.TagType) (*[]apiclient.Broker, error) { + bl.Lock() + defer bl.Unlock() + + if bl.brokers == nil { + return nil, fmt.Errorf("invalid state, broker list is nil") + } + + if len(*bl.brokers) == 0 { + return nil, fmt.Errorf("invalid state, empty broker list") + } + + var list []apiclient.Broker + + for _, b := range *bl.brokers { + numTagsFound := 0 + for _, t := range b.Tags { + for _, st := range searchTags { + if strings.EqualFold(t, st) { + numTagsFound++ + break + } + } + } + if numTagsFound == len(searchTags) { + list = append(list, b) + } + } + + return &list, nil +} diff --git a/internal/broker_list/log.go b/internal/broker_list/log.go new file mode 100644 index 0000000..0525b29 --- /dev/null +++ b/internal/broker_list/log.go @@ -0,0 +1,9 @@ +package brokerlist + +type Logger interface { + Printf(fmt string, v ...interface{}) + Debugf(fmt string, v ...interface{}) + Infof(fmt string, v ...interface{}) + Warnf(fmt string, v ...interface{}) + Errorf(fmt string, v ...interface{}) +} diff --git a/submit.go b/submit.go index 967f048..6283033 100644 --- a/submit.go +++ b/submit.go @@ -26,14 +26,15 @@ import ( ) type TrapResult struct { - CheckUUID string - Error string `json:"error,omitempty"` - SubmitUUID string - Filtered uint64 `json:"filtered,omitempty"` - Stats uint64 `json:"stats"` - SubmitDuration time.Duration - LastReqDuration time.Duration - BytesSent int + CheckUUID string `json:"check_uuid"` + Error string `json:"error,omitempty"` + SubmitUUID string `json:"submit_uuid"` + Filtered uint64 `json:"filtered,omitempty"` + Stats uint64 `json:"stats"` + SubmitDuration time.Duration `json:"submit_dur"` + LastReqDuration time.Duration `json:"last_req_dur"` + BytesSent int `json:"bytes_sent"` + BytesSentGzip int `json:"bytes_sent_gz"` } const ( @@ -73,6 +74,7 @@ func (tc *TrapCheck) submit(ctx context.Context, metrics bytes.Buffer) (*TrapRes MaxIdleConns: 1, MaxIdleConnsPerHost: 0, }, + Timeout: 60 * time.Second, // hard 60s timeout } } else { client = &http.Client{ @@ -88,6 +90,7 @@ func (tc *TrapCheck) submit(ctx context.Context, metrics bytes.Buffer) (*TrapRes MaxIdleConns: 1, MaxIdleConnsPerHost: 0, }, + Timeout: 60 * time.Second, // hard 60s timeout } } @@ -256,7 +259,8 @@ func (tc *TrapCheck) submit(ctx context.Context, metrics bytes.Buffer) (*TrapRes result.SubmitUUID = submitUUID result.SubmitDuration = time.Since(start) result.LastReqDuration = time.Since(reqStart) - result.BytesSent = dataLen + result.BytesSent = metricLen + result.BytesSentGzip = dataLen if result.Error == "" { result.Error = "none" } diff --git a/tls.go b/tls.go index f3a2991..12ad31e 100644 --- a/tls.go +++ b/tls.go @@ -29,6 +29,7 @@ func (tc *TrapCheck) setBrokerTLSConfig() error { tc.tlsConfig = nil // don't use, refresh and reset tc.resetTLSConfig = false // tc.custTLSConfig = nil // don't use, refresh and reset + _ = tc.brokerList.RefreshBrokers() } // setBrokerTLSConfig has already initialized it diff --git a/trapcheck.go b/trapcheck.go index 66c2c7a..ef5828d 100644 --- a/trapcheck.go +++ b/trapcheck.go @@ -20,6 +20,7 @@ import ( "github.com/circonus-labs/go-apiclient" "github.com/circonus-labs/go-apiclient/config" + brokerList "github.com/circonus-labs/go-trapcheck/internal/broker_list" ) type Config struct { @@ -49,14 +50,15 @@ type Config struct { type TrapCheck struct { client API Log Logger + brokerList brokerList.BrokerList checkConfig *apiclient.CheckBundle checkBundle *apiclient.CheckBundle broker *apiclient.Broker tlsConfig *tls.Config custTLSConfig *tls.Config - submissionURL string custSubmissionURL string traceMetrics string + submissionURL string checkSearchTags apiclient.TagType brokerSelectTags apiclient.TagType brokerMaxResponseTime time.Duration @@ -111,6 +113,16 @@ func New(cfg *Config) (*TrapCheck, error) { } } + if err := brokerList.Init(cfg.Client, tc.Log); err != nil { + return nil, fmt.Errorf("initializing broker list: %w", err) + } + + if bl, err := brokerList.GetInstance(); err != nil { + return nil, fmt.Errorf("getting broker list instance: %w", err) + } else { + tc.brokerList = bl + } + dur := cfg.BrokerMaxResponseTime if dur == "" { dur = defaultBrokerMaxResponseTime