diff --git a/.gitignore b/.gitignore index 5221bfd9c..174981ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ opentelemetry-java-contrib-jmx-metrics.jar VERSION.txt release_deps /tmp +/local # OpAmp Files collector*.yaml diff --git a/docs/receivers.md b/docs/receivers.md index 14fd7e34c..10b3e41be 100644 --- a/docs/receivers.md +++ b/docs/receivers.md @@ -67,6 +67,7 @@ Below is a list of supported receivers with links to their documentation pages. | SAP Netweaver Receiver | [sapnetweaverreceiver](../receiver/sapnetweaverreceiver/README.md) | | SAPM Receiver | [sapmreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.113.0/receiver/sapmreceiver/README.md) | | SNMP Receiver | [snmpreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.113.0/receiver/snmpreceiver/README.md) | +| Splunk Search API Receiver | [splunksearchapireceiver](../receiver/splunksearchapireceiver/README.md) | | Splunk HEC Receiver | [splunkhecreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.113.0/receiver/splunkhecreceiver/README.md) | | StatsD Receiver | [statsdreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.113.0/receiver/statsdreceiver/README.md) | | SQL Query Receiver | [sqlqueryreceiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/v0.113.0/receiver/sqlqueryreceiver/README.md) | diff --git a/factories/receivers.go b/factories/receivers.go index ca3f3e282..ceb5a4317 100644 --- a/factories/receivers.go +++ b/factories/receivers.go @@ -23,6 +23,7 @@ import ( "github.com/observiq/bindplane-agent/receiver/pluginreceiver" "github.com/observiq/bindplane-agent/receiver/routereceiver" "github.com/observiq/bindplane-agent/receiver/sapnetweaverreceiver" + "github.com/observiq/bindplane-agent/receiver/splunksearchapireceiver" "github.com/observiq/bindplane-agent/receiver/telemetrygeneratorreceiver" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/activedirectorydsreceiver" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/aerospikereceiver" @@ -157,6 +158,7 @@ var defaultReceivers = []receiver.Factory{ sapnetweaverreceiver.NewFactory(), simpleprometheusreceiver.NewFactory(), snmpreceiver.NewFactory(), + splunksearchapireceiver.NewFactory(), splunkhecreceiver.NewFactory(), sqlqueryreceiver.NewFactory(), sqlserverreceiver.NewFactory(), diff --git a/go.mod b/go.mod index 9d89ad41d..e4ba0dbfb 100644 --- a/go.mod +++ b/go.mod @@ -197,6 +197,7 @@ require ( ) require ( + github.com/observiq/bindplane-agent/receiver/splunksearchapireceiver v0.0.0-00010101000000-000000000000 github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/aesprovider v0.113.0 github.com/open-telemetry/opentelemetry-collector-contrib/processor/intervalprocessor v0.113.0 go.opentelemetry.io/collector/processor/processortest v0.113.0 @@ -869,6 +870,8 @@ replace github.com/observiq/bindplane-agent/internal/report => ./internal/report replace github.com/observiq/bindplane-agent/internal/measurements => ./internal/measurements +replace github.com/observiq/bindplane-agent/receiver/splunksearchapireceiver => ./receiver/splunksearchapireceiver + // Does not build with windows and only used in configschema executable // Relevant issue https://github.com/mattn/go-ieproxy/issues/45 replace github.com/mattn/go-ieproxy => github.com/mattn/go-ieproxy v0.0.1 diff --git a/receiver/splunksearchapireceiver/README.md b/receiver/splunksearchapireceiver/README.md new file mode 100644 index 000000000..becbe410b --- /dev/null +++ b/receiver/splunksearchapireceiver/README.md @@ -0,0 +1 @@ +# Splunk Search API Receiver \ No newline at end of file diff --git a/receiver/splunksearchapireceiver/client.go b/receiver/splunksearchapireceiver/client.go new file mode 100644 index 000000000..4e6d89c7c --- /dev/null +++ b/receiver/splunksearchapireceiver/client.go @@ -0,0 +1,153 @@ +// Copyright observIQ, Inc. +// +// 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 splunksearchapireceiver contains the Splunk Search API receiver. +package splunksearchapireceiver + +import ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + + "go.opentelemetry.io/collector/component" + "go.uber.org/zap" +) + +type splunkSearchAPIClient interface { + CreateSearchJob(search string) (CreateJobResponse, error) + GetJobStatus(searchID string) (JobStatusResponse, error) + GetSearchResults(searchID string, offset int, batchSize int) (SearchResultsResponse, error) +} + +type defaultSplunkSearchAPIClient struct { + client *http.Client + endpoint string + logger *zap.Logger + username string + password string +} + +func newSplunkSearchAPIClient(ctx context.Context, settings component.TelemetrySettings, conf Config, host component.Host) (*defaultSplunkSearchAPIClient, error) { + client, err := conf.ClientConfig.ToClient(ctx, host, settings) + if err != nil { + return nil, err + } + return &defaultSplunkSearchAPIClient{ + client: client, + endpoint: conf.Endpoint, + logger: settings.Logger, + username: conf.Username, + password: conf.Password, + }, nil +} + +func (c defaultSplunkSearchAPIClient) CreateSearchJob(search string) (CreateJobResponse, error) { + endpoint := fmt.Sprintf("%s/services/search/jobs", c.endpoint) + + reqBody := fmt.Sprintf(`search=%s`, search) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte(reqBody))) + if err != nil { + return CreateJobResponse{}, err + } + req.SetBasicAuth(c.username, c.password) + + resp, err := c.client.Do(req) + if err != nil { + return CreateJobResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return CreateJobResponse{}, fmt.Errorf("failed to create search job: %d", resp.StatusCode) + } + + var jobResponse CreateJobResponse + body, err := io.ReadAll(resp.Body) + if err != nil { + return CreateJobResponse{}, fmt.Errorf("failed to read search job status response: %v", err) + } + + err = xml.Unmarshal(body, &jobResponse) + if err != nil { + return CreateJobResponse{}, fmt.Errorf("failed to unmarshal search job response: %v", err) + } + return jobResponse, nil +} + +func (c defaultSplunkSearchAPIClient) GetJobStatus(sid string) (JobStatusResponse, error) { + endpoint := fmt.Sprintf("%s/services/search/v2/jobs/%s", c.endpoint, sid) + + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return JobStatusResponse{}, err + } + req.SetBasicAuth(c.username, c.password) + + resp, err := c.client.Do(req) + if err != nil { + return JobStatusResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return JobStatusResponse{}, fmt.Errorf("failed to get search job status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return JobStatusResponse{}, fmt.Errorf("failed to read search job status response: %v", err) + } + var jobStatusResponse JobStatusResponse + err = xml.Unmarshal(body, &jobStatusResponse) + if err != nil { + return JobStatusResponse{}, fmt.Errorf("failed to unmarshal search job response: %v", err) + } + + return jobStatusResponse, nil +} + +func (c defaultSplunkSearchAPIClient) GetSearchResults(sid string, offset int, batchSize int) (SearchResultsResponse, error) { + endpoint := fmt.Sprintf("%s/services/search/v2/jobs/%s/results?output_mode=json&offset=%d&count=%d", c.endpoint, sid, offset, batchSize) + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return SearchResultsResponse{}, err + } + req.SetBasicAuth(c.username, c.password) + + resp, err := c.client.Do(req) + if err != nil { + return SearchResultsResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return SearchResultsResponse{}, fmt.Errorf("failed to get search job results: %d", resp.StatusCode) + } + + var searchResults SearchResultsResponse + body, err := io.ReadAll(resp.Body) + if err != nil { + return SearchResultsResponse{}, fmt.Errorf("failed to read search job results response: %v", err) + } + err = json.Unmarshal(body, &searchResults) + if err != nil { + return SearchResultsResponse{}, fmt.Errorf("failed to unmarshal search job results: %v", err) + } + + return searchResults, nil +} diff --git a/receiver/splunksearchapireceiver/config.go b/receiver/splunksearchapireceiver/config.go new file mode 100644 index 000000000..3a164bc91 --- /dev/null +++ b/receiver/splunksearchapireceiver/config.go @@ -0,0 +1,101 @@ +// Copyright observIQ, Inc. +// +// 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 splunksearchapireceiver + +import ( + "errors" + "fmt" + "strings" + "time" + + "go.opentelemetry.io/collector/config/confighttp" +) + +var ( + errNonStandaloneSearchQuery = errors.New("only standalone search commands can be used for scraping data") +) + +// 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"` + Searches []Search `mapstructure:"searches"` + JobPollInterval time.Duration `mapstructure:"job_poll_interval"` +} + +// Search struct to represent a Splunk search +type Search struct { + Query string `mapstructure:"query"` + EarliestTime string `mapstructure:"earliest_time"` + LatestTime string `mapstructure:"latest_time"` + Limit int `mapstructure:"limit"` + EventBatchSize int `mapstructure:"event_batch_size"` +} + +// Validate validates the Splunk Search API receiver configuration +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.Password == "" { + return errors.New("missing Splunk password") + } + if len(cfg.Searches) == 0 { + return errors.New("at least one search must be provided") + } + + for _, search := range cfg.Searches { + if search.Query == "" { + return errors.New("missing query in search") + } + + // query implicitly starts with "search" command + if !strings.HasPrefix(search.Query, "search ") { + return errNonStandaloneSearchQuery + } + + if strings.Contains(search.Query, "|") { + return errNonStandaloneSearchQuery + } + + if strings.Contains(search.Query, "earliest=") || strings.Contains(search.Query, "latest=") { + return fmt.Errorf("time query parameters must be configured using only the \"earliest_time\" and \"latest_time\" configuration parameters") + } + + if search.EarliestTime == "" { + return errors.New("missing earliest_time in search") + } + if search.LatestTime == "" { + return errors.New("missing latest_time in search") + } + + // parse time strings to time.Time + _, err := time.Parse(time.RFC3339, search.EarliestTime) + if err != nil { + return errors.New("earliest_time failed to parse as RFC3339") + } + + _, err = time.Parse(time.RFC3339, search.LatestTime) + if err != nil { + return errors.New("latest_time failed to parse as RFC3339") + } + + } + return nil +} diff --git a/receiver/splunksearchapireceiver/config_test.go b/receiver/splunksearchapireceiver/config_test.go new file mode 100644 index 000000000..9e0741fa8 --- /dev/null +++ b/receiver/splunksearchapireceiver/config_test.go @@ -0,0 +1,234 @@ +// Copyright observIQ, Inc. +// +// 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 splunksearchapireceiver + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidate(t *testing.T) { + testCases := []struct { + desc string + endpoint string + username string + password string + searches []Search + errExpected bool + errText string + }{ + { + desc: "Missing endpoint", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "missing Splunk server endpoint", + }, + { + desc: "Missing username", + endpoint: "http://localhost:8089", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "missing Splunk username", + }, + { + desc: "Missing password", + endpoint: "http://localhost:8089", + username: "user", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "missing Splunk password", + }, + { + desc: "Missing searches", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + errExpected: true, + errText: "at least one search must be provided", + }, + { + desc: "Missing query", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "missing query in search", + }, + { + desc: "Missing earliest_time", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "missing earliest_time in search", + }, + { + desc: "Unparsable earliest_time", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "-1hr", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "earliest_time failed to parse as RFC3339", + }, + { + desc: "Missing latest_time", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + }, + }, + errExpected: true, + errText: "missing latest_time in search", + }, + { + desc: "Unparsable latest_time", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "-1hr", + }, + }, + errExpected: true, + errText: "latest_time failed to parse as RFC3339", + }, + { + desc: "Invalid query chaining", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal | stats count by sourcetype", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: true, + errText: "only standalone search commands can be used for scraping data", + }, + { + desc: "Valid config", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + 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", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + { + Query: "search index=_audit", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + }, + }, + errExpected: false, + }, + { + desc: "Valid config with limit", + endpoint: "http://localhost:8089", + username: "user", + password: "password", + searches: []Search{ + { + Query: "search index=_internal", + EarliestTime: "2024-10-30T04:00:00.000Z", + LatestTime: "2024-10-30T14:00:00.000Z", + Limit: 10, + }, + }, + errExpected: false, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + cfg := NewFactory().CreateDefaultConfig().(*Config) + cfg.Endpoint = tc.endpoint + cfg.Username = tc.username + cfg.Password = tc.password + cfg.Searches = tc.searches + err := cfg.Validate() + if tc.errExpected { + require.EqualError(t, err, tc.errText) + return + } + require.NoError(t, err) + }) + } +} diff --git a/receiver/splunksearchapireceiver/factory.go b/receiver/splunksearchapireceiver/factory.go new file mode 100644 index 000000000..dc61db414 --- /dev/null +++ b/receiver/splunksearchapireceiver/factory.go @@ -0,0 +1,56 @@ +// Copyright observIQ, Inc. +// +// 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 splunksearchapireceiver + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confighttp" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" +) + +var ( + typeStr = component.MustNewType("splunksearchapi") +) + +func createDefaultConfig() component.Config { + return &Config{ + ClientConfig: confighttp.NewDefaultClientConfig(), + JobPollInterval: 10 * time.Second, + } +} + +func createLogsReceiver(_ context.Context, + params receiver.Settings, + cfg component.Config, + consumer consumer.Logs, +) (receiver.Logs, error) { + ssapirConfig := cfg.(*Config) + ssapir := &splunksearchapireceiver{ + logger: params.Logger, + logsConsumer: consumer, + config: ssapirConfig, + settings: params.TelemetrySettings, + } + return ssapir, nil +} + +// NewFactory creates a factory for Splunk Search API receiver +func NewFactory() receiver.Factory { + return receiver.NewFactory(typeStr, createDefaultConfig, receiver.WithLogs(createLogsReceiver, component.StabilityLevelAlpha)) +} diff --git a/receiver/splunksearchapireceiver/go.mod b/receiver/splunksearchapireceiver/go.mod new file mode 100644 index 000000000..c419d11af --- /dev/null +++ b/receiver/splunksearchapireceiver/go.mod @@ -0,0 +1,55 @@ +module github.com/open-telemetry/opentelemtry-collector-contrib/receiver/splunksearchapireceiver + +go 1.22.5 + +require ( + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/component v0.113.0 + go.opentelemetry.io/collector/consumer v0.113.0 + go.opentelemetry.io/collector/pdata v1.19.0 + go.opentelemetry.io/collector/receiver v0.112.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.11.1 // indirect + go.opentelemetry.io/collector/client v1.19.0 // indirect + go.opentelemetry.io/collector/config/configauth v0.113.0 // indirect + go.opentelemetry.io/collector/config/configcompression v1.19.0 // indirect + go.opentelemetry.io/collector/config/configopaque v1.19.0 // indirect + go.opentelemetry.io/collector/config/configtls v1.19.0 // indirect + go.opentelemetry.io/collector/config/internal v0.113.0 // indirect + go.opentelemetry.io/collector/extension v0.113.0 // indirect + go.opentelemetry.io/collector/extension/auth v0.113.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/gogo/protobuf v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + go.opentelemetry.io/collector/config/confighttp v0.113.0 + go.opentelemetry.io/collector/config/configtelemetry v0.113.0 // indirect + go.opentelemetry.io/collector/pipeline v0.112.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect +) diff --git a/receiver/splunksearchapireceiver/go.sum b/receiver/splunksearchapireceiver/go.sum new file mode 100644 index 000000000..a061e9529 --- /dev/null +++ b/receiver/splunksearchapireceiver/go.sum @@ -0,0 +1,146 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector/client v1.19.0 h1:TUal8WV1agTrZStgE7BJ8ZC0IHLGtrfgO9ogU9t1mv8= +go.opentelemetry.io/collector/client v1.19.0/go.mod h1:jgiXMEM6l8L2QEyf2I/M47Zd8+G7e4z+6H8q5SkHOlQ= +go.opentelemetry.io/collector/component v0.113.0 h1:/nx+RvZgxUEXP+YcTj69rEtuSEGkfaCyp/ad5zQGLjU= +go.opentelemetry.io/collector/component v0.113.0/go.mod h1:2T779hIGHU9i7xbXbV3q1/JnRw2FyzUYXW2vq47A6EU= +go.opentelemetry.io/collector/config/configauth v0.113.0 h1:CBz43fGpN41MwLdwe3mw/XVSIDvGRMT8aaaPuqKukTU= +go.opentelemetry.io/collector/config/configauth v0.113.0/go.mod h1:Q8SlxrIvL3FJO51hXa4n9ARvox04lK8mmpjf4b3UNAU= +go.opentelemetry.io/collector/config/configcompression v1.19.0 h1:bTSjTLhnPXX1NSFM6GzguEM/NBe8QUPsXHc9kMOAJzE= +go.opentelemetry.io/collector/config/configcompression v1.19.0/go.mod h1:pnxkFCLUZLKWzYJvfSwZnPrnm0twX14CYj2ADth5xiU= +go.opentelemetry.io/collector/config/confighttp v0.113.0 h1:a6iO0y1ZM5CPDvwbryzU+GpqAtAQ3eSfNseoAUogw7c= +go.opentelemetry.io/collector/config/confighttp v0.113.0/go.mod h1:JZ9EwoiWMIrXt5v+d/q54TeUhPdAoLDimSEqTtddW6E= +go.opentelemetry.io/collector/config/configopaque v1.19.0 h1:7uvntQeAAtqCaeiS2dDGrT1wLPhWvDlEsD3SliA/koQ= +go.opentelemetry.io/collector/config/configopaque v1.19.0/go.mod h1:6zlLIyOoRpJJ+0bEKrlZOZon3rOp5Jrz9fMdR4twOS4= +go.opentelemetry.io/collector/config/configtelemetry v0.113.0 h1:hweTRrVddnUeA3k7HzRY4oUR9lRdMa7of3mHNUS5YyA= +go.opentelemetry.io/collector/config/configtelemetry v0.113.0/go.mod h1:R0MBUxjSMVMIhljuDHWIygzzJWQyZHXXWIgQNxcFwhc= +go.opentelemetry.io/collector/config/configtls v1.19.0 h1:GQ/cF1hgNqHVBq2oSSrOFXxVCyMDyd5kq4R/RMEbL98= +go.opentelemetry.io/collector/config/configtls v1.19.0/go.mod h1:1hyqnYB3JqEUlk1ME/s9HYz4oCRcxQCRxsJitFFT/cA= +go.opentelemetry.io/collector/config/internal v0.113.0 h1:9RAzH8v7ItFT1npHpvP0SvUzBHcZDliCGRo9Spp6v7c= +go.opentelemetry.io/collector/config/internal v0.113.0/go.mod h1:yC7E4h1Uj0SubxcFImh6OvBHFTjMh99+A5PuyIgDWqc= +go.opentelemetry.io/collector/consumer v0.113.0 h1:KJSiK5vSIY9dgPxwKfQ3gOgKtQsqc+7IB7mGhUAL5c8= +go.opentelemetry.io/collector/consumer v0.113.0/go.mod h1:zHMlXYFaJlZoLCBR6UwWoyXZ/adcO1u2ydqUal3VmYU= +go.opentelemetry.io/collector/consumer/consumerprofiles v0.112.0 h1:ym+QxemlbWwfMSUto1hRTfcZeYbj2q8FpMzjk8O+X60= +go.opentelemetry.io/collector/consumer/consumerprofiles v0.112.0/go.mod h1:4PjDUpURFh85R6NLEHrEf/uZjpk4LAYmmOrqu+iZsyE= +go.opentelemetry.io/collector/consumer/consumertest v0.112.0 h1:pGvNH+H4rMygUOql6ynVQim6UFdimTiJ0HRfQL6v0GE= +go.opentelemetry.io/collector/consumer/consumertest v0.112.0/go.mod h1:rfVo0tYt/BaLWw3IaQKVQafjUlMsA5qTkvsSOfFrr9c= +go.opentelemetry.io/collector/extension v0.113.0 h1:Vp/YSL8ZCkJQrP1lf2Bm5yaTvcp6ROO3AnfuSL3GEXM= +go.opentelemetry.io/collector/extension v0.113.0/go.mod h1:Pwp0TNqdHeER4V1I6H6oCvrto/riiOAqs3737BWCnjw= +go.opentelemetry.io/collector/extension/auth v0.113.0 h1:4ggRy1vepOabUiCWfU+6M9P/ftXojMUNAvBpeLihYj8= +go.opentelemetry.io/collector/extension/auth v0.113.0/go.mod h1:VbvAm2YZAqePkWgwn0m0vBaq3aC49CxPVwHmrJ24aeQ= +go.opentelemetry.io/collector/pdata v1.19.0 h1:jmnU5R8TOCbwRr4B8sjdRxM7L5WnEKlQWX1dtLYxIbE= +go.opentelemetry.io/collector/pdata v1.19.0/go.mod h1:Ox1YVLe87cZDB/TL30i4SUz1cA5s6AM6SpFMfY61ICs= +go.opentelemetry.io/collector/pdata/pprofile v0.112.0 h1:t+LYorcMqZ3sDz5/jp3xU2l5lIhIXuIOOGO4Ef9CG2c= +go.opentelemetry.io/collector/pdata/pprofile v0.112.0/go.mod h1:F2aTCoDzIaxEUK1g92LZvMwradySFMo3ZsAnBIpOdUg= +go.opentelemetry.io/collector/pipeline v0.112.0 h1:jqKDdb8k53OLPibvxzX6fmMec0ZHAtqe4p2+cuHclEI= +go.opentelemetry.io/collector/pipeline v0.112.0/go.mod h1:4vOvjVsoYTHVGTbfFwqfnQOSV2K3RKUHofh3jNRc2Mg= +go.opentelemetry.io/collector/receiver v0.112.0 h1:gdTBDOPGKMZlZghtN5A7ZLNlNwCHWYcoJQeIiXvyGEQ= +go.opentelemetry.io/collector/receiver v0.112.0/go.mod h1:3QmfSUiyFzRTnHUqF8fyEvQpU5q/xuwS43jGt8JXEEA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/receiver/splunksearchapireceiver/metadata.yaml b/receiver/splunksearchapireceiver/metadata.yaml new file mode 100644 index 000000000..3ca815db9 --- /dev/null +++ b/receiver/splunksearchapireceiver/metadata.yaml @@ -0,0 +1,7 @@ +type: splunksearchapi + +status: + class: receiver + stability: + alpha: [logs] + distributions: [observiq] diff --git a/receiver/splunksearchapireceiver/model.go b/receiver/splunksearchapireceiver/model.go new file mode 100644 index 000000000..61b6e7691 --- /dev/null +++ b/receiver/splunksearchapireceiver/model.go @@ -0,0 +1,60 @@ +// Copyright observIQ, Inc. +// +// 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 splunksearchapireceiver + +// CreateJobResponse struct to represent the XML response from Splunk create job endpoint +// https://docs.splunk.com/Documentation/Splunk/9.3.1/RESTREF/RESTsearch#search.2Fjobs +type CreateJobResponse struct { + SID string `xml:"sid"` +} + +// JobStatusResponse struct to represent the XML response from Splunk job status endpoint +// https://docs.splunk.com/Documentation/Splunk/9.3.1/RESTREF/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D +type JobStatusResponse struct { + Content struct { + Type string `xml:"type,attr"` + Dict Dict `xml:"dict"` + } `xml:"content"` +} + +// Dict struct to represent elements +type Dict struct { + Keys []Key `xml:"key"` +} + +// Key struct to represent elements +type Key struct { + Name string `xml:"name,attr"` + Value string `xml:",chardata"` + Dict *Dict `xml:"dict,omitempty"` + List *List `xml:"list,omitempty"` +} + +// List struct to represent elements +type List struct { + Items []struct { + Value string `xml:",chardata"` + } `xml:"item"` +} + +// SearchResultsResponse struct to represent the JSON response from Splunk search results endpoint +// https://docs.splunk.com/Documentation/Splunk/9.3.1/RESTREF/RESTsearch#search.2Fv2.2Fjobs.2F.7Bsearch_id.7D.2Fresults +type SearchResultsResponse struct { + InitOffset int `json:"init_offset"` + Results []struct { + Raw string `json:"_raw"` + Time string `json:"_time"` + } `json:"results"` +} diff --git a/receiver/splunksearchapireceiver/receiver.go b/receiver/splunksearchapireceiver/receiver.go new file mode 100644 index 000000000..1eef408d3 --- /dev/null +++ b/receiver/splunksearchapireceiver/receiver.go @@ -0,0 +1,203 @@ +// Copyright observIQ, Inc. +// +// 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 splunksearchapireceiver + +import ( + "context" + "fmt" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.uber.org/zap" +) + +var ( + offset = 0 // offset for pagination and checkpointing + exportedEvents = 0 // track the number of events returned by the results endpoint that are exported + limitReached = false // flag to stop processing search results when limit is reached +) + +type splunksearchapireceiver struct { + host component.Host + logger *zap.Logger + logsConsumer consumer.Logs + config *Config + settings component.TelemetrySettings + client splunkSearchAPIClient +} + +func (ssapir *splunksearchapireceiver) Start(ctx context.Context, host component.Host) error { + ssapir.host = host + var err error + ssapir.client, err = newSplunkSearchAPIClient(ctx, ssapir.settings, *ssapir.config, ssapir.host) + if err != nil { + return err + } + go ssapir.runQueries(ctx) + return nil +} + +func (ssapir *splunksearchapireceiver) Shutdown(_ context.Context) error { + return nil +} + +func (ssapir *splunksearchapireceiver) runQueries(ctx context.Context) error { + for _, search := range ssapir.config.Searches { + // create search in Splunk + ssapir.logger.Info("creating search", zap.String("query", search.Query)) + searchID, err := ssapir.createSplunkSearch(search.Query) + if err != nil { + ssapir.logger.Error("error creating search", zap.Error(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)) + } + + for { + ssapir.logger.Info("fetching search results") + results, err := ssapir.getSplunkSearchResults(searchID, offset, search.EventBatchSize) + if err != nil { + ssapir.logger.Error("error fetching search results", zap.Error(err)) + } + ssapir.logger.Info("search results fetched", zap.Int("num_results", len(results.Results))) + + // parse time strings to time.Time + earliestTime, err := time.Parse(time.RFC3339, search.EarliestTime) + if err != nil { + ssapir.logger.Error("earliest_time failed to be parsed as RFC3339", zap.Error(err)) + } + + latestTime, err := time.Parse(time.RFC3339, search.LatestTime) + if err != nil { + ssapir.logger.Error("latest_time failed to be parsed as RFC3339", zap.Error(err)) + } + + logs := plog.NewLogs() + for idx, splunkLog := range results.Results { + if (idx+exportedEvents) >= search.Limit && search.Limit != 0 { + limitReached = true + break + } + // convert log timestamp to ISO8601 (UTC() makes RFC3339 into ISO8601) + logTimestamp, err := time.Parse(time.RFC3339, splunkLog.Time) + if err != nil { + ssapir.logger.Error("error parsing log timestamp", zap.Error(err)) + break + } + if logTimestamp.UTC().After(latestTime.UTC()) { + ssapir.logger.Info("skipping log entry - timestamp after latestTime", zap.Time("time", logTimestamp.UTC()), zap.Time("latestTime", latestTime.UTC())) + // logger will only log up to 10 times for a given code block, known weird behavior + continue + } + if logTimestamp.UTC().Before(earliestTime) { + ssapir.logger.Info("skipping log entry - timestamp before earliestTime", zap.Time("time", logTimestamp.UTC()), zap.Time("earliestTime", earliestTime.UTC())) + break + } + log := logs.ResourceLogs().AppendEmpty().ScopeLogs().AppendEmpty().LogRecords().AppendEmpty() + + // convert time to timestamp + timestamp := pcommon.NewTimestampFromTime(logTimestamp.UTC()) + log.SetTimestamp(timestamp) + log.Body().SetStr(splunkLog.Raw) + + if logs.ResourceLogs().Len() == 0 { + ssapir.logger.Info("search returned no logs within the given time range") + return nil + } + } + + // pass logs, wait for exporter to confirm successful export to GCP + err = ssapir.logsConsumer.ConsumeLogs(ctx, logs) + if err != nil { + // Error from down the pipeline, freak out + ssapir.logger.Error("error consuming logs", zap.Error(err)) + } + if limitReached { + ssapir.logger.Info("limit reached, stopping search result export") + exportedEvents += logs.ResourceLogs().Len() + break + } + // 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 { + exportedEvents += len(results.Results) + break + } + exportedEvents += logs.ResourceLogs().Len() + offset += len(results.Results) + } + ssapir.logger.Info("search results exported", zap.String("query", search.Query), zap.Int("total results", exportedEvents)) + } + return nil +} + +func (ssapir *splunksearchapireceiver) pollSearchCompletion(ctx context.Context, searchID string) error { + t := time.NewTicker(ssapir.config.JobPollInterval) + defer t.Stop() + for { + select { + case <-t.C: + ssapir.logger.Debug("polling for search completion") + done, err := ssapir.isSearchCompleted(searchID) + if err != nil { + return fmt.Errorf("error polling for search completion: %v", err) + } + if done { + ssapir.logger.Info("search completed") + return nil + } + ssapir.logger.Debug("search not completed yet") + case <-ctx.Done(): + return nil + } + } +} + +func (ssapir *splunksearchapireceiver) createSplunkSearch(search string) (string, error) { + resp, err := ssapir.client.CreateSearchJob(search) + if err != nil { + return "", err + } + return resp.SID, nil +} + +func (ssapir *splunksearchapireceiver) isSearchCompleted(sid string) (bool, error) { + resp, err := ssapir.client.GetJobStatus(sid) + if err != nil { + return false, err + } + + for _, key := range resp.Content.Dict.Keys { + if key.Name == "dispatchState" { + if key.Value == "DONE" { + return true, nil + } + break + } + } + return false, nil +} + +func (ssapir *splunksearchapireceiver) getSplunkSearchResults(sid string, offset int, batchSize int) (SearchResultsResponse, error) { + resp, err := ssapir.client.GetSearchResults(sid, offset, batchSize) + if err != nil { + return SearchResultsResponse{}, err + } + return resp, nil +}