Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Splunk scaler #5905

Merged
merged 8 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
- **General**: Add --ca-dir flag to KEDA operator to specify directories with CA certificates for scalers to authenticate TLS connections (defaults to /custom/ca) ([#5860](https://github.com/kedacore/keda/issues/5860))
- **General**: Declarative parsing of scaler config ([#5037](https://github.com/kedacore/keda/issues/5037)|[#5797](https://github.com/kedacore/keda/issues/5797))
- **General**: Introduce new Splunk Scaler ([#5904](https://github.com/kedacore/keda/issues/5904))
- **General**: Remove deprecated Kustomize commonLabels ([#5888](https://github.com/kedacore/keda/pull/5888))
- **General**: Support for Kubernetes v1.30 ([#5828](https://github.com/kedacore/keda/issues/5828))

Expand Down
120 changes: 120 additions & 0 deletions pkg/scalers/splunk/splunk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package splunk

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/kedacore/keda/v2/pkg/scalers/scalersconfig"
kedautil "github.com/kedacore/keda/v2/pkg/util"
)

const (
savedSearchPathTemplateStr = "/servicesNS/%s/search/search/jobs/export"
)

// Config contains the information required to authenticate with a Splunk instance.
type Config struct {
Host string
Username string
Password string
APIToken string
HTTPTimeout time.Duration
UnsafeSsl bool
}

// Client contains Splunk config information as well as an http client for requests.
type Client struct {
*Config
*http.Client
}

// SearchResponse is used for unmarshalling search results.
type SearchResponse struct {
Result map[string]string `json:"result"`
}

// NewClient returns a new Splunk client.
func NewClient(c *Config, sc *scalersconfig.ScalerConfig) (*Client, error) {
if c.APIToken != "" && c.Password != "" {
return nil, errors.New("API token and Password were all set. If APIToken is set, username and password must not be used")
}

if c.APIToken != "" && c.Username == "" {
return nil, errors.New("API token was set and username was not. Username is needed to determine who owns the saved search")
}

httpClient := kedautil.CreateHTTPClient(sc.GlobalHTTPTimeout, c.UnsafeSsl)

client := &Client{
c,
httpClient,
}

return client, nil
}

// SavedSearch fetches the results of a saved search/report in Splunk.
func (c *Client) SavedSearch(name string) (*SearchResponse, error) {
savedSearchAPIPath := fmt.Sprintf(savedSearchPathTemplateStr, c.Username)
endpoint := fmt.Sprintf("%s%s", c.Host, savedSearchAPIPath)

body := strings.NewReader(fmt.Sprintf("search=savedsearch %s", name))
req, err := http.NewRequest(http.MethodPost, endpoint, body)
if err != nil {
return nil, err
}

req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if c.APIToken != "" {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIToken))
} else {
req.SetBasicAuth(c.Username, c.Password)
}

req.URL.RawQuery = url.Values{
"output_mode": {"json"},
}.Encode()

resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}

defer resp.Body.Close()

if resp.StatusCode > 399 {
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return nil, errors.New(string(bodyText))
}

result := &SearchResponse{}

err = json.NewDecoder(resp.Body).Decode(&result)

return result, err
}

// ToMetric converts a search response to a consumable metric value.
func (s *SearchResponse) ToMetric(valueField string) (float64, error) {
metricValueStr, ok := s.Result[valueField]
if !ok {
return 0, fmt.Errorf("field: %s not found in search results", valueField)
}

metricValueInt, err := strconv.ParseFloat(metricValueStr, 64)
if err != nil {
return 0, fmt.Errorf("value: %s is not a float value", valueField)
}

return metricValueInt, nil
}
Loading
Loading