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

Refactor New Relic scaler #6326

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
147 changes: 38 additions & 109 deletions pkg/scalers/newrelic_scaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package scalers
import (
"context"
"fmt"
"log"
"strconv"

"github.com/go-logr/logr"
"github.com/newrelic/newrelic-client-go/newrelic"
Expand All @@ -17,31 +15,25 @@ import (
)

const (
account = "account"
queryKeyParamater = "queryKey"
regionParameter = "region"
nrql = "nrql"
threshold = "threshold"
noDataError = "noDataError"
scalerName = "new-relic"
scalerName = "new-relic"
)

type newrelicScaler struct {
metricType v2.MetricTargetType
metadata *newrelicMetadata
metadata newrelicMetadata
nrClient *newrelic.NewRelic
logger logr.Logger
}

type newrelicMetadata struct {
account int
region string
queryKey string
noDataError bool
nrql string
threshold float64
activationThreshold float64
triggerIndex int
Account int `keda:"name=account, order=authParams;triggerMetadata"`
Region string `keda:"name=region, order=authParams;triggerMetadata, default=US"`
QueryKey string `keda:"name=queryKey, order=authParams;triggerMetadata"`
NoDataError bool `keda:"name=noDataError, order=triggerMetadata, default=false"`
NRQL string `keda:"name=nrql, order=triggerMetadata"`
Threshold float64 `keda:"name=threshold, order=triggerMetadata"`
ActivationThreshold float64 `keda:"name=activationThreshold, order=triggerMetadata, default=0"`
TriggerIndex int
}

func NewNewRelicScaler(config *scalersconfig.ScalerConfig) (Scaler, error) {
Expand All @@ -52,128 +44,69 @@ func NewNewRelicScaler(config *scalersconfig.ScalerConfig) (Scaler, error) {

logger := InitializeLogger(config, fmt.Sprintf("%s_scaler", scalerName))

meta, err := parseNewRelicMetadata(config, logger)
meta, err := parseNewRelicMetadata(config)
if err != nil {
return nil, fmt.Errorf("error parsing %s metadata: %w", scalerName, err)
}

nrClient, err := newrelic.New(
newrelic.ConfigPersonalAPIKey(meta.queryKey),
newrelic.ConfigRegion(meta.region))

newrelic.ConfigPersonalAPIKey(meta.QueryKey),
newrelic.ConfigRegion(meta.Region))
if err != nil {
log.Fatal("error initializing client:", err)
return nil, fmt.Errorf("error initializing client: %w", err)
}

logMsg := fmt.Sprintf("Initializing New Relic Scaler (account %d in region %s)", meta.account, meta.region)

logger.Info(logMsg)
logger.Info(fmt.Sprintf("Initializing New Relic Scaler (account %d in region %s)", meta.Account, meta.Region))

return &newrelicScaler{
metricType: metricType,
metadata: meta,
nrClient: nrClient,
logger: logger}, nil
logger: logger,
}, nil
}

func parseNewRelicMetadata(config *scalersconfig.ScalerConfig, logger logr.Logger) (*newrelicMetadata, error) {
func parseNewRelicMetadata(config *scalersconfig.ScalerConfig) (newrelicMetadata, error) {
meta := newrelicMetadata{}
var err error

val, err := GetFromAuthOrMeta(config, account)
if err != nil {
return nil, err
}

t, err := strconv.Atoi(val)
if err != nil {
return nil, fmt.Errorf("error parsing %s: %w", account, err)
}
meta.account = t

if val, ok := config.TriggerMetadata[nrql]; ok && val != "" {
meta.nrql = val
} else {
return nil, fmt.Errorf("no %s given", nrql)
}

queryKey, err := GetFromAuthOrMeta(config, queryKeyParamater)
if err != nil {
return nil, err
}
meta.queryKey = queryKey

region, err := GetFromAuthOrMeta(config, regionParameter)
err := config.TypedConfig(&meta)
if err != nil {
region = "US"
logger.Info("Using default 'US' region")
return meta, fmt.Errorf("error parsing newrelic metadata: %w", err)
}
meta.region = region

if val, ok := config.TriggerMetadata[threshold]; ok && val != "" {
t, err := strconv.ParseFloat(val, 64)
if err != nil {
return nil, fmt.Errorf("error parsing %s", threshold)
}
meta.threshold = t
} else {
if config.AsMetricSource {
meta.threshold = 0
} else {
return nil, fmt.Errorf("missing %s value", threshold)
}
if config.AsMetricSource {
meta.Threshold = 0
}

meta.activationThreshold = 0
if val, ok := config.TriggerMetadata["activationThreshold"]; ok {
activationThreshold, err := strconv.ParseFloat(val, 64)
if err != nil {
return nil, fmt.Errorf("queryValue parsing error %w", err)
}
meta.activationThreshold = activationThreshold
}

// If Query Return an Empty Data , shall we treat it as an error or not
// default is NO error is returned when query result is empty/no data
if val, ok := config.TriggerMetadata[noDataError]; ok {
noDataError, err := strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf("noDataError has invalid value")
}
meta.noDataError = noDataError
} else {
meta.noDataError = false
}
meta.triggerIndex = config.TriggerIndex
return &meta, nil
meta.TriggerIndex = config.TriggerIndex
return meta, nil
}

func (s *newrelicScaler) Close(context.Context) error {
return nil
}

func (s *newrelicScaler) executeNewRelicQuery(ctx context.Context) (float64, error) {
nrdbQuery := nrdb.NRQL(s.metadata.nrql)
resp, err := s.nrClient.Nrdb.QueryWithContext(ctx, s.metadata.account, nrdbQuery)
nrdbQuery := nrdb.NRQL(s.metadata.NRQL)
resp, err := s.nrClient.Nrdb.QueryWithContext(ctx, s.metadata.Account, nrdbQuery)
if err != nil {
return 0, fmt.Errorf("error running NRQL %s (%s)", s.metadata.nrql, err.Error())
return 0, fmt.Errorf("error running NRQL %s: %w", s.metadata.NRQL, err)
}
// Check for empty results set, as New Relic lib does not report these as errors

if len(resp.Results) == 0 {
if s.metadata.noDataError {
return 0, fmt.Errorf("query return no results %s", s.metadata.nrql)
if s.metadata.NoDataError {
return 0, fmt.Errorf("query returned no results: %s", s.metadata.NRQL)
}
return 0, nil
}
// Only use the first result from the query, as the query should not be multi row
for _, v := range resp.Results[0] {
val, ok := v.(float64)
if ok {
if val, ok := v.(float64); ok {
return val, nil
}
}
if s.metadata.noDataError {
return 0, fmt.Errorf("query return no results %s", s.metadata.nrql)

if s.metadata.NoDataError {
return 0, fmt.Errorf("query returned no numeric results: %s", s.metadata.NRQL)
}
return 0, nil
}
Expand All @@ -186,21 +119,17 @@ func (s *newrelicScaler) GetMetricsAndActivity(ctx context.Context, metricName s
}

metric := GenerateMetricInMili(metricName, val)

return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.activationThreshold, nil
return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.ActivationThreshold, nil
}

func (s *newrelicScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec {
metricName := kedautil.NormalizeString(scalerName)

externalMetric := &v2.ExternalMetricSource{
Metric: v2.MetricIdentifier{
Name: GenerateMetricNameWithIndex(s.metadata.triggerIndex, metricName),
Name: GenerateMetricNameWithIndex(s.metadata.TriggerIndex, metricName),
},
Target: GetMetricTargetMili(s.metricType, s.metadata.threshold),
}
metricSpec := v2.MetricSpec{
External: externalMetric, Type: externalMetricType,
Target: GetMetricTargetMili(s.metricType, s.metadata.Threshold),
}
metricSpec := v2.MetricSpec{External: externalMetric, Type: externalMetricType}
return []v2.MetricSpec{metricSpec}
}
43 changes: 28 additions & 15 deletions pkg/scalers/newrelic_scaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/go-logr/logr"
v2 "k8s.io/api/autoscaling/v2"

"github.com/kedacore/keda/v2/pkg/scalers/scalersconfig"
)
Expand All @@ -26,7 +27,7 @@ var testNewRelicMetadata = []parseNewRelicMetadataTestData{
{map[string]string{}, map[string]string{}, true},
// all properly formed
{map[string]string{"account": "0", "threshold": "100", "queryKey": "somekey", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{}, false},
// all properly formed
// all properly formed with region and activationThreshold
{map[string]string{"account": "0", "region": "EU", "threshold": "100", "activationThreshold": "20", "queryKey": "somekey", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{}, false},
// account passed via auth params
{map[string]string{"region": "EU", "threshold": "100", "queryKey": "somekey", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{"account": "0"}, false},
Expand All @@ -48,7 +49,7 @@ var testNewRelicMetadata = []parseNewRelicMetadataTestData{
{map[string]string{"account": "0", "threshold": "100", "queryKey": "somekey"}, map[string]string{}, true},
// noDataError invalid value
{map[string]string{"account": "0", "threshold": "100", "queryKey": "somekey", "noDataError": "invalid", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{}, true},
// noDataError valid value
// noDataError valid values
{map[string]string{"account": "0", "threshold": "100", "queryKey": "somekey", "noDataError": "true", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{}, false},
{map[string]string{"account": "0", "threshold": "100", "queryKey": "somekey", "noDataError": "false", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{}, false},
{map[string]string{"account": "0", "threshold": "100", "queryKey": "somekey", "noDataError": "0", "nrql": "SELECT average(cpuUsedCores) as result FROM K8sContainerSample WHERE containerName='coredns'"}, map[string]string{}, false},
Expand All @@ -61,27 +62,39 @@ var newrelicMetricIdentifiers = []newrelicMetricIdentifier{
}

func TestNewRelicParseMetadata(t *testing.T) {
for _, testData := range testNewRelicMetadata {
_, err := parseNewRelicMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadata, AuthParams: testData.authParams}, logr.Discard())
if err != nil && !testData.isError {
fmt.Printf("X: %s", testData.metadata)
t.Error("Expected success but got error", err)
}
if testData.isError && err == nil {
fmt.Printf("X: %s", testData.metadata)
t.Error("Expected error but got success")
}
for i, testData := range testNewRelicMetadata {
t.Run(fmt.Sprintf("test case %d", i), func(t *testing.T) {
_, err := parseNewRelicMetadata(&scalersconfig.ScalerConfig{
TriggerMetadata: testData.metadata,
AuthParams: testData.authParams,
})
if err != nil && !testData.isError {
t.Errorf("Test case %d: Expected success but got error: %v\nMetadata: %v\nAuthParams: %v",
i, err, testData.metadata, testData.authParams)
}
if testData.isError && err == nil {
t.Errorf("Test case %d: Expected error but got success\nMetadata: %v\nAuthParams: %v",
i, testData.metadata, testData.authParams)
}
})
}
}

func TestNewRelicGetMetricSpecForScaling(t *testing.T) {
for _, testData := range newrelicMetricIdentifiers {
meta, err := parseNewRelicMetadata(&scalersconfig.ScalerConfig{TriggerMetadata: testData.metadataTestData.metadata, AuthParams: testData.metadataTestData.authParams, TriggerIndex: testData.triggerIndex}, logr.Discard())
meta, err := parseNewRelicMetadata(&scalersconfig.ScalerConfig{
TriggerMetadata: testData.metadataTestData.metadata,
AuthParams: testData.metadataTestData.authParams,
TriggerIndex: testData.triggerIndex,
})
if err != nil {
t.Fatal("Could not parse metadata:", err)
}
mockNewRelicScaler := newrelicScaler{
metadata: meta,
nrClient: nil,
metadata: meta,
nrClient: nil,
logger: logr.Discard(),
metricType: v2.AverageValueMetricType,
}

metricSpec := mockNewRelicScaler.GetMetricSpecForScaling(context.Background())
Expand Down
8 changes: 8 additions & 0 deletions pkg/scalers/scalersconfig/typed_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,14 @@ func setConfigValueHelper(params Params, valFromConfig string, field reflect.Val
if field.Kind() == reflect.Slice {
return setConfigValueSlice(params, valFromConfig, field)
}
if field.Kind() == reflect.Bool {
boolVal, err := strconv.ParseBool(valFromConfig)
if err != nil {
return fmt.Errorf("unable to parse boolean value %q: %w", valFromConfig, err)
}
field.SetBool(boolVal)
return nil
}
if field.CanInterface() {
ifc := reflect.New(field.Type()).Interface()
if err := json.Unmarshal([]byte(valFromConfig), &ifc); err != nil {
Expand Down
Loading