Skip to content

Commit d54c1f9

Browse files
feat(alerts): adds support for signal_seasonality for NRQL baseline conditions (#2844)
feat(alerts): adds support for `signal_seasonality` for NRQL baseline conditions (#2844) Co-authored-by: pranav-new-relic <[email protected]>
1 parent 6cbc205 commit d54c1f9

7 files changed

+186
-3
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/mitchellh/go-homedir v1.1.0
1010
github.com/newrelic/go-agent/v3 v3.30.0
1111
github.com/newrelic/go-insights v1.0.3
12-
github.com/newrelic/newrelic-client-go/v2 v2.57.0
12+
github.com/newrelic/newrelic-client-go/v2 v2.58.0
1313
github.com/stretchr/testify v1.9.0
1414
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
1515
)

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ github.com/newrelic/go-agent/v3 v3.30.0 h1:ZXHCT/Cot4iIPwcegCZURuRQOsfmGA6wilW+S
270270
github.com/newrelic/go-agent/v3 v3.30.0/go.mod h1:9utrgxlSryNqRrTvII2XBL+0lpofXbqXApvVWPpbzUg=
271271
github.com/newrelic/go-insights v1.0.3 h1:zSNp1CEZnXktzSIEsbHJk8v6ZihdPFP2WsO/fzau3OQ=
272272
github.com/newrelic/go-insights v1.0.3/go.mod h1:A20BoT8TNkqPGX2nS/Z2fYmKl3Cqa3iKZd4whzedCY4=
273-
github.com/newrelic/newrelic-client-go/v2 v2.57.0 h1:80DIUhk8IvxITO/ocgZvSa8cbNraOoJiCbU5aKOnUT8=
274-
github.com/newrelic/newrelic-client-go/v2 v2.57.0/go.mod h1:+RRjI3nDGWT3kLm9Oi3QxpBm70uu8q1upEHBVWCZFpo=
273+
github.com/newrelic/newrelic-client-go/v2 v2.58.0 h1:plkfLvFNrS5b38v2CHQG6j/Axh+aDP05caJSvWMyZ+w=
274+
github.com/newrelic/newrelic-client-go/v2 v2.58.0/go.mod h1:+RRjI3nDGWT3kLm9Oi3QxpBm70uu8q1upEHBVWCZFpo=
275275
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
276276
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
277277
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=

newrelic/resource_newrelic_nrql_alert_condition.go

+20
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func resourceNewRelicNrqlAlertCondition() *schema.Resource {
127127
Importer: &schema.ResourceImporter{
128128
StateContext: resourceImportStateWithMetadata(2, "type"),
129129
},
130+
CustomizeDiff: validateNrqlConditionAttributes,
130131
Schema: map[string]*schema.Schema{
131132
"policy_id": {
132133
Type: schema.TypeInt,
@@ -389,6 +390,25 @@ func resourceNewRelicNrqlAlertCondition() *schema.Resource {
389390
return strings.EqualFold(old, new) // Case fold this attribute when diffing
390391
},
391392
},
393+
"signal_seasonality": {
394+
Type: schema.TypeString,
395+
Optional: true,
396+
Description: "Seasonality under which a condition's signal(s) are evaluated. Valid values are: 'NEW_RELIC_CALCULATION', 'HOURLY', 'DAILY', 'WEEKLY', or 'NONE'. To have New Relic calculate seasonality automatically, set to 'NEW_RELIC_CALCULATION' (default). To turn off seasonality completely, set to 'NONE'.",
397+
ValidateFunc: validation.StringInSlice(
398+
[]string{
399+
string(alerts.NrqlSignalSeasonalities.NewRelicCalculation),
400+
string(alerts.NrqlSignalSeasonalities.Hourly),
401+
string(alerts.NrqlSignalSeasonalities.Daily),
402+
string(alerts.NrqlSignalSeasonalities.Weekly),
403+
string(alerts.NrqlSignalSeasonalities.None),
404+
},
405+
true,
406+
),
407+
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
408+
// If a value is not provided and the condition uses the default value, don't show a diff. Also case insensitive.
409+
return (strings.EqualFold(old, string(alerts.NrqlSignalSeasonalities.NewRelicCalculation)) && new == "") || strings.EqualFold(old, new)
410+
},
411+
},
392412
},
393413
Timeouts: &schema.ResourceTimeout{
394414
Create: schema.DefaultTimeout(30 * time.Second),

newrelic/resource_newrelic_nrql_alert_condition_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,41 @@ func TestAccNewRelicNrqlAlertCondition_TitleTemplate(t *testing.T) {
668668
})
669669
}
670670

671+
func TestAccNewRelicNrqlAlertCondition_SignalSeasonality(t *testing.T) {
672+
resourceName := "newrelic_nrql_alert_condition.foo"
673+
rName := acctest.RandString(5)
674+
signalSeasonality := "daily"
675+
signalSeasonalityNRCalc := "new_relic_calculation"
676+
677+
resource.ParallelTest(t, resource.TestCase{
678+
PreCheck: func() { testAccPreCheckEnvVars(t) },
679+
Providers: testAccProviders,
680+
CheckDestroy: testAccCheckNewRelicNrqlAlertConditionDestroy,
681+
Steps: []resource.TestStep{
682+
// Test: Create baseline condition with signal seasonality
683+
{
684+
Config: testAccNewRelicNrqlAlertConditionWithSignalSeasonality(
685+
rName,
686+
signalSeasonality,
687+
),
688+
Check: resource.ComposeTestCheckFunc(
689+
testAccCheckNewRelicNrqlAlertConditionExists(resourceName),
690+
),
691+
},
692+
// Test: New Relic Calculation is valid value for signal seasonality
693+
{
694+
Config: testAccNewRelicNrqlAlertConditionWithSignalSeasonality(
695+
rName,
696+
signalSeasonalityNRCalc,
697+
),
698+
Check: resource.ComposeTestCheckFunc(
699+
testAccCheckNewRelicNrqlAlertConditionExists(resourceName),
700+
),
701+
},
702+
},
703+
})
704+
}
705+
671706
func testAccCheckNewRelicNrqlAlertConditionDestroy(s *terraform.State) error {
672707
providerConfig := testAccProvider.Meta().(*ProviderConfig)
673708
client := providerConfig.NewClient
@@ -1423,3 +1458,39 @@ resource "newrelic_nrql_alert_condition" "foo" {
14231458
}
14241459
`, name, predictBy)
14251460
}
1461+
1462+
func testAccNewRelicNrqlAlertConditionWithSignalSeasonality(
1463+
name string,
1464+
signalSeasonality string,
1465+
) string {
1466+
return fmt.Sprintf(`
1467+
resource "newrelic_alert_policy" "foo" {
1468+
name = "tf-test-%[1]s"
1469+
}
1470+
1471+
resource "newrelic_nrql_alert_condition" "foo" {
1472+
policy_id = newrelic_alert_policy.foo.id
1473+
1474+
name = "tf-test-%[1]s"
1475+
type = "baseline"
1476+
enabled = false
1477+
signal_seasonality = "%[2]s"
1478+
violation_time_limit_seconds = 3600
1479+
baseline_direction = "lower_only"
1480+
aggregation_delay = 120
1481+
aggregation_method = "event_flow"
1482+
1483+
nrql {
1484+
query = "SELECT uniqueCount(hostname) FROM ComputeSample"
1485+
}
1486+
1487+
critical {
1488+
operator = "above"
1489+
threshold = 1.0
1490+
threshold_duration = 120
1491+
threshold_occurrences = "ALL"
1492+
}
1493+
}
1494+
1495+
`, name, signalSeasonality)
1496+
}

newrelic/structures_newrelic_nrql_alert_condition.go

+67
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package newrelic
22

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"strconv"
68
"strings"
@@ -72,6 +74,17 @@ func expandNrqlAlertConditionCreateInput(d *schema.ResourceData) (*alerts.NrqlCo
7274
}
7375
}
7476

77+
if conditionType == "baseline" {
78+
if attr, ok := d.GetOk("signal_seasonality"); ok {
79+
seasonality := alerts.NrqlSignalSeasonality(strings.ToUpper(attr.(string)))
80+
input.SignalSeasonality = &seasonality
81+
} else {
82+
// Null is equivalent to the default value of `NEW_RELIC_CALCULATION` in the API
83+
seasonality := alerts.NrqlSignalSeasonalities.NewRelicCalculation
84+
input.SignalSeasonality = &seasonality
85+
}
86+
}
87+
7588
if runbookURL, ok := d.GetOk("runbook_url"); ok {
7689
input.RunbookURL = runbookURL.(string)
7790
}
@@ -132,6 +145,18 @@ func expandNrqlAlertConditionUpdateInput(d *schema.ResourceData) (*alerts.NrqlCo
132145
}
133146
}
134147

148+
if conditionType == "baseline" {
149+
if attr, ok := d.GetOk("signal_seasonality"); ok {
150+
seasonality := alerts.NrqlSignalSeasonality(strings.ToUpper(attr.(string)))
151+
input.SignalSeasonality = &seasonality
152+
} else {
153+
// Null is equivalent to the default value of `NEW_RELIC_CALCULATION` in the API
154+
// so this effectively allows the user to null out the signal seasonality on update
155+
seasonality := alerts.NrqlSignalSeasonalities.NewRelicCalculation
156+
input.SignalSeasonality = &seasonality
157+
}
158+
}
159+
135160
if runbookURL, ok := d.GetOk("runbook_url"); ok {
136161
input.RunbookURL = runbookURL.(string)
137162
}
@@ -569,6 +594,10 @@ func flattenNrqlAlertCondition(accountID int, condition *alerts.NrqlAlertConditi
569594

570595
if conditionType == "baseline" {
571596
_ = d.Set("baseline_direction", string(*condition.BaselineDirection))
597+
598+
if condition.SignalSeasonality != nil {
599+
_ = d.Set("signal_seasonality", string(*condition.SignalSeasonality))
600+
}
572601
}
573602

574603
configuredNrql := d.Get("nrql.0").(map[string]interface{})
@@ -850,3 +879,41 @@ func getConfiguredTerms(configTerms []interface{}) []map[string]interface{} {
850879

851880
return setTerms
852881
}
882+
883+
func validateNrqlConditionAttributes(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error {
884+
var errorsList []error
885+
886+
_, conditionType := d.GetChange("type")
887+
if conditionType != nil {
888+
isNotBaselineCondition := !strings.Contains(conditionType.(string), "baseline")
889+
if isNotBaselineCondition {
890+
err := validateSignalSeasonality(d)
891+
if err != nil {
892+
errorsList = append(errorsList, err)
893+
}
894+
}
895+
}
896+
897+
if len(errorsList) == 0 {
898+
return nil
899+
}
900+
901+
errorsString := "the following validation errors have been identified with the configuration of the nrql alert condition: \n"
902+
903+
for index, val := range errorsList {
904+
errorsString += fmt.Sprintf("(%d): %s\n", index+1, val)
905+
}
906+
907+
return errors.New(errorsString)
908+
}
909+
910+
func validateSignalSeasonality(d *schema.ResourceDiff) error {
911+
rawConfiguration := d.GetRawConfig()
912+
913+
signalSeasonalityIsNotNil := !rawConfiguration.GetAttr("signal_seasonality").IsNull()
914+
915+
if signalSeasonalityIsNotNil {
916+
return fmt.Errorf(`'signal_seasonality' is only valid on baseline conditions. Please remove this field or change the condition type`)
917+
}
918+
return nil
919+
}

newrelic/structures_newrelic_nrql_alert_condition_test.go

+23
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func TestExpandNrqlAlertConditionInput(t *testing.T) {
5151

5252
titleTemplate := "Title {{template}}"
5353

54+
signalSeasonality := alerts.NrqlSignalSeasonalities.Daily
55+
5456
cases := map[string]struct {
5557
Data map[string]interface{}
5658
ExpectErr bool
@@ -398,6 +400,25 @@ func TestExpandNrqlAlertConditionInput(t *testing.T) {
398400
},
399401
},
400402
},
403+
"signal seasonality not nil": {
404+
Data: map[string]interface{}{
405+
"nrql": []interface{}{nrql},
406+
"signal_seasonality": "daily",
407+
},
408+
Expanded: &alerts.NrqlConditionCreateInput{
409+
NrqlConditionCreateBase: alerts.NrqlConditionCreateBase{},
410+
SignalSeasonality: &signalSeasonality,
411+
},
412+
},
413+
"signal seasonality nil": {
414+
Data: map[string]interface{}{
415+
"nrql": []interface{}{nrql},
416+
"signal_seasonality": nil,
417+
},
418+
Expanded: &alerts.NrqlConditionCreateInput{
419+
NrqlConditionCreateBase: alerts.NrqlConditionCreateBase{},
420+
},
421+
},
401422
}
402423

403424
r := resourceNewRelicNrqlAlertCondition()
@@ -606,6 +627,7 @@ func TestFlattenNrqlAlertCondition(t *testing.T) {
606627
nrqlConditionBaseline.Type = alerts.NrqlConditionTypes.Baseline
607628
nrqlConditionBaseline.BaselineDirection = &alerts.NrqlBaselineDirections.LowerOnly
608629
nrqlConditionBaseline.EntityGUID = common.EntityGUID("NDAwMzA0fEFPTkRJVElPTnwxNDMzNjc3")
630+
nrqlConditionBaseline.SignalSeasonality = &alerts.NrqlSignalSeasonalities.Daily
609631

610632
// Static
611633
nrqlConditionStatic := nrqlCondition
@@ -673,6 +695,7 @@ func TestFlattenNrqlAlertCondition(t *testing.T) {
673695
switch condition.Type {
674696
case alerts.NrqlConditionTypes.Baseline:
675697
require.Equal(t, string(alerts.NrqlBaselineDirections.LowerOnly), d.Get("baseline_direction").(string))
698+
require.Equal(t, string(alerts.NrqlSignalSeasonalities.Daily), d.Get("signal_seasonality").(string))
676699
require.Equal(t, 120, d.Get("expiration_duration").(int))
677700
require.True(t, d.Get("open_violation_on_expiration").(bool))
678701
require.True(t, d.Get("close_violations_on_expiration").(bool))

website/docs/r/nrql_alert_condition.html.markdown

+2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ The following arguments are supported:
9999
- `aggregation_timer` - (Optional) How long we wait after each data point arrives to make sure we've processed the whole batch. Use `aggregation_timer` with the `event_timer` method. The timer value can range from 0 seconds to 1200 seconds (20 minutes); the default is 60 seconds. `aggregation_timer` cannot be set with `nrql.evaluation_offset`.
100100
- `evaluation_delay` - (Optional) How long we wait until the signal starts evaluating. The maximum delay is 7200 seconds (120 minutes).
101101
- `slide_by` - (Optional) Gathers data in overlapping time windows to smooth the chart line, making it easier to spot trends. The `slide_by` value is specified in seconds and must be smaller than and a factor of the `aggregation_window`.
102+
- `signal_seasonality` - (Optional) Seasonality under which a condition's signal(s) are evaluated. Only available for baseline conditions. Valid values are: `NEW_RELIC_CALCULATION`, `HOURLY`, `DAILY`, `WEEKLY`, or `NONE`. To have New Relic calculate seasonality automatically, set to `NEW_RELIC_CALCULATION`. To turn off seasonality completely, set to `NONE`.
102103

103104
## NRQL
104105

@@ -180,6 +181,7 @@ resource "newrelic_nrql_alert_condition" "foo" {
180181
181182
# baseline type only
182183
baseline_direction = "upper_only"
184+
signal_seasonality = "weekly"
183185
184186
nrql {
185187
query = "SELECT percentile(duration, 95) FROM Transaction WHERE appName = 'ExampleAppName'"

0 commit comments

Comments
 (0)