diff --git a/receiver/splunksearchapireceiver/client.go b/receiver/splunksearchapireceiver/client.go index d2f2c71bf..d2d93172a 100644 --- a/receiver/splunksearchapireceiver/client.go +++ b/receiver/splunksearchapireceiver/client.go @@ -37,11 +37,13 @@ type splunkSearchAPIClient interface { } type defaultSplunkSearchAPIClient struct { - client *http.Client - endpoint string - logger *zap.Logger - username string - password string + client *http.Client + endpoint string + logger *zap.Logger + username string + password string + authToken string + tokenType string } func newSplunkSearchAPIClient(ctx context.Context, settings component.TelemetrySettings, conf Config, host component.Host) (*defaultSplunkSearchAPIClient, error) { @@ -49,12 +51,15 @@ func newSplunkSearchAPIClient(ctx context.Context, settings component.TelemetryS if err != nil { return nil, err } + return &defaultSplunkSearchAPIClient{ - client: client, - endpoint: conf.Endpoint, - logger: settings.Logger, - username: conf.Username, - password: conf.Password, + client: client, + endpoint: conf.Endpoint, + logger: settings.Logger, + username: conf.Username, + password: conf.Password, + authToken: conf.AuthToken, + tokenType: conf.TokenType, }, nil } @@ -70,7 +75,11 @@ func (c defaultSplunkSearchAPIClient) CreateSearchJob(search string) (CreateJobR if err != nil { return CreateJobResponse{}, err } - req.SetBasicAuth(c.username, c.password) + + err = c.SetSplunkRequestAuth(req) + if err != nil { + return CreateJobResponse{}, err + } resp, err := c.client.Do(req) if err != nil { @@ -102,7 +111,11 @@ func (c defaultSplunkSearchAPIClient) GetJobStatus(sid string) (SearchJobStatusR if err != nil { return SearchJobStatusResponse{}, err } - req.SetBasicAuth(c.username, c.password) + + err = c.SetSplunkRequestAuth(req) + if err != nil { + return SearchJobStatusResponse{}, err + } resp, err := c.client.Do(req) if err != nil { @@ -133,7 +146,11 @@ func (c defaultSplunkSearchAPIClient) GetSearchResults(sid string, offset int, b if err != nil { return SearchResultsResponse{}, err } - req.SetBasicAuth(c.username, c.password) + + err = c.SetSplunkRequestAuth(req) + if err != nil { + return SearchResultsResponse{}, err + } resp, err := c.client.Do(req) if err != nil { @@ -157,3 +174,18 @@ func (c defaultSplunkSearchAPIClient) GetSearchResults(sid string, offset int, b return searchResults, nil } + +func (c defaultSplunkSearchAPIClient) SetSplunkRequestAuth(req *http.Request) error { + if c.authToken != "" { + if strings.EqualFold(c.tokenType, TokenTypeBearer) { + req.Header.Set("Authorization", "Bearer "+string(c.authToken)) + } else if strings.EqualFold(c.tokenType, TokenTypeSplunk) { + req.Header.Set("Authorization", "Splunk "+string(c.authToken)) + } else { + return fmt.Errorf("auth_token provided without a correct token type, valid token types are %v", []string{TokenTypeBearer, TokenTypeSplunk}) + } + } else { + req.SetBasicAuth(c.username, c.password) + } + return nil +} diff --git a/receiver/splunksearchapireceiver/client_test.go b/receiver/splunksearchapireceiver/client_test.go index 9e3edec18..bcf64087a 100644 --- a/receiver/splunksearchapireceiver/client_test.go +++ b/receiver/splunksearchapireceiver/client_test.go @@ -98,6 +98,30 @@ func TestGetSearchResults(t *testing.T) { require.Empty(t, resp) } +func TestSetSplunkRequestAuth(t *testing.T) { + client := defaultSplunkSearchAPIClient{ + username: "user", + password: "password", + } + req := httptest.NewRequest("GET", "http://localhost:8089", nil) + client.SetSplunkRequestAuth(req) + require.Equal(t, req.Header.Get("Authorization"), "Basic dXNlcjpwYXNzd29yZA==") + + client = defaultSplunkSearchAPIClient{ + authToken: "token", + tokenType: TokenTypeBearer, + } + client.SetSplunkRequestAuth(req) + require.Equal(t, req.Header.Get("Authorization"), "Bearer token") + + client = defaultSplunkSearchAPIClient{ + authToken: "token", + tokenType: TokenTypeSplunk, + } + client.SetSplunkRequestAuth(req) + require.Equal(t, req.Header.Get("Authorization"), "Splunk token") +} + // mock Splunk servers func newMockServer() *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { diff --git a/receiver/splunksearchapireceiver/config.go b/receiver/splunksearchapireceiver/config.go index 345cd6ab9..9c15c31ca 100644 --- a/receiver/splunksearchapireceiver/config.go +++ b/receiver/splunksearchapireceiver/config.go @@ -16,6 +16,7 @@ package splunksearchapireceiver import ( "errors" + "fmt" "strings" "time" @@ -25,13 +26,19 @@ import ( var ( errNonStandaloneSearchQuery = errors.New("only standalone search commands can be used for scraping data") + // TokenTypeBearer is the token type for Bearer tokens + TokenTypeBearer = "Bearer" + // TokenTypeSplunk is the token type for Splunk tokens + TokenTypeSplunk = "Splunk" ) // Config struct to represent the configuration for the Splunk Search API receiver type Config struct { confighttp.ClientConfig `mapstructure:",squash"` - Username string `mapstructure:"splunk_username"` - Password string `mapstructure:"splunk_password"` + Username string `mapstructure:"splunk_username,omitempty"` + Password string `mapstructure:"splunk_password,omitempty"` + AuthToken string `mapstructure:"auth_token,omitempty"` + TokenType string `mapstructure:"token_type,omitempty"` Searches []Search `mapstructure:"searches"` JobPollInterval time.Duration `mapstructure:"job_poll_interval"` StorageID *component.ID `mapstructure:"storage"` @@ -51,12 +58,27 @@ func (cfg *Config) Validate() error { if cfg.Endpoint == "" { return errors.New("missing Splunk server endpoint") } - if cfg.Username == "" { - return errors.New("missing Splunk username") + + if cfg.Username == "" && cfg.AuthToken == "" { + return errors.New("missing Splunk username or auth token") + } + + if cfg.Password == "" && cfg.AuthToken == "" { + return errors.New("missing Splunk password or auth token") } - if cfg.Password == "" { - return errors.New("missing Splunk password") + + if cfg.AuthToken != "" { + if cfg.TokenType == "" { + return errors.New("auth_token provided without a token type") + } + if !strings.EqualFold(cfg.TokenType, TokenTypeBearer) && !strings.EqualFold(cfg.TokenType, TokenTypeSplunk) { + return fmt.Errorf("auth_token provided without a correct token type, valid token types are %v", []string{TokenTypeBearer, TokenTypeSplunk}) + } + if cfg.Username != "" || cfg.Password != "" { + return errors.New("auth_token and username/password were both provided, only one can be provided to authenticate with Splunk") + } } + if len(cfg.Searches) == 0 { return errors.New("at least one search must be provided") } diff --git a/receiver/splunksearchapireceiver/config_test.go b/receiver/splunksearchapireceiver/config_test.go index 90440234c..789825451 100644 --- a/receiver/splunksearchapireceiver/config_test.go +++ b/receiver/splunksearchapireceiver/config_test.go @@ -27,6 +27,8 @@ func TestValidate(t *testing.T) { endpoint string username string password string + authToken string + tokenType string storage string searches []Search errExpected bool @@ -48,7 +50,7 @@ func TestValidate(t *testing.T) { errText: "missing Splunk server endpoint", }, { - desc: "Missing username", + desc: "Missing username, no auth token", endpoint: "http://localhost:8089", password: "password", storage: "file_storage", @@ -60,10 +62,10 @@ func TestValidate(t *testing.T) { }, }, errExpected: true, - errText: "missing Splunk username", + errText: "missing Splunk username or auth token", }, { - desc: "Missing password", + desc: "Missing password, no auth token", endpoint: "http://localhost:8089", username: "user", storage: "file_storage", @@ -75,7 +77,56 @@ func TestValidate(t *testing.T) { }, }, errExpected: true, - errText: "missing Splunk password", + errText: "missing Splunk password or auth token", + }, + { + desc: "Auth token without token type", + endpoint: "http://localhost:8089", + authToken: "token", + storage: "file_storage", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "auth_token provided without a token type", + }, + { + desc: "Auth token with invalid token type", + endpoint: "http://localhost:8089", + authToken: "token", + tokenType: "invalid", + storage: "file_storage", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "auth_token provided without a correct token type, valid token types are [Bearer Splunk]", + }, + { + desc: "Auth token and username/password provided", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + authToken: "token", + tokenType: "Bearer", + storage: "file_storage", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "auth_token and username/password were both provided, only one can be provided to authenticate with Splunk", }, { desc: "Missing storage", @@ -209,6 +260,21 @@ func TestValidate(t *testing.T) { }, errExpected: false, }, + { + desc: "Valid config with auth token", + endpoint: "http://localhost:8089", + authToken: "token", + tokenType: "Bearer", + storage: "file_storage", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: false, + }, { desc: "Valid config with multiple searches", endpoint: "http://localhost:8089", @@ -268,6 +334,8 @@ func TestValidate(t *testing.T) { cfg.Endpoint = tc.endpoint cfg.Username = tc.username cfg.Password = tc.password + cfg.AuthToken = tc.authToken + cfg.TokenType = tc.tokenType cfg.Searches = tc.searches if tc.storage != "" { cfg.StorageID = &component.ID{} diff --git a/receiver/splunksearchapireceiver/receiver.go b/receiver/splunksearchapireceiver/receiver.go index e035df0e5..5f32bafc0 100644 --- a/receiver/splunksearchapireceiver/receiver.go +++ b/receiver/splunksearchapireceiver/receiver.go @@ -111,15 +111,22 @@ func (ssapir *splunksearchapireceiver) Shutdown(ctx context.Context) error { func (ssapir *splunksearchapireceiver) runQueries(ctx context.Context) error { for _, search := range ssapir.config.Searches { + // set default event batch size + if search.EventBatchSize == 0 { + search.EventBatchSize = 100 + } + // create search in Splunk searchID, err := ssapir.createSplunkSearch(search) if err != nil { ssapir.logger.Error("error creating search", zap.Error(err)) + return err } // wait for search to complete if err = ssapir.pollSearchCompletion(ctx, searchID); err != nil { ssapir.logger.Error("error polling for search completion", zap.Error(err)) + return err } for { @@ -127,6 +134,7 @@ func (ssapir *splunksearchapireceiver) runQueries(ctx context.Context) error { results, err := ssapir.getSplunkSearchResults(searchID, offset, search.EventBatchSize) if err != nil { ssapir.logger.Error("error fetching search results", zap.Error(err)) + return err } ssapir.logger.Info("search results fetched", zap.Int("num_results", len(results.Results))) @@ -198,10 +206,11 @@ func (ssapir *splunksearchapireceiver) runQueries(ctx context.Context) error { } // if the number of results is less than the results per request, we have queried all pages for the search if len(results.Results) < search.EventBatchSize { + ssapir.logger.Debug("results less than batch size, stopping search result export") break } - } + ssapir.logger.Debug("all search results exported", zap.String("query", search.Query), zap.Int("total results", exportedEvents)) } return nil