Skip to content

Commit

Permalink
Add Splunk scaler (#5905)
Browse files Browse the repository at this point in the history
Signed-off-by: circa10a <caleblemoine@gmail.com>
Signed-off-by: Caleb Lemoine <21261388+circa10a@users.noreply.github.com>
  • Loading branch information
circa10a committed Jul 1, 2024
1 parent 2bb1cfb commit 9a1d3d2
Show file tree
Hide file tree
Showing 7 changed files with 956 additions and 0 deletions.
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.Username == "" {
return nil, errors.New("username was not set")
}

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")
}

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

0 comments on commit 9a1d3d2

Please sign in to comment.