diff --git a/pkg/collector/corechecks/network-devices/versa/client/client.go b/pkg/collector/corechecks/network-devices/versa/client/client.go index 60ed12b2f79f..aac726f9ec8d 100644 --- a/pkg/collector/corechecks/network-devices/versa/client/client.go +++ b/pkg/collector/corechecks/network-devices/versa/client/client.go @@ -23,14 +23,15 @@ import ( ) const ( - defaultBasicPort = 9182 - defaultOAuthPort = 9183 - defaultMaxAttempts = 3 - defaultMaxPages = 100 - defaultMaxCount = "2000" - defaultLookback = "30minutesAgo" - defaultHTTPTimeout = 10 - defaultHTTPScheme = "https" + defaultBasicPort = 9182 + defaultOAuthPort = 9183 + defaultMaxAttempts = 3 + defaultMaxPages = 100 + defaultMaxCount = "2000" + defaultLookback = "30minutesAgo" + defaultExtendedLookback = "1daysAgo" + defaultHTTPTimeout = 10 + defaultHTTPScheme = "https" ) // Useful for mocking @@ -46,18 +47,21 @@ type Client struct { directorToken string directorTokenExpiry time.Time // Session token for Analytics endpoints (always uses session auth) - sessionToken string - sessionTokenExpiry time.Time - username string - password string - clientID string - clientSecret string - authMethod authMethod - authenticationMutex *sync.Mutex - maxAttempts int - maxPages int - maxCount string // Stored as string to be passed as an HTTP param - lookback string + sessionToken string + sessionTokenExpiry time.Time + username string + password string + clientID string + clientSecret string + authMethod authMethod + authenticationMutex *sync.Mutex + maxAttempts int + maxPages int + maxCount string // Stored as string to be passed as an HTTP param + lookback string + extendedLookback string // Use for endpoints that require longer lookback windows + useStartPagination bool // Use "start" instead of "offset" for pagination (necessary for some deployments) + useAlternateAppliances bool // Use GetAppliances instead of GetChildAppliancesDetail for appliance collection } // ClientOptions are the functional options for the Versa client @@ -125,6 +129,7 @@ func NewClient(directorEndpoint string, directorPort int, analyticsEndpoint stri maxPages: defaultMaxPages, maxCount: defaultMaxCount, lookback: defaultLookback, + extendedLookback: defaultExtendedLookback, } for _, opt := range options { @@ -147,6 +152,14 @@ func validateParams(directorEndpoint string, directorPort int, analyticsEndpoint return nil } +// WithTimeout is a functional option to set the HTTP Client timeout +// in seconds +func WithTimeout(timeoutSeconds int) ClientOptions { + return func(c *Client) { + c.httpClient.Timeout = time.Duration(timeoutSeconds) * time.Second + } +} + // WithTLSConfig is a functional option to set the HTTP Client TLS Config func WithTLSConfig(insecure bool, CAFile string) (ClientOptions, error) { var caCert []byte @@ -200,12 +213,41 @@ func WithMaxPages(maxPages int) ClientOptions { } // WithLookback is a functional option to set the client lookback interval -func WithLookback(lookback int) ClientOptions { +func WithLookback(lookback string) ClientOptions { + return func(c *Client) { + c.lookback = lookback + } +} + +// WithExtendedLookback is a functional option to set the client extended lookback interval +func WithExtendedLookback(extendedLookback string) ClientOptions { + return func(c *Client) { + c.extendedLookback = extendedLookback + } +} + +// WithStartPagination is a functional option to enable using "start" instead of "offset" for pagination +func WithStartPagination(useStart bool) ClientOptions { return func(c *Client) { - c.lookback = createLookbackString(lookback) + c.useStartPagination = useStart } } +// WithAlternateAppliances is a functional option to enable using GetAppliances instead of GetChildAppliancesDetail +func WithAlternateAppliances(useAlternate bool) ClientOptions { + return func(c *Client) { + c.useAlternateAppliances = useAlternate + } +} + +// getOffsetParamName returns the pagination parameter name based on the feature flag +func (client *Client) getOffsetParamName() string { + if client.useStartPagination { + return "start" + } + return "offset" +} + // GetOrganizations retrieves a list of organizations func (client *Client) GetOrganizations() ([]Organization, error) { var organizations []Organization @@ -220,8 +262,8 @@ func (client *Client) GetOrganizations() ([]Organization, error) { totalPages := (resp.TotalCount + maxCount - 1) / maxCount // calculate total pages, rounding up if there's any remainder for i := 1; i < totalPages; i++ { // start from 1 to skip the first page params := map[string]string{ - "limit": client.maxCount, - "offset": strconv.Itoa(i * maxCount), + "limit": client.maxCount, + client.getOffsetParamName(): strconv.Itoa(i * maxCount), } resp, err := get[OrganizationListResponse](client, "/vnms/organization/orgs", params, false) if err != nil { @@ -243,9 +285,9 @@ func (client *Client) GetChildAppliancesDetail(tenant string) ([]Appliance, erro uri := "/vnms/dashboard/childAppliancesDetail/" + tenant var appliances []Appliance params := map[string]string{ - "fetch": "count", - "limit": client.maxCount, - "offset": "0", + "fetch": "count", + "limit": client.maxCount, + client.getOffsetParamName(): "0", } // Get the total count of appliances @@ -262,7 +304,7 @@ func (client *Client) GetChildAppliancesDetail(tenant string) ([]Appliance, erro totalPages := (*totalCount + maxCount - 1) / maxCount // calculate total pages, rounding up if there's any remainder for i := 0; i < totalPages; i++ { params["fetch"] = "all" - params["offset"] = fmt.Sprintf("%d", i*maxCount) + params[client.getOffsetParamName()] = fmt.Sprintf("%d", i*maxCount) resp, err := get[[]Appliance](client, uri, params, false) if err != nil { return nil, fmt.Errorf("failed to get appliance detail response: %v", err) @@ -280,8 +322,8 @@ func (client *Client) GetChildAppliancesDetail(tenant string) ([]Appliance, erro func (client *Client) GetAppliances() ([]Appliance, error) { var allAppliances []Appliance params := map[string]string{ - "limit": client.maxCount, - "offset": "0", + "limit": client.maxCount, + client.getOffsetParamName(): "0", } // Make the first request to get the first page and total count @@ -306,7 +348,7 @@ func (client *Client) GetAppliances() ([]Appliance, error) { // Paginate through the remaining pages for i := 1; i < totalPages; i++ { - params["offset"] = strconv.Itoa(i * maxCount) + params[client.getOffsetParamName()] = strconv.Itoa(i * maxCount) pageResp, err := get[ApplianceListResponse](client, "/vnms/appliance/appliance", params, false) if err != nil { @@ -471,7 +513,7 @@ func (client *Client) GetPathQoSMetrics(tenant string) ([]QoSMetrics, error) { client, tenant, "SDWAN", - client.lookback, + client.extendedLookback, "pathcos(localsitename,remotesitename)", "", "", @@ -557,12 +599,12 @@ func (client *Client) GetSiteMetrics(tenant string) ([]SiteMetrics, error) { // GetApplicationsByAppliance retrieves applications by appliance metrics from the Versa Analytics API func (client *Client) GetApplicationsByAppliance(tenant string) ([]ApplicationsByApplianceMetrics, error) { - // TODO: should the lookback be configurable for these? no data is returned for 30min lookback + // Uses extended lookback since no data is returned for short lookback periods return getPaginatedAnalytics( client, tenant, "SDWAN", - "1daysAgo", + client.extendedLookback, "app(site,appId)", "", "", @@ -580,12 +622,11 @@ func (client *Client) GetApplicationsByAppliance(tenant string) ([]ApplicationsB // GetTopUsers retrieves top users of applications by appliance from the Versa Analytics API func (client *Client) GetTopUsers(tenant string) ([]TopUserMetrics, error) { - // TODO: should the lookback be configurable for these? no data is returned for 30min lookback return getPaginatedAnalytics( client, tenant, "SDWAN", - "1daysAgo", + client.extendedLookback, "appUser(site,user)", "", "", @@ -611,7 +652,7 @@ func (client *Client) GetTunnelMetrics(tenant string) ([]TunnelMetrics, error) { client, tenant, "SYSTEM", - client.lookback, + client.extendedLookback, "tunnelstats(appliance,ipsecLocalIp,ipsecPeerIp,ipsecVpnProfName)", "", "", @@ -633,7 +674,7 @@ func (client *Client) GetDIAMetrics(tenant string) ([]DIAMetrics, error) { client, tenant, "SDWAN", - "1daysAgo", + client.extendedLookback, "usage(site,accckt,accckt.ip)", "(accessType:DIA)", "", @@ -718,7 +759,3 @@ func buildAnalyticsPath(tenant string, feature string, lookback string, query st } return path + "?" + params.Encode() } - -func createLookbackString(lookbackMinutes int) string { - return fmt.Sprintf("%dminutesAgo", lookbackMinutes) -} diff --git a/pkg/collector/corechecks/network-devices/versa/client/client_test.go b/pkg/collector/corechecks/network-devices/versa/client/client_test.go index cac6878a291d..4db4eaf266e8 100644 --- a/pkg/collector/corechecks/network-devices/versa/client/client_test.go +++ b/pkg/collector/corechecks/network-devices/versa/client/client_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/DataDog/datadog-agent/pkg/collector/corechecks/network-devices/versa/client/fixtures" "github.com/stretchr/testify/require" @@ -406,19 +407,21 @@ func TestGetSLAMetrics(t *testing.T) { func TestGetLinkUsageMetrics(t *testing.T) { expectedLinkUsageMetrics := []LinkUsageMetrics{ { - DrillKey: "test-branch-2B,INET-1", - Site: "test-branch-2B", - AccessCircuit: "INET-1", - UplinkBandwidth: "10000000000", - DownlinkBandwidth: "10000000000", - Type: "Unknown", - Media: "Unknown", - IP: "10.20.20.7", - ISP: "", - VolumeTx: 757144.0, - VolumeRx: 457032.0, - BandwidthTx: 6730.168888888889, - BandwidthRx: 4062.5066666666667, + DrillKey: "test-branch-2B,INET-1", + Site: "test-branch-2B", + AccessCircuit: "INET-1", + UplinkBandwidthString: "10000000000", + DownlinkBandwidthString: "10000000000", + UplinkBandwidth: 10000000000.0, + DownlinkBandwidth: 10000000000.0, + Type: "Unknown", + Media: "Unknown", + IP: "10.20.20.7", + ISP: "", + VolumeTx: 757144.0, + VolumeRx: 457032.0, + BandwidthTx: 6730.168888888889, + BandwidthRx: 4062.5066666666667, }, } server := SetupMockAPIServer() @@ -823,3 +826,198 @@ func TestGetAnalyticsInterfacesEmptyTenant(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "tenant cannot be empty") } + +func TestPaginationParameterName(t *testing.T) { + tests := []struct { + name string + useStartPagination bool + expectedParam string + }{ + { + name: "default pagination uses offset", + useStartPagination: false, + expectedParam: "offset", + }, + { + name: "feature flag enabled uses start", + useStartPagination: true, + expectedParam: "start", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authConfig := AuthConfig{ + Method: "basic", + Username: "user", + Password: "password", + } + + client, err := NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig) + require.NoError(t, err) + + client.useStartPagination = tt.useStartPagination + + param := client.getOffsetParamName() + require.Equal(t, tt.expectedParam, param) + }) + } +} + +func TestWithStartPaginationOption(t *testing.T) { + // Create mock AuthConfig + authConfig := AuthConfig{ + Method: "basic", + Username: "user", + Password: "password", + } + + // Test with feature flag disabled (default) + client, err := NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig) + require.NoError(t, err) + require.False(t, client.useStartPagination) + require.Equal(t, "offset", client.getOffsetParamName()) + + // Test with feature flag enabled + client, err = NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithStartPagination(true)) + require.NoError(t, err) + require.True(t, client.useStartPagination) + require.Equal(t, "start", client.getOffsetParamName()) + + // Test with feature flag explicitly disabled + client, err = NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithStartPagination(false)) + require.NoError(t, err) + require.False(t, client.useStartPagination) + require.Equal(t, "offset", client.getOffsetParamName()) +} + +func TestWithAlternateAppliancesOption(t *testing.T) { + authConfig := AuthConfig{ + Method: "basic", + Username: "user", + Password: "password", + } + + // Test with feature flag disabled (default) + client, err := NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig) + require.NoError(t, err) + require.False(t, client.useAlternateAppliances) + + // Test with feature flag enabled + client, err = NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithAlternateAppliances(true)) + require.NoError(t, err) + require.True(t, client.useAlternateAppliances) + + // Test with feature flag explicitly disabled + client, err = NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithAlternateAppliances(false)) + require.NoError(t, err) + require.False(t, client.useAlternateAppliances) +} + +func TestUseAlternateAppliancesFlag(t *testing.T) { + authConfig := AuthConfig{ + Method: "basic", + Username: "user", + Password: "password", + } + + t.Run("UseAlternateAppliances=false", func(t *testing.T) { + client, err := NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithAlternateAppliances(false)) + require.NoError(t, err) + require.False(t, client.useAlternateAppliances) + }) + + t.Run("UseAlternateAppliances=true", func(t *testing.T) { + client, err := NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithAlternateAppliances(true)) + require.NoError(t, err) + require.True(t, client.useAlternateAppliances) + }) +} + +func TestWithTimeout(t *testing.T) { + authConfig := AuthConfig{ + Method: "basic", + Username: "user", + Password: "password", + } + + tests := []struct { + name string + timeoutSeconds int + expectedTimeout time.Duration + }{ + { + name: "default timeout without option", + timeoutSeconds: 0, // not using WithTimeout option + expectedTimeout: defaultHTTPTimeout * time.Second, + }, + { + name: "custom timeout 5 seconds", + timeoutSeconds: 5, + expectedTimeout: 5 * time.Second, + }, + { + name: "custom timeout 30 seconds", + timeoutSeconds: 30, + expectedTimeout: 30 * time.Second, + }, + { + name: "custom timeout 1 second", + timeoutSeconds: 1, + expectedTimeout: 1 * time.Second, + }, + { + name: "custom timeout 60 seconds", + timeoutSeconds: 60, + expectedTimeout: 60 * time.Second, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var client *Client + var err error + + if tt.timeoutSeconds == 0 { + // Test default timeout (no WithTimeout option) + client, err = NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig) + } else { + // Test custom timeout with WithTimeout option + client, err = NewClient("example.com", 9182, "analytics.example.com:8443", false, authConfig, WithTimeout(tt.timeoutSeconds)) + } + + require.NoError(t, err) + require.NotNil(t, client) + require.NotNil(t, client.httpClient) + require.Equal(t, tt.expectedTimeout, client.httpClient.Timeout) + }) + } +} + +func TestWithTimeoutMultipleOptions(t *testing.T) { + authConfig := AuthConfig{ + Method: "basic", + Username: "user", + Password: "password", + } + + // Test that WithTimeout works correctly when combined with other options + client, err := NewClient( + "example.com", + 9182, + "analytics.example.com:8443", + false, + authConfig, + WithTimeout(15), + WithMaxAttempts(5), + WithMaxCount(1000), + WithStartPagination(true), + ) + + require.NoError(t, err) + require.NotNil(t, client) + require.Equal(t, 15*time.Second, client.httpClient.Timeout) + require.Equal(t, 5, client.maxAttempts) + require.Equal(t, "1000", client.maxCount) + require.True(t, client.useStartPagination) +} diff --git a/pkg/collector/corechecks/network-devices/versa/client/parser.go b/pkg/collector/corechecks/network-devices/versa/client/parser.go index f52bcbfc9f32..ab01c29ed1f3 100644 --- a/pkg/collector/corechecks/network-devices/versa/client/parser.go +++ b/pkg/collector/corechecks/network-devices/versa/client/parser.go @@ -8,6 +8,7 @@ package client import ( "fmt" + "strconv" ) // parseSLAMetrics parses the raw AaData response into SLAMetrics structs @@ -139,10 +140,10 @@ func parseLinkUsageMetrics(data [][]interface{}) ([]LinkUsageMetrics, error) { if m.AccessCircuit, ok = row[2].(string); !ok { return nil, fmt.Errorf("expected string for AccessCircuit") } - if m.UplinkBandwidth, ok = row[3].(string); !ok { + if m.UplinkBandwidthString, ok = row[3].(string); !ok { return nil, fmt.Errorf("expected string for UplinkBandwidth") } - if m.DownlinkBandwidth, ok = row[4].(string); !ok { + if m.DownlinkBandwidthString, ok = row[4].(string); !ok { return nil, fmt.Errorf("expected string for DownlinkBandwidth") } if m.Type, ok = row[5].(string); !ok { @@ -158,6 +159,18 @@ func parseLinkUsageMetrics(data [][]interface{}) ([]LinkUsageMetrics, error) { return nil, fmt.Errorf("expected string for ISP") } + // Parse bandwidth strings to numeric values + var err error + m.UplinkBandwidth, err = strconv.ParseFloat(m.UplinkBandwidthString, 64) + if err != nil { + return nil, fmt.Errorf("error parsing uplink bandwidth %q: %w", m.UplinkBandwidthString, err) + } + + m.DownlinkBandwidth, err = strconv.ParseFloat(m.DownlinkBandwidthString, 64) + if err != nil { + return nil, fmt.Errorf("error parsing downlink bandwidth %q: %w", m.DownlinkBandwidthString, err) + } + // Floats from index 9–12 floatFields := []*float64{ &m.VolumeTx, &m.VolumeRx, &m.BandwidthTx, &m.BandwidthRx, diff --git a/pkg/collector/corechecks/network-devices/versa/client/parser_test.go b/pkg/collector/corechecks/network-devices/versa/client/parser_test.go index e62bab472f75..024994b4d9ce 100644 --- a/pkg/collector/corechecks/network-devices/versa/client/parser_test.go +++ b/pkg/collector/corechecks/network-devices/versa/client/parser_test.go @@ -34,19 +34,21 @@ func TestParseLinkUsageMetrics(t *testing.T) { expected := []LinkUsageMetrics{ { - DrillKey: "test-branch-2B,INET-1", - Site: "test-branch-2B", - AccessCircuit: "INET-1", - UplinkBandwidth: "10000000000", - DownlinkBandwidth: "10000000000", - Type: "Unknown", - Media: "Unknown", - IP: "10.20.20.7", - ISP: "", - VolumeTx: 757144.0, - VolumeRx: 457032.0, - BandwidthTx: 6730.168888888889, - BandwidthRx: 4062.5066666666667, + DrillKey: "test-branch-2B,INET-1", + Site: "test-branch-2B", + AccessCircuit: "INET-1", + UplinkBandwidthString: "10000000000", + DownlinkBandwidthString: "10000000000", + UplinkBandwidth: 10000000000.0, + DownlinkBandwidth: 10000000000.0, + Type: "Unknown", + Media: "Unknown", + IP: "10.20.20.7", + ISP: "", + VolumeTx: 757144.0, + VolumeRx: 457032.0, + BandwidthTx: 6730.168888888889, + BandwidthRx: 4062.5066666666667, }, } diff --git a/pkg/collector/corechecks/network-devices/versa/client/types.go b/pkg/collector/corechecks/network-devices/versa/client/types.go index c37d50e8722d..6092629f37e1 100644 --- a/pkg/collector/corechecks/network-devices/versa/client/types.go +++ b/pkg/collector/corechecks/network-devices/versa/client/types.go @@ -418,19 +418,21 @@ type InterfaceMetrics struct { // LinkUsageMetrics represents the columns to parse from the LinkExtendedMetricsResponse type LinkUsageMetrics struct { - DrillKey string - Site string - AccessCircuit string - UplinkBandwidth string - DownlinkBandwidth string - Type string - Media string - IP string - ISP string - VolumeTx float64 - VolumeRx float64 - BandwidthTx float64 - BandwidthRx float64 + DrillKey string + Site string + AccessCircuit string + UplinkBandwidthString string + DownlinkBandwidthString string + UplinkBandwidth float64 + DownlinkBandwidth float64 + Type string + Media string + IP string + ISP string + VolumeTx float64 + VolumeRx float64 + BandwidthTx float64 + BandwidthRx float64 } // SiteMetrics represents the columns to parse from the Site metrics response diff --git a/pkg/collector/corechecks/network-devices/versa/report/sender.go b/pkg/collector/corechecks/network-devices/versa/report/sender.go index 74b0c40c0434..51a6e8ef0cc8 100644 --- a/pkg/collector/corechecks/network-devices/versa/report/sender.go +++ b/pkg/collector/corechecks/network-devices/versa/report/sender.go @@ -170,7 +170,6 @@ func (s *Sender) SendSLAMetrics(slaMetrics []client.SLAMetrics, deviceNameToIDMa func (s *Sender) SendLinkUsageMetrics(linkUsageMetrics []client.LinkUsageMetrics, deviceNameToIDMap map[string]string) { for _, linkMetric := range linkUsageMetrics { var tags = []string{ - "interface:" + linkMetric.Site, "site:" + linkMetric.Site, "access_circuit:" + linkMetric.AccessCircuit, "type:" + linkMetric.Type, @@ -181,10 +180,26 @@ func (s *Sender) SendLinkUsageMetrics(linkUsageMetrics []client.LinkUsageMetrics if deviceIP, ok := deviceNameToIDMap[linkMetric.Site]; ok { tags = append(tags, s.GetDeviceTags(defaultIPTag, deviceIP)...) } + s.Gauge(versaMetricPrefix+"link.volume_tx", linkMetric.VolumeTx, "", tags) s.Gauge(versaMetricPrefix+"link.volume_rx", linkMetric.VolumeRx, "", tags) s.Gauge(versaMetricPrefix+"link.bandwidth_tx", linkMetric.BandwidthTx, "", tags) s.Gauge(versaMetricPrefix+"link.bandwidth_rx", linkMetric.BandwidthRx, "", tags) + + // Send link speed metrics + s.Gauge(versaMetricPrefix+"link.tx_speed", linkMetric.UplinkBandwidth, "", tags) + s.Gauge(versaMetricPrefix+"link.rx_speed", linkMetric.DownlinkBandwidth, "", tags) + + // Calculate and send bandwidth utilization percentages + if linkMetric.UplinkBandwidth > 0 { + txBandwidthUsage := (linkMetric.BandwidthTx / linkMetric.UplinkBandwidth) * 100 + s.Gauge(versaMetricPrefix+"link.tx_util", txBandwidthUsage, "", tags) + } + + if linkMetric.DownlinkBandwidth > 0 { + rxBandwidthUsage := (linkMetric.BandwidthRx / linkMetric.DownlinkBandwidth) * 100 + s.Gauge(versaMetricPrefix+"link.rx_util", rxBandwidthUsage, "", tags) + } } } diff --git a/pkg/collector/corechecks/network-devices/versa/versa.go b/pkg/collector/corechecks/network-devices/versa/versa.go index 586a45af193f..f7ddffc37f17 100644 --- a/pkg/collector/corechecks/network-devices/versa/versa.go +++ b/pkg/collector/corechecks/network-devices/versa/versa.go @@ -44,8 +44,10 @@ type checkCfg struct { MaxAttempts int `yaml:"max_attempts"` MaxPages int `yaml:"max_pages"` MaxCount int `yaml:"max_count"` - LookbackTimeWindowMinutes int `yaml:"lookback_time_window_minutes"` + LookbackWindow string `yaml:"lookback_window"` + ExtendedLookbackWindow string `yaml:"extended_lookback_window"` UseHTTP bool `yaml:"use_http"` + ClientTimeout int `yaml:"client_timeout"` Insecure bool `yaml:"insecure"` CAFile string `yaml:"ca_file"` Namespace string `yaml:"namespace"` @@ -68,6 +70,8 @@ type checkCfg struct { SendInterfaceMetadataFromAnalytics *bool `yaml:"send_interface_metadata_from_analytics"` ClientID string `yaml:"client_id"` ClientSecret string `yaml:"client_secret"` + UseStartPagination *bool `yaml:"use_start_pagination"` + UseAlternateAppliancesEndpoint *bool `yaml:"use_alternate_appliances_endpoint"` } // VersaCheck contains the fields for the Versa check @@ -121,6 +125,7 @@ func (v *VersaCheck) Run() error { // Process each organization and collect all required data var appliances []client.Appliance var interfaces []client.Interface + var appliancesByOrg map[string][]client.Appliance // Determine if we need appliances for device mapping needsDeviceMapping := *v.config.SendInterfaceMetadata || *v.config.CollectDirectorInterfaceMetrics || @@ -129,21 +134,41 @@ func (v *VersaCheck) Run() error { *v.config.CollectTunnelMetrics || *v.config.CollectQoSMetrics || *v.config.CollectDIAMetrics || *v.config.CollectInterfaceMetrics - for _, org := range organizations { - log.Tracef("Processing organization: %s", org.Name) + // Collect appliances based on configuration + if *v.config.SendDeviceMetadata || *v.config.CollectHardwareMetrics || needsDeviceMapping { + appliancesByOrg = make(map[string][]client.Appliance) - // Gather appliances if we need device metadata, hardware metrics, or device mapping - if *v.config.SendDeviceMetadata || *v.config.CollectHardwareMetrics || needsDeviceMapping { - orgAppliances, err := c.GetChildAppliancesDetail(org.Name) + if *v.config.UseAlternateAppliancesEndpoint { + // Use alternate appliances endpoint and organize by org + allAppliances, err := c.GetAppliances() if err != nil { - log.Errorf("error getting appliances from organization %s: %v", org.Name, err) - } else { - for _, appliance := range orgAppliances { - log.Tracef("Processing appliance: %+v", appliance) + log.Errorf("error getting all appliances: %v", err) + } + for _, appliance := range allAppliances { + appliancesByOrg[appliance.OwnerOrg] = append(appliancesByOrg[appliance.OwnerOrg], appliance) + appliances = append(appliances, appliance) + log.Tracef("Processing appliance: %+v", appliance) + } + } else { + // Use per-organization calls (default behavior) + for _, org := range organizations { + orgAppliances, err := c.GetChildAppliancesDetail(org.Name) + if err != nil { + log.Errorf("error getting appliances from organization %s: %v", org.Name, err) + } else { + appliancesByOrg[org.Name] = orgAppliances + for _, appliance := range orgAppliances { + log.Tracef("Processing appliance: %+v", appliance) + } + appliances = append(appliances, orgAppliances...) } - appliances = append(appliances, orgAppliances...) } } + } + + // Collect interfaces per organization (this still needs to be done per org) + for _, org := range organizations { + log.Tracef("Processing organization: %s", org.Name) // Grab interfaces if we need interface metadata // Note: collect_interface_metrics depends on this data, but requires explicit send_interface_metadata enablement @@ -334,7 +359,6 @@ func (v *VersaCheck) Run() error { tunnelMetrics, err := c.GetTunnelMetrics(org.Name) if err != nil { log.Warnf("error getting tunnel metrics for tenant %s from Versa client: %v", org.Name, err) - continue } v.metricsSender.SendTunnelMetrics(tunnelMetrics, deviceNameToIDMap) } @@ -364,7 +388,6 @@ func (v *VersaCheck) Run() error { analyticsInterfaceMetrics, err := c.GetAnalyticsInterfaces(org.Name) if err != nil { log.Errorf("error getting analytics interface metrics from organization %s: %v", org.Name, err) - continue } if len(analyticsInterfaceMetrics) > 0 { @@ -429,6 +452,8 @@ func (v *VersaCheck) Configure(senderManager sender.SenderManager, integrationCo instanceConfig.CollectSiteMetrics = boolPointer(false) instanceConfig.CollectInterfaceMetrics = boolPointer(false) instanceConfig.SendInterfaceMetadataFromAnalytics = boolPointer(false) + instanceConfig.UseStartPagination = boolPointer(false) + instanceConfig.UseAlternateAppliancesEndpoint = boolPointer(false) err = yaml.Unmarshal(rawInstance, &instanceConfig) if err != nil { @@ -458,6 +483,10 @@ func (v *VersaCheck) Configure(senderManager sender.SenderManager, integrationCo func (v *VersaCheck) buildClientOptions() ([]client.ClientOptions, error) { var clientOptions []client.ClientOptions + if v.config.ClientTimeout > 0 { + clientOptions = append(clientOptions, client.WithTimeout(v.config.ClientTimeout)) + } + if v.config.Insecure || v.config.CAFile != "" { options, err := client.WithTLSConfig(v.config.Insecure, v.config.CAFile) if err != nil { @@ -479,8 +508,20 @@ func (v *VersaCheck) buildClientOptions() ([]client.ClientOptions, error) { clientOptions = append(clientOptions, client.WithMaxCount(v.config.MaxCount)) } - if v.config.LookbackTimeWindowMinutes > 0 { - clientOptions = append(clientOptions, client.WithLookback(v.config.LookbackTimeWindowMinutes)) + if v.config.LookbackWindow != "" { + clientOptions = append(clientOptions, client.WithLookback(v.config.LookbackWindow)) + } + + if v.config.ExtendedLookbackWindow != "" { + clientOptions = append(clientOptions, client.WithExtendedLookback(v.config.ExtendedLookbackWindow)) + } + + if v.config.UseStartPagination != nil && *v.config.UseStartPagination { + clientOptions = append(clientOptions, client.WithStartPagination(true)) + } + + if v.config.UseAlternateAppliancesEndpoint != nil && *v.config.UseAlternateAppliancesEndpoint { + clientOptions = append(clientOptions, client.WithAlternateAppliances(true)) } return clientOptions, nil