From 526a97b5284944498b1e67f48b8770b608be2771 Mon Sep 17 00:00:00 2001 From: obs-gh-catherman <145469146+obs-gh-catherman@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:42:29 -0500 Subject: [PATCH] feature: update monitorv2 to allow inline actions (#174) --- client/api.go | 17 + .../internal/meta/operation/monitorv2.graphql | 90 +++ client/internal/meta/schema/monitorv2.graphql | 222 ++++++- .../meta/schema/monitorv2_extend.graphql | 70 +- client/meta/genqlient.generated.go | 622 +++++++++++++++++- client/meta/monitorv2.go | 11 + docs/data-sources/monitor_v2.md | 5 +- docs/resources/monitor_v2.md | 60 +- observe/data_source_monitor_v2.go | 9 +- observe/descriptions/monitorv2.yaml | 6 +- observe/resource_monitor_v2.go | 192 ++++-- observe/resource_monitor_v2_action.go | 26 +- observe/resource_monitor_v2_test.go | 187 ++++++ 13 files changed, 1395 insertions(+), 122 deletions(-) diff --git a/client/api.go b/client/api.go index 63e6b8fc..2c753c41 100644 --- a/client/api.go +++ b/client/api.go @@ -500,6 +500,23 @@ func (c *Client) GetMonitor(ctx context.Context, id string) (*meta.Monitor, erro return c.Meta.GetMonitor(ctx, id) } +func (c *Client) SaveMonitorV2WithActions( + ctx context.Context, + workspaceId string, + monitorId *string, + input *meta.MonitorV2Input, + actions []meta.MonitorV2ActionAndRelationInput, +) (*meta.MonitorV2, error) { + if !c.Flags[flagObs2110] { + c.obs2110.Lock() + defer c.obs2110.Unlock() + } + if c.Config.ManagingObjectID != nil { + input.ManagedById = c.Config.ManagingObjectID + } + return c.Meta.SaveMonitorV2WithActions(ctx, workspaceId, monitorId, input, actions) +} + func (c *Client) CreateMonitorV2(ctx context.Context, workspaceId string, input *meta.MonitorV2Input) (*meta.MonitorV2, error) { if !c.Flags[flagObs2110] { c.obs2110.Lock() diff --git a/client/internal/meta/operation/monitorv2.graphql b/client/internal/meta/operation/monitorv2.graphql index a90a78e1..f5e0276f 100644 --- a/client/internal/meta/operation/monitorv2.graphql +++ b/client/internal/meta/operation/monitorv2.graphql @@ -113,6 +113,7 @@ fragment MonitorV2Definition on MonitorV2Definition{ } lookbackTime dataStabilizationDelay + maxAlertsPerHour # @genqlient(flatten: true) groupings { ...MonitorV2Column @@ -155,6 +156,23 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { levels sendEndNotifications sendRemindersInterval + # @genqlient(flatten: true) + definition { + ...MonitorV2ActionDefinition + } +} + +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + # @genqlient(flatten: true) + email { + ...MonitorV2EmailAction + } + # @genqlient(flatten: true) + webhook { + ...MonitorV2WebhookAction + } } # @genqlient(for: "MonitorV2Input.iconUrl", omitempty: true) @@ -162,6 +180,7 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { # @genqlient(for: "MonitorV2Input.managedById", omitempty: true) # @genqlient(for: "MonitorV2Input.folderId", omitempty: true) # @genqlient(for: "MonitorV2DefinitionInput.dataStabilizationDelay", omitempty: true) +# @genqlient(for: "MonitorV2DefinitionInput.maxAlertsPerHour", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.count", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.threshold", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.promote", omitempty: true) @@ -180,6 +199,7 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { # @genqlient(for: "PrimitiveValueInput.string", omitempty: true) # @genqlient(for: "PrimitiveValueInput.timestamp", omitempty: true) # @genqlient(for: "PrimitiveValueInput.duration", omitempty: true) +# @genclient(for: "MonitorV2ComparisonExpressionInput.conditions", omitempty: true) fragment MonitorV2 on MonitorV2 { id workspaceId @@ -209,6 +229,7 @@ fragment MonitorV2 on MonitorV2 { # @genqlient(for: "MonitorV2Input.managedById", omitempty: true) # @genqlient(for: "MonitorV2Input.folderId", omitempty: true) # @genqlient(for: "MonitorV2DefinitionInput.dataStabilizationDelay", omitempty: true) +# @genqlient(for: "MonitorV2DefinitionInput.maxAlertsPerHour", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.count", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.threshold", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.promote", omitempty: true) @@ -227,6 +248,7 @@ fragment MonitorV2 on MonitorV2 { # @genqlient(for: "PrimitiveValueInput.string", omitempty: true) # @genqlient(for: "PrimitiveValueInput.timestamp", omitempty: true) # @genqlient(for: "PrimitiveValueInput.duration", omitempty: true) +# @genclient(for: "MonitorV2ComparisonExpressionInput.conditions", omitempty: true) mutation createMonitorV2( $workspaceId: ObjectId!, $input: MonitorV2Input! @@ -242,6 +264,7 @@ mutation createMonitorV2( # @genqlient(for: "MonitorV2Input.managedById", omitempty: true) # @genqlient(for: "MonitorV2Input.folderId", omitempty: true) # @genqlient(for: "MonitorV2DefinitionInput.dataStabilizationDelay", omitempty: true) +# @genqlient(for: "MonitorV2DefinitionInput.maxAlertsPerHour", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.count", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.threshold", omitempty: true) # @genqlient(for: "MonitorV2RuleInput.promote", omitempty: true) @@ -260,6 +283,7 @@ mutation createMonitorV2( # @genqlient(for: "PrimitiveValueInput.string", omitempty: true) # @genqlient(for: "PrimitiveValueInput.timestamp", omitempty: true) # @genqlient(for: "PrimitiveValueInput.duration", omitempty: true) +# @genclient(for: "MonitorV2ComparisonExpressionInput.conditions", omitempty: true) mutation updateMonitorV2( $id: ObjectId!, $input: MonitorV2Input! @@ -291,6 +315,8 @@ query lookupMonitorV2($workspaceId: ObjectId, $folderId: ObjectId, $nameExact: S } } +# @genqlient(for: "MonitorV2ActionRuleInput.levels", omitempty: true) +# @genqlient(for: "MonitorV2ActionRuleInput.conditions", omitempty: true) # @genqlient(for: "MonitorV2ActionRuleInput.sendEndNotifications", omitempty: true) # @genqlient(for: "MonitorV2ActionRuleInput.sendRemindersInterval", omitempty: true) mutation saveMonitorV2Relations( @@ -302,3 +328,67 @@ mutation saveMonitorV2Relations( ...MonitorV2 } } + +# @genqlient(for: "MonitorV2Input.iconUrl", omitempty: true) +# @genqlient(for: "MonitorV2Input.description", omitempty: true) +# @genqlient(for: "MonitorV2Input.managedById", omitempty: true) +# @genqlient(for: "MonitorV2Input.folderId", omitempty: true) +# @genqlient(for: "MonitorV2DefinitionInput.groupings", omitempty: true) +# @genqlient(for: "MonitorV2DefinitionInput.dataStabilizationDelay", omitempty: true) +# @genqlient(for: "MonitorV2DefinitionInput.maxAlertsPerHour", omitempty: true) +# @genqlient(for: "MonitorV2RuleInput.count", omitempty: true) +# @genqlient(for: "MonitorV2RuleInput.threshold", omitempty: true) +# @genqlient(for: "MonitorV2RuleInput.promote", omitempty: true) +# @genqlient(for: "MonitorV2ColumnInput.linkColumn", omitempty: true) +# @genqlient(for: "MonitorV2ColumnInput.columnPath", omitempty: true) +# @genqlient(for: "MonitorV2LinkColumnInput.meta", omitempty: true) +# @genqlient(for: "MonitorV2ColumnPathInput.path", omitempty: true) +# @genqlient(for: "InputDefinitionInput.stageID", omitempty: true) +# @genqlient(for: "InputDefinitionInput.stageId", omitempty: true) +# @genqlient(for: "StageQueryInput.stageID", omitempty: true) +# @genqlient(for: "StageQueryInput.stageId", omitempty: true) +# @genqlient(for: "StageQueryInput.id", omitempty: true) +# @genqlient(for: "PrimitiveValueInput.bool", omitempty: true) +# @genqlient(for: "PrimitiveValueInput.float64", omitempty: true) +# @genqlient(for: "PrimitiveValueInput.int64", omitempty: true) +# @genqlient(for: "PrimitiveValueInput.string", omitempty: true) +# @genqlient(for: "PrimitiveValueInput.timestamp", omitempty: true) +# @genqlient(for: "PrimitiveValueInput.duration", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.inline", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.email", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.webhook", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.iconUrl", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.description", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.managedById", omitempty: true) +# @genqlient(for: "MonitorV2ActionInput.folderId", omitempty: true) +# @genqlient(for: "MonitorV2EmailActionInput.fragments", omitempty: true) +# @genqlient(for: "MonitorV2WebhookActionInput.headers", omitempty: true) +# @genqlient(for: "MonitorV2WebhookActionInput.fragments", omitempty: true) +# @genqlient(for: "MonitorV2ActionAndRelationInput.action", omitempty: true) +# @genqlient(for: "MonitorV2ActionAndRelationInput.actionID", omitempty: true) +# @genqlient(for: "MonitorV2ActionAndRelationInput.levels", omitempty: true) +# @genqlient(for: "MonitorV2ActionAndRelationInput.conditions", omitempty: true) +# @genqlient(for: "MonitorV2ActionAndRelationInput.sendEndNotifications", omitempty: true) +# @genqlient(for: "MonitorV2ActionAndRelationInput.sendRemindersInterval", omitempty: true) +# @genqlient(for: "MonitorV2ComparisonExpressionInput.compareTerms", omitempty: true) +# @genqlient(for: "MonitorV2ComparisonExpressionInput.subExpressions", omitempty: true) +# @genqlient(for: "MonitorV2CountRuleInput.compareGroups", omitempty: true) +# @genqlient(for: "MonitorV2ThresholdRuleInput.compareGroups", omitempty: true) +# @genqlient(for: "MonitorV2PromoteRuleInput.compareColumns", omitempty: true) +mutation saveMonitorV2WithActions( + $workspaceId: ObjectId!, + $monitorId: ObjectId, + $input: MonitorV2Input!, + $actions: [MonitorV2ActionAndRelationInput!], +) { + # @genqlient(flatten: true) + monitorV2: saveMonitorV2WithActions( + workspaceId: $workspaceId, + monitorId: $monitorId, + input: $input, + actions: $actions, + ) { + ...MonitorV2 + } +} + diff --git a/client/internal/meta/schema/monitorv2.graphql b/client/internal/meta/schema/monitorv2.graphql index ce12e956..3742f5d5 100644 --- a/client/internal/meta/schema/monitorv2.graphql +++ b/client/internal/meta/schema/monitorv2.graphql @@ -150,6 +150,7 @@ enum MonitorV2ValueAggregation @goModel(model: "observe/meta/metatypes.MonitorV2 type MonitorV2 implements WorkspaceObject & AuditedObject & FolderObject @goModel(model: "observe/meta/metatypes.MonitorV2") { # payload + # not in output: sharingRules: [MonitorSharingRule!] """ Indicates if the monitor is enabled or disabled. Use setMonitorV2Enabled to flip this flag. """ @@ -160,7 +161,7 @@ type MonitorV2 implements WorkspaceObject & AuditedObject & FolderObject @goMode """ comment: String @deprecated(reason:"Use the description field over the comment field") meta: MonitorV2Meta @goField(forceResolver: true) - rollupStatus: MonitorV2RollupStatus! @goField(forceResolver: true) + rollupStatus: MonitorV2RollupStatus! @deprecated(reason:"rollup status has been broken up and states are inferred in the UI") definition: MonitorV2Definition! """ Describes the type of each of the rules in the definition (they must all be the same type). @@ -195,6 +196,12 @@ type MonitorV2 implements WorkspaceObject & AuditedObject & FolderObject @goMode Mutes is a list of mute rules currently linked to this monitor. """ mutes: [MonitorV2MuteRule!]! @goField(forceResolver: true) + """ + InvestigationInfo describes structured information the monitor creator + wishes to advertise for investigators of alerts. This includes things like + runbook references. + """ + investigationInfo: MonitorV2InvestigationInfo @deprecated(reason:"This is a POC field and not for production use.") # WorkspaceObject id: ObjectId! workspaceId: ObjectId! @@ -217,7 +224,6 @@ type MonitorV2 implements WorkspaceObject & AuditedObject & FolderObject @goMode } input MonitorV2Input @goModel(model: "observe/meta/metatypes.MonitorV2Input") { - # payload # not in input: disabled: Boolean! comment: String # resolver: meta: MonitorV2MetaInput @@ -230,6 +236,7 @@ input MonitorV2Input @goModel(model: "observe/meta/metatypes.MonitorV2Input") { # resolver: actionRules: [MonitorV2ActionRuleInput!]! # not in input: monitorVersion: Int64! # resolver: mutes: [MonitorV2MuteRuleInput!]! + investigationInfo: MonitorV2InvestigationInfoInput # WorkspaceObject name: String! iconUrl: String @@ -299,6 +306,10 @@ type MonitorV2Meta @goModel(model: "observe/meta/metatypes.MonitorV2Meta") { dataset and all upstream dependencies using dataset APIs for transform-based monitors. """ outputDatasetID: ObjectId + """ + Describes the alert's schema installed by the transform-based monitor. + """ + alertSchema: MonitorV2AlertSchema # not in output: outputDatasetStrategyVersion: String """ The expected next time this monitor will run if it's a schedule-based monitor. @@ -318,12 +329,43 @@ input MonitorV2MetaInput @goModel(model: "observe/meta/metatypes.MonitorV2MetaIn lastWarningTime: Time lastAlarmTime: Time outputDatasetID: ObjectId + alertSchema: MonitorV2AlertSchemaInput outputDatasetStrategyVersion: String nextScheduledTime: Time lastScheduleBookmark: Time } +type MonitorV2InvestigationInfo @goModel(model: "observe/meta/metatypes.MonitorV2InvestigationInfo") { + # payload + """ + This is free-form text used in investigation POC work and likely will be + removed in preference to reference links. DO NOT USE THIS FIELD FOR PRODUCTION. + """ + runbookContent: String! +} + +input MonitorV2InvestigationInfoInput @goModel(model: "observe/meta/metatypes.MonitorV2InvestigationInfoInput") { + # payload + runbookContent: String! +} + + +# MonitorV2AlertSchema contains the Monitor V2 Column information of the captured values within the MonitorV2Alarm. +type MonitorV2AlertSchema @goModel(model: "observe/meta/metatypes.MonitorV2AlertSchema") { + # payload + """ + All the columns that are inside the captured values of the alert. + """ + columns: [MonitorV2Column!] +} + +input MonitorV2AlertSchemaInput @goModel(model: "observe/meta/metatypes.MonitorV2AlertSchemaInput") { + # payload + columns: [MonitorV2ColumnInput!] +} + + # MonitorV2Definition describes the configuration logic that defines what a monitor to evaluate to detect # what the user wants. type MonitorV2Definition @goModel(model: "observe/meta/metatypes.MonitorV2Definition") { @@ -353,6 +395,12 @@ type MonitorV2Definition @goModel(model: "observe/meta/metatypes.MonitorV2Defini """ dataStabilizationDelay: Duration """ + MaxAlertsPerHour sets the rate allowed before a monitor is considered possibly bad + and automatically disabled by the system. This has a default value of 100 if null/unset. + A value of 0 means "no limit". + """ + maxAlertsPerHour: Int64 + """ Groupings describes the groups that logically separate events/rows/etc from each other. When the input monitor dataset is of type resource and the monitor strategy is of type promote, this field should either be left empty to be mutated with the primary keys of the resource dataset or it should only contain the @@ -373,6 +421,7 @@ input MonitorV2DefinitionInput @goModel(model: "observe/meta/metatypes.MonitorV2 rules: [MonitorV2RuleInput!]! lookbackTime: Duration dataStabilizationDelay: Duration + maxAlertsPerHour: Int64 groupings: [MonitorV2ColumnInput!] scheduling: MonitorV2SchedulingInput } @@ -421,7 +470,7 @@ type MonitorV2Scheduling @goModel(model: "observe/meta/metatypes.MonitorV2Schedu """ Interval should be used to run explicit ad-hoc queries. """ - interval: MonitorV2IntervalSchedule + interval: MonitorV2IntervalSchedule @deprecated(reason:"ad-hoc scheduling is no longer supported") """ Transform should be used to defer scheduling to the transformer and evaluate when data becomes available. @@ -468,6 +517,10 @@ input MonitorV2CapturedValueInput @goModel(model: "observe/meta/metatypes.Monito type MonitorV2Column @goModel(model: "observe/meta/metatypes.MonitorV2Column") { # payload """ + Type of the column such as string, int64, and more. + """ + columnType: FieldType + """ Link Column is for link typed column which the user wants to group by. """ linkColumn: MonitorV2LinkColumn @@ -475,12 +528,17 @@ type MonitorV2Column @goModel(model: "observe/meta/metatypes.MonitorV2Column") { Column path is any non-link typed column along with an optional path which the user wants to group by. """ columnPath: MonitorV2ColumnPath + # not in output: xIsInGroupings: Boolean! + # not in output: xIsAggregation: Boolean! } input MonitorV2ColumnInput @goModel(model: "observe/meta/metatypes.MonitorV2ColumnInput") { # payload + # not in input: columnType: FieldType linkColumn: MonitorV2LinkColumnInput columnPath: MonitorV2ColumnPathInput + # not in input: xIsInGroupings: Boolean! + # not in input: xIsAggregation: Boolean! } @@ -549,8 +607,6 @@ type MonitorV2ColumnPath @goModel(model: "observe/meta/metatypes.MonitorV2Column name: String! path: String # not in output: xRef: String! - # not in output: xIsLinkSourceField: Boolean! - # not in output: xIsNotInGroupings: Boolean! } input MonitorV2ColumnPathInput @goModel(model: "observe/meta/metatypes.MonitorV2ColumnPathInput") { @@ -558,8 +614,6 @@ input MonitorV2ColumnPathInput @goModel(model: "observe/meta/metatypes.MonitorV2 name: String! path: String # not in input: xRef: String! - # not in input: xIsLinkSourceField: Boolean! - # not in input: xIsNotInGroupings: Boolean! } @@ -698,12 +752,83 @@ input MonitorV2ColumnComparisonInput @goModel(model: "observe/meta/metatypes.Mon } +type MonitorV2AlarmActionStats @goModel(model: "observe/meta/metatypes.MonitorV2AlarmActionStats") { + # payload + """ + MonitorID is the monitor object id + """ + monitorID: ObjectId! + """ + AlarmID is the alarm/alert identifier + """ + alarmID: String! + """ + NumNotifsDiscarded tells the total events discarded that might otherwise have + generated a notification. This could be because there is no matching rule or + there is a matching mute or similar. It does not count events ignored + that are not subscribed to (like end notifications when end notifications are + not enabled). + """ + numNotifsDiscarded: Int64! + """ + NumNotifsMuted counts the number of events that might have generated a notification + but didn't because they matched a mute rule. + """ + numNotifsMuted: Int64! + """ + LastMuteTime is updated every time NumNotifsMuted is incremeneted. + """ + lastMuteTime: Time + """ + NumNotifsSent counts the number of notifications that were dispatched + to their destination. + """ + numNotifsSent: Int64! + """ + LastSendtime is updated every time NumNotifsSent is incremeneted + """ + lastSendTime: Time + """ + NumErrors is incremented whenever there is a failure generating or evaluating + a candidate notification. This might be due to template rendering errors + or objects having been deleted. + """ + numErrors: Int64! + """ + LastErrorTime is updated every time NumErrors is incremented. + """ + lastErrorTime: Time + """ + This unique identifier is assigned by the detection pipeline to the + detection event (when it originates in the detection pipeline) and can be + connected back to that evaluation, the stats of that evaluation, and the + events in the same evaluation. + note: In rare cases, events are not generated from detections so the + evaluationID will not correlate to stats, etc. + """ + evaluationID: String! +} + +input MonitorV2AlarmActionStatsInput @goModel(model: "observe/meta/metatypes.MonitorV2AlarmActionStatsInput") { + # payload + monitorID: ObjectId! + alarmID: String! + numNotifsDiscarded: Int64! + numNotifsMuted: Int64! + lastMuteTime: Time + numNotifsSent: Int64! + lastSendTime: Time + numErrors: Int64! + lastErrorTime: Time + evaluationID: String! +} + + type MonitorV2Stats @goModel(model: "observe/meta/metatypes.MonitorV2Stats") { # payload monitorID: ObjectId! """ OutputDatasetId is the monitor's output dataset id, which is the dataset that feeds monitor evaluation - if the monitor is a real-time monitor. This field is not used when the monitor is scheduled. """ outputDatasetID: ObjectId """ @@ -756,19 +881,38 @@ type MonitorV2Stats @goModel(model: "observe/meta/metatypes.MonitorV2Stats") { that are considered "ready" because they satisify the StabilityBookmarkTime. These may or may not generate state change events. """ - numReadyAlarmStates: Int64! + numReadyAlarmStates: Int64! @deprecated(reason:"No longer used") """ NumFutureAlarmStates is the number of MonitorAlarmState's generated by this evaluation that are considered predictions because they do not satisfy the StabilityBookmarkTime. These results may get used to generate events at some evaluation point after the bookmark time. """ - numFutureAlarmStates: Int64! + numFutureAlarmStates: Int64! @deprecated(reason:"No longer used") + """ + NumPackedAlarmStates is the number of MonitorAlarmState's generated by this evaluation + that are already packed. Since evaluation only works on data that is considered + "ready" by the bookmark, the number of dataset rows mostly conveys the unpacked + result count. + """ + numPackedAlarmStates: Int64! """ NumEvaluatedGroupings is the distinct number of groupings that generated results (ready or future). This is a count of logically different entities are generating results. """ numEvaluatedGroupings: Int64! + """ + NumEventsGenerated conveys how many of the alarm states were converted into state + change events in this evaluation for emission into the datastream and actions pipeline. + """ + numEventsGenerated: Int64! + """ + A unique identifier assigned to the evaluation that produced these stats. + This value can be connected to down stream activities, like actions, to + know what evaluation and statistics (like freshness time, etc) were that + lead to the actions. + """ + evaluationID: String! } input MonitorV2StatsInput @goModel(model: "observe/meta/metatypes.MonitorV2StatsInput") { @@ -785,7 +929,10 @@ input MonitorV2StatsInput @goModel(model: "observe/meta/metatypes.MonitorV2Stats numDatasetRows: Int64! numReadyAlarmStates: Int64! numFutureAlarmStates: Int64! + numPackedAlarmStates: Int64! numEvaluatedGroupings: Int64! + numEventsGenerated: Int64! + evaluationID: String! } @@ -836,13 +983,21 @@ type MonitorV2Alarm @goModel(model: "observe/meta/metatypes.MonitorV2Alarm") { """ groupingHash: Int64! """ - monitorVersion on the alarm object shows the version of the monitor at the time this alarm was created. + Version of the monitor at the time this alarm was created. """ monitorVersion: Int64! """ The monitor that generated this alarm. """ monitor: MonitorV2ForAlarm @goField(forceResolver: true) + """ + IsInvalid will be set when an alarm has previously been triggered but new data + has caused the entire alarm to be considered a false positive. The alarm remains for + historical purposes, including the start and end times, but the alarm itself is no + longer valid. + """ + isInvalid: Boolean! + stats: MonitorV2AlarmActionStats @goField(forceResolver: true) } input MonitorV2AlarmInput @goModel(model: "observe/meta/metatypes.MonitorV2AlarmInput") { @@ -856,6 +1011,8 @@ input MonitorV2AlarmInput @goModel(model: "observe/meta/metatypes.MonitorV2Alarm # not in input: groupingHash: Int64! # not in input: monitorVersion: Int64! # resolver: monitor: MonitorV2ForAlarmInput + # not in input: isInvalid: Boolean! + # resolver: stats: MonitorV2AlarmActionStatsInput } @@ -863,12 +1020,17 @@ type MonitorV2Preview @goModel(model: "observe/meta/metatypes.MonitorV2Preview") # payload alarms: [MonitorV2Alarm!]! """ - stabilityBookmarkTime displays the time until which the alarms are stable. Any alarm with - an end time that crosses the bookmark time would mean that the end time may change from later - arriving data. This is an internal backend heuristic to do the work of "anything after - this time is potentially unstable and need to re-validate". + Displays the time until which the alarms are stable. Any alarm with an end time that + crosses the bookmark time would mean that the end time may change from later arriving + data. This is an internal backend heuristic to do the work of "anything after this + time is potentially unstable and need to re-validate". """ stabilityBookmarkTime: Time! + """ + Retrieve the preview of the alert schema for the frontend. This way the frontend can utilize + it for creating mutes or value-based routings before saving the monitor. + """ + alertSchema: MonitorV2AlertSchema! } @@ -878,7 +1040,7 @@ type MonitorV2Comparison @goModel(model: "observe/meta/metatypes.MonitorV2Compar # payload compareFn: MonitorV2ComparisonFunction! """ - compareValue is the right-side value for comparisons that use it (like x > 10, this is 10). + The right-side value for comparisons that use it (like x > 10, this is 10). """ compareValue: PrimitiveValue! } @@ -890,6 +1052,10 @@ input MonitorV2ComparisonInput @goModel(model: "observe/meta/metatypes.MonitorV2 } +# An action rule combines an action with a single monitor or all monitors. +# The rule has the ability to specify optional conditions that must be met. These +# can include a set of severity levels or other conditions against captured values +# in the alert. type MonitorV2ActionRule @goModel(model: "observe/meta/metatypes.MonitorV2ActionRule") { # payload # not in output: monitorID: ObjectId! @@ -898,10 +1064,23 @@ type MonitorV2ActionRule @goModel(model: "observe/meta/metatypes.MonitorV2Action """ actionID: ObjectId! """ - Dispatch this action when the alarm matches any of the provided levels. + Dispatch this action when the alarm matches any of the provided levels + AND'd with any of the optional conditions. """ levels: [MonitorV2AlarmLevel!] + """ + Conditions are additional comparisons to apply (AND'd with levels) to decide if an alert applies. + """ + conditions: MonitorV2ComparisonExpression + """ + Send notifications when the condition ends. + note: At this time, this only happens on the AlarmEnded event. + """ sendEndNotifications: Boolean + """ + Send a reminder notification for as long as the condition is active + on this interval. + """ sendRemindersInterval: Duration """ Included to be shown as part of the MonitorV2 output. @@ -914,6 +1093,7 @@ input MonitorV2ActionRuleInput @goModel(model: "observe/meta/metatypes.MonitorV2 # not in input: monitorID: ObjectId! actionID: ObjectId! levels: [MonitorV2AlarmLevel!] + conditions: MonitorV2ComparisonExpressionInput sendEndNotifications: Boolean sendRemindersInterval: Duration # not in input: definition: MonitorV2ActionDefinitionInput! @@ -1218,15 +1398,15 @@ input MonitorV2MuteLinkInput @goModel(model: "observe/meta/metatypes.MonitorV2Mu type MonitorV2ComparisonExpression @goModel(model: "observe/meta/metatypes.MonitorV2ComparisonExpression") { # payload - compareTerms: [MonitorV2ComparisonTerm!]! - subExpressions: [MonitorV2ComparisonExpression!]! + compareTerms: [MonitorV2ComparisonTerm!] + subExpressions: [MonitorV2ComparisonExpression!] operator: MonitorV2BooleanOperator! } input MonitorV2ComparisonExpressionInput @goModel(model: "observe/meta/metatypes.MonitorV2ComparisonExpressionInput") { # payload - compareTerms: [MonitorV2ComparisonTermInput!]! - subExpressions: [MonitorV2ComparisonExpressionInput!]! + compareTerms: [MonitorV2ComparisonTermInput!] + subExpressions: [MonitorV2ComparisonExpressionInput!] operator: MonitorV2BooleanOperator! } diff --git a/client/internal/meta/schema/monitorv2_extend.graphql b/client/internal/meta/schema/monitorv2_extend.graphql index e7751a38..2c821cdf 100644 --- a/client/internal/meta/schema/monitorv2_extend.graphql +++ b/client/internal/meta/schema/monitorv2_extend.graphql @@ -1,13 +1,13 @@ extend type Query { """ - previewMonitorV2 accepts the same input as create or update, but for the purpose of showing to the user + Accepts the same input as create or update, but for the purpose of showing to the user how the candidate monitor definition will behave against the input data. The return is a preview type that shows how the monitoring strategy will emit results. """ previewMonitorV2(workspaceId: ObjectId!, input: MonitorV2Input!, params: QueryParams!): MonitorV2Preview! """ - monitorV2ByVersion allows fetching of the current or previous MonitorV2 by id and version + Allows fetching of the current or previous MonitorV2 by id and version (where version is the same as the monitorVersion of the MonitorV2). The purpose here is to obtain the definition for historical DetectionEvent's emitted. This can be used to understand what the upstream data looked like at the time of the detection event (noting of course that the actual data may have changed @@ -16,10 +16,17 @@ extend type Query { monitorV2DefinitionByVersion(id: ObjectId!, version: Int64!): MonitorV2Definition! """ - monitorV2TemplateDictionary takes in the monitor v2 input and the alarm input to produce a template dictionary - for the frontend which can be used to render the template. + Takes in the monitor v2 input and the alarm input to produce a template dictionary + for the frontend which can be used to render the template. The URLs generated will be the normal + URLs in the observe UI, but with zero value identifiers. The optional workspaceId is so that + any URLs with a workspaceId have the correct value. """ - monitorV2TemplateDictionary(alertType: MonitorV2AlertType, monitorInput: MonitorV2Input!, alarmInput: MonitorV2AlarmInput!): TemplateDictionary! + monitorV2TemplateDictionary( + alertType: MonitorV2AlertType, + monitorInput: MonitorV2Input!, + alarmInput: MonitorV2AlarmInput!, + workspaceId: ObjectId, + ): TemplateDictionary! """ Receive an actionInput and sample data payload to render all the fields in the mustache template. @@ -28,7 +35,7 @@ extend type Query { monitorV2RenderTemplate(templateDict: JsonObject!, actionInput: MonitorV2ActionInput!): RenderedTemplate! """ - searchMonitorV2Alarms can be used to query alerts in the explorer using various optional filters. + Can be used to query alerts in the explorer using various optional filters. monitorIds optionally restricts to a specific monitors nameSubstring restricts to monitors with partial match on the name @@ -57,7 +64,7 @@ extend type Query { extend type Mutation { """ - saveMonitorV2Relations replaces all monitor relations (MonitorV2ActionRule, ActionDestinationLink) + Replaces all monitor relations (MonitorV2ActionRule, ActionDestinationLink) for the provided monitor with the provided list of actionRules and destinationLinks. Shared Actions can't be mutated through this call other than attaching it to the monitor, so you will need to used saveActionWithDestinationLinks to mutate sharedAction's links to the destinations. @@ -68,7 +75,7 @@ extend type Mutation { saveMonitorV2Relations(monitorId: ObjectId!, actionRelations: [ActionRelationInput!]): MonitorV2! """ - terminateMonitorV2Alarm allows an explicit termination of an active alarm. The purpose is to + Allows an explicit termination of an active alarm. The purpose is to give the user the ability to eliminate via termination an active alarm that for some reason did not end normally. This is possibly an escape hatch for early adoption bugs, but could end up being an imperative for edge cases we haven't anticipated and where a snooze or mute is @@ -77,24 +84,43 @@ extend type Mutation { terminateMonitorV2Alarm(alarmId: String!): MonitorV2Alarm! """ - unmuteMonitorV2 is a fast way to remove all mute rules tied to a single monitor. + A fast way to remove all mute rules tied to a single monitor. note: This has no impact on global mutes that may exist, which always apply to all monitors. """ unmuteMonitorV2(id: ObjectId!): MonitorV2! """ - setMonitorV2Enabled sets the enabled/disabled-ness of a monitor. Disabling a monitor will + Sets the enabled/disabled-ness of a monitor. Disabling a monitor will stop evaluation and detection of the monitor. """ setMonitorV2Enabled(id: ObjectId!, enabled: Boolean!): MonitorV2! """ - sendMonitorV2TestAlert takes in the template dictionary generated from monitorV2TemplateDictionary along with + Takes in the template dictionary generated from monitorV2TemplateDictionary along with the actionInput and destinationInputs object. Afterward, it sends a test alert to see whether the customer can receive an alert at their designated end point. """ testMonitorV2Alert(templateDict: JsonObject!, actionInput: MonitorV2ActionInput!): ResultStatus! + + """ + SaveMonitorV2WithActions builds on the primitives of createMonitorV2, updateMonitorV2, + createMonitorV2Action, updateMonitorV2Action, deleteMonitorV2Action, and saveMonitorV2Relations. + The intent is to provide a one-shot API for easier use in terraforming and a more transactional + API for the front-end. The way this function works is as follows. + + If monitorId is set, the input field is considered an update to an existing monitor. Otherwise, + this is treated as a creation. + + The actions list is the ordered list the user is creating the actions. It can include existing + actions (typically already-created shared actions) or actions to be created (typically inline). + """ + saveMonitorV2WithActions( + workspaceId: ObjectId!, + monitorId: ObjectId, + input: MonitorV2Input!, + actions: [MonitorV2ActionAndRelationInput!], + ): MonitorV2! } # Future placeholder for mutation in one go -- an optional definition can be added along with what type of mutation @@ -126,3 +152,25 @@ type RenderedWebhook @goModel(model: "observe/meta/metatypes.RenderedWebhook") { type MonitorV2AlarmSearchResult @goModel(model: "observe/meta/metatypes.MonitorV2AlarmSearchResult") { results: [MonitorV2Alarm!]! } + +""" +MonitorV2ActionAndRelationInput allows for defining a relation and an optional action at the same +time for the saveMonitorV2WithActions function. This emulates what the primitives +do using MonitorV2ActionInput and MonitorV2ActionRuleInput. + +One of `action` or `actionID` is required. The `actionID` references an existing +action (typically a shared action) or the `action` can be defined and created +in this input. + +The remaining parameters (like `levels` and others) are to bind the +relationship (see saveMonitorV2Relations) to the monitor. +""" +input MonitorV2ActionAndRelationInput @goModel(model: "observe/meta/metatypes.MonitorV2ActionAndRelationInput") { + action: MonitorV2ActionInput + actionID: ObjectId + + levels: [MonitorV2AlarmLevel!] + conditions: MonitorV2ComparisonExpressionInput + sendEndNotifications: Boolean + sendRemindersInterval: Duration +} diff --git a/client/meta/genqlient.generated.go b/client/meta/genqlient.generated.go index c626e6cf..a2677fd5 100644 --- a/client/meta/genqlient.generated.go +++ b/client/meta/genqlient.generated.go @@ -4444,7 +4444,7 @@ func (v *MonitorRuleThresholdInput) GetThresholdAggFunction() *ThresholdAggFunct // GetExpressionSummary returns MonitorRuleThresholdInput.ExpressionSummary, and is useful for accessing the field via an interface. func (v *MonitorRuleThresholdInput) GetExpressionSummary() *string { return v.ExpressionSummary } -// MonitorV2 includes the GraphQL fields of MonitorV2 requested by the fragment MonitorV2. +// @genclient(for: "MonitorV2ComparisonExpressionInput.conditions", omitempty: true) type MonitorV2 struct { Id string `json:"id"` WorkspaceId string `json:"workspaceId"` @@ -4553,6 +4553,70 @@ func (v *MonitorV2Action) GetCreatedBy() types.UserIdScalar { return v.CreatedBy // GetCreatedDate returns MonitorV2Action.CreatedDate, and is useful for accessing the field via an interface. func (v *MonitorV2Action) GetCreatedDate() types.TimeScalar { return v.CreatedDate } +// MonitorV2ActionAndRelationInput allows for defining a relation and an optional action at the same +// time for the saveMonitorV2WithActions function. This emulates what the primitives +// do using MonitorV2ActionInput and MonitorV2ActionRuleInput. +// +// One of `action` or `actionID` is required. The `actionID` references an existing +// action (typically a shared action) or the `action` can be defined and created +// in this input. +// +// The remaining parameters (like `levels` and others) are to bind the +// relationship (see saveMonitorV2Relations) to the monitor. +type MonitorV2ActionAndRelationInput struct { + Action *MonitorV2ActionInput `json:"action,omitempty"` + ActionID *string `json:"actionID,omitempty"` + Levels []MonitorV2AlarmLevel `json:"levels,omitempty"` + Conditions *MonitorV2ComparisonExpressionInput `json:"conditions,omitempty"` + SendEndNotifications *bool `json:"sendEndNotifications,omitempty"` + SendRemindersInterval *types.DurationScalar `json:"sendRemindersInterval,omitempty"` +} + +// GetAction returns MonitorV2ActionAndRelationInput.Action, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionAndRelationInput) GetAction() *MonitorV2ActionInput { return v.Action } + +// GetActionID returns MonitorV2ActionAndRelationInput.ActionID, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionAndRelationInput) GetActionID() *string { return v.ActionID } + +// GetLevels returns MonitorV2ActionAndRelationInput.Levels, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionAndRelationInput) GetLevels() []MonitorV2AlarmLevel { return v.Levels } + +// GetConditions returns MonitorV2ActionAndRelationInput.Conditions, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionAndRelationInput) GetConditions() *MonitorV2ComparisonExpressionInput { + return v.Conditions +} + +// GetSendEndNotifications returns MonitorV2ActionAndRelationInput.SendEndNotifications, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionAndRelationInput) GetSendEndNotifications() *bool { + return v.SendEndNotifications +} + +// GetSendRemindersInterval returns MonitorV2ActionAndRelationInput.SendRemindersInterval, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionAndRelationInput) GetSendRemindersInterval() *types.DurationScalar { + return v.SendRemindersInterval +} + +// MonitorV2ActionDefinition includes the GraphQL fields of MonitorV2ActionDefinition requested by the fragment MonitorV2ActionDefinition. +type MonitorV2ActionDefinition struct { + // The inline field determines whether the object is inlined within another object or not. If not inlined, it can be shared with other objects. + Inline *bool `json:"inline"` + Type MonitorV2ActionType `json:"type"` + Email *MonitorV2EmailAction `json:"email"` + Webhook *MonitorV2WebhookAction `json:"webhook"` +} + +// GetInline returns MonitorV2ActionDefinition.Inline, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionDefinition) GetInline() *bool { return v.Inline } + +// GetType returns MonitorV2ActionDefinition.Type, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionDefinition) GetType() MonitorV2ActionType { return v.Type } + +// GetEmail returns MonitorV2ActionDefinition.Email, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionDefinition) GetEmail() *MonitorV2EmailAction { return v.Email } + +// GetWebhook returns MonitorV2ActionDefinition.Webhook, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionDefinition) GetWebhook() *MonitorV2WebhookAction { return v.Webhook } + type MonitorV2ActionInput struct { Inline *bool `json:"inline"` Type MonitorV2ActionType `json:"type"` @@ -4596,10 +4660,17 @@ func (v *MonitorV2ActionInput) GetFolderId() *string { return v.FolderId } type MonitorV2ActionRule struct { // Takes in a private or public action id created from an earlier createAction API call. ActionID string `json:"actionID"` - // Dispatch this action when the alarm matches any of the provided levels. - Levels []MonitorV2AlarmLevel `json:"levels"` - SendEndNotifications *bool `json:"sendEndNotifications"` + // Dispatch this action when the alarm matches any of the provided levels + // AND'd with any of the optional conditions. + Levels []MonitorV2AlarmLevel `json:"levels"` + // Send notifications when the condition ends. + // note: At this time, this only happens on the AlarmEnded event. + SendEndNotifications *bool `json:"sendEndNotifications"` + // Send a reminder notification for as long as the condition is active + // on this interval. SendRemindersInterval *types.DurationScalar `json:"sendRemindersInterval"` + // Included to be shown as part of the MonitorV2 output. + Definition MonitorV2ActionDefinition `json:"definition"` } // GetActionID returns MonitorV2ActionRule.ActionID, and is useful for accessing the field via an interface. @@ -4616,11 +4687,15 @@ func (v *MonitorV2ActionRule) GetSendRemindersInterval() *types.DurationScalar { return v.SendRemindersInterval } +// GetDefinition returns MonitorV2ActionRule.Definition, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionRule) GetDefinition() MonitorV2ActionDefinition { return v.Definition } + type MonitorV2ActionRuleInput struct { - ActionID string `json:"actionID"` - Levels []MonitorV2AlarmLevel `json:"levels"` - SendEndNotifications *bool `json:"sendEndNotifications,omitempty"` - SendRemindersInterval *types.DurationScalar `json:"sendRemindersInterval,omitempty"` + ActionID string `json:"actionID"` + Levels []MonitorV2AlarmLevel `json:"levels,omitempty"` + Conditions *MonitorV2ComparisonExpressionInput `json:"conditions,omitempty"` + SendEndNotifications *bool `json:"sendEndNotifications,omitempty"` + SendRemindersInterval *types.DurationScalar `json:"sendRemindersInterval,omitempty"` } // GetActionID returns MonitorV2ActionRuleInput.ActionID, and is useful for accessing the field via an interface. @@ -4629,6 +4704,11 @@ func (v *MonitorV2ActionRuleInput) GetActionID() string { return v.ActionID } // GetLevels returns MonitorV2ActionRuleInput.Levels, and is useful for accessing the field via an interface. func (v *MonitorV2ActionRuleInput) GetLevels() []MonitorV2AlarmLevel { return v.Levels } +// GetConditions returns MonitorV2ActionRuleInput.Conditions, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionRuleInput) GetConditions() *MonitorV2ComparisonExpressionInput { + return v.Conditions +} + // GetSendEndNotifications returns MonitorV2ActionRuleInput.SendEndNotifications, and is useful for accessing the field via an interface. func (v *MonitorV2ActionRuleInput) GetSendEndNotifications() *bool { return v.SendEndNotifications } @@ -4666,6 +4746,13 @@ const ( MonitorV2AlarmLevelWarning MonitorV2AlarmLevel = "Warning" ) +type MonitorV2BooleanOperator string + +const ( + MonitorV2BooleanOperatorAnd MonitorV2BooleanOperator = "And" + MonitorV2BooleanOperatorOr MonitorV2BooleanOperator = "Or" +) + // MonitorV2Column includes the GraphQL fields of MonitorV2Column requested by the fragment MonitorV2Column. type MonitorV2Column struct { // Link Column is for link typed column which the user wants to group by. @@ -4748,7 +4835,7 @@ func (v *MonitorV2ColumnPathInput) GetPath() *string { return v.Path } // MonitorV2Comparison includes the GraphQL fields of MonitorV2Comparison requested by the fragment MonitorV2Comparison. type MonitorV2Comparison struct { CompareFn MonitorV2ComparisonFunction `json:"compareFn"` - // compareValue is the right-side value for comparisons that use it (like x > 10, this is 10). + // The right-side value for comparisons that use it (like x > 10, this is 10). CompareValue PrimitiveValue `json:"compareValue"` } @@ -4758,6 +4845,27 @@ func (v *MonitorV2Comparison) GetCompareFn() MonitorV2ComparisonFunction { retur // GetCompareValue returns MonitorV2Comparison.CompareValue, and is useful for accessing the field via an interface. func (v *MonitorV2Comparison) GetCompareValue() PrimitiveValue { return v.CompareValue } +type MonitorV2ComparisonExpressionInput struct { + CompareTerms []MonitorV2ComparisonTermInput `json:"compareTerms"` + SubExpressions []MonitorV2ComparisonExpressionInput `json:"subExpressions"` + Operator MonitorV2BooleanOperator `json:"operator"` +} + +// GetCompareTerms returns MonitorV2ComparisonExpressionInput.CompareTerms, and is useful for accessing the field via an interface. +func (v *MonitorV2ComparisonExpressionInput) GetCompareTerms() []MonitorV2ComparisonTermInput { + return v.CompareTerms +} + +// GetSubExpressions returns MonitorV2ComparisonExpressionInput.SubExpressions, and is useful for accessing the field via an interface. +func (v *MonitorV2ComparisonExpressionInput) GetSubExpressions() []MonitorV2ComparisonExpressionInput { + return v.SubExpressions +} + +// GetOperator returns MonitorV2ComparisonExpressionInput.Operator, and is useful for accessing the field via an interface. +func (v *MonitorV2ComparisonExpressionInput) GetOperator() MonitorV2BooleanOperator { + return v.Operator +} + type MonitorV2ComparisonFunction string const ( @@ -4784,6 +4892,17 @@ func (v *MonitorV2ComparisonInput) GetCompareFn() MonitorV2ComparisonFunction { // GetCompareValue returns MonitorV2ComparisonInput.CompareValue, and is useful for accessing the field via an interface. func (v *MonitorV2ComparisonInput) GetCompareValue() PrimitiveValueInput { return v.CompareValue } +type MonitorV2ComparisonTermInput struct { + Comparison MonitorV2ComparisonInput `json:"comparison"` + Column MonitorV2ColumnInput `json:"column"` +} + +// GetComparison returns MonitorV2ComparisonTermInput.Comparison, and is useful for accessing the field via an interface. +func (v *MonitorV2ComparisonTermInput) GetComparison() MonitorV2ComparisonInput { return v.Comparison } + +// GetColumn returns MonitorV2ComparisonTermInput.Column, and is useful for accessing the field via an interface. +func (v *MonitorV2ComparisonTermInput) GetColumn() MonitorV2ColumnInput { return v.Column } + // MonitorV2CountRule includes the GraphQL fields of MonitorV2CountRule requested by the fragment MonitorV2CountRule. type MonitorV2CountRule struct { // CompareValues is a list of comparisons that provide an implicit AND where all comparisons must match. @@ -4839,6 +4958,10 @@ type MonitorV2Definition struct { // to arrive later than other data and thus would change previously evaluated results. Another way to think of this // value is defining where the "Ragged Right Edge" starts relative to the clock. DataStabilizationDelay *types.DurationScalar `json:"dataStabilizationDelay"` + // MaxAlertsPerHour sets the rate allowed before a monitor is considered possibly bad + // and automatically disabled by the system. This has a default value of 100 if null/unset. + // A value of 0 means "no limit". + MaxAlertsPerHour *types.Int64Scalar `json:"maxAlertsPerHour"` // Groupings describes the groups that logically separate events/rows/etc from each other. // When the input monitor dataset is of type resource and the monitor strategy is of type promote, this field should // either be left empty to be mutated with the primary keys of the resource dataset or it should only contain the @@ -4866,6 +4989,9 @@ func (v *MonitorV2Definition) GetDataStabilizationDelay() *types.DurationScalar return v.DataStabilizationDelay } +// GetMaxAlertsPerHour returns MonitorV2Definition.MaxAlertsPerHour, and is useful for accessing the field via an interface. +func (v *MonitorV2Definition) GetMaxAlertsPerHour() *types.Int64Scalar { return v.MaxAlertsPerHour } + // GetGroupings returns MonitorV2Definition.Groupings, and is useful for accessing the field via an interface. func (v *MonitorV2Definition) GetGroupings() []MonitorV2Column { return v.Groupings } @@ -4877,6 +5003,7 @@ type MonitorV2DefinitionInput struct { Rules []MonitorV2RuleInput `json:"rules"` LookbackTime *types.DurationScalar `json:"lookbackTime"` DataStabilizationDelay *types.DurationScalar `json:"dataStabilizationDelay,omitempty"` + MaxAlertsPerHour *types.Int64Scalar `json:"maxAlertsPerHour,omitempty"` Groupings []MonitorV2ColumnInput `json:"groupings"` Scheduling *MonitorV2SchedulingInput `json:"scheduling"` } @@ -4895,6 +5022,11 @@ func (v *MonitorV2DefinitionInput) GetDataStabilizationDelay() *types.DurationSc return v.DataStabilizationDelay } +// GetMaxAlertsPerHour returns MonitorV2DefinitionInput.MaxAlertsPerHour, and is useful for accessing the field via an interface. +func (v *MonitorV2DefinitionInput) GetMaxAlertsPerHour() *types.Int64Scalar { + return v.MaxAlertsPerHour +} + // GetGroupings returns MonitorV2DefinitionInput.Groupings, and is useful for accessing the field via an interface. func (v *MonitorV2DefinitionInput) GetGroupings() []MonitorV2ColumnInput { return v.Groupings } @@ -4975,14 +5107,15 @@ const ( ) type MonitorV2Input struct { - Comment *string `json:"comment"` - Definition MonitorV2DefinitionInput `json:"definition"` - RuleKind MonitorV2RuleKind `json:"ruleKind"` - Name string `json:"name"` - IconUrl *string `json:"iconUrl,omitempty"` - Description *string `json:"description,omitempty"` - ManagedById *string `json:"managedById,omitempty"` - FolderId *string `json:"folderId,omitempty"` + Comment *string `json:"comment"` + Definition MonitorV2DefinitionInput `json:"definition"` + RuleKind MonitorV2RuleKind `json:"ruleKind"` + InvestigationInfo *MonitorV2InvestigationInfoInput `json:"investigationInfo"` + Name string `json:"name"` + IconUrl *string `json:"iconUrl,omitempty"` + Description *string `json:"description,omitempty"` + ManagedById *string `json:"managedById,omitempty"` + FolderId *string `json:"folderId,omitempty"` } // GetComment returns MonitorV2Input.Comment, and is useful for accessing the field via an interface. @@ -4994,6 +5127,11 @@ func (v *MonitorV2Input) GetDefinition() MonitorV2DefinitionInput { return v.Def // GetRuleKind returns MonitorV2Input.RuleKind, and is useful for accessing the field via an interface. func (v *MonitorV2Input) GetRuleKind() MonitorV2RuleKind { return v.RuleKind } +// GetInvestigationInfo returns MonitorV2Input.InvestigationInfo, and is useful for accessing the field via an interface. +func (v *MonitorV2Input) GetInvestigationInfo() *MonitorV2InvestigationInfoInput { + return v.InvestigationInfo +} + // GetName returns MonitorV2Input.Name, and is useful for accessing the field via an interface. func (v *MonitorV2Input) GetName() string { return v.Name } @@ -5038,6 +5176,13 @@ func (v *MonitorV2IntervalScheduleInput) GetInterval() types.DurationScalar { re // GetRandomize returns MonitorV2IntervalScheduleInput.Randomize, and is useful for accessing the field via an interface. func (v *MonitorV2IntervalScheduleInput) GetRandomize() types.DurationScalar { return v.Randomize } +type MonitorV2InvestigationInfoInput struct { + RunbookContent string `json:"runbookContent"` +} + +// GetRunbookContent returns MonitorV2InvestigationInfoInput.RunbookContent, and is useful for accessing the field via an interface. +func (v *MonitorV2InvestigationInfoInput) GetRunbookContent() string { return v.RunbookContent } + // MonitorV2LinkColumn includes the GraphQL fields of MonitorV2LinkColumn requested by the fragment MonitorV2LinkColumn. type MonitorV2LinkColumn struct { Name string `json:"name"` @@ -9393,6 +9538,28 @@ func (v *__saveMonitorV2RelationsInput) GetActionRelations() []ActionRelationInp return v.ActionRelations } +// __saveMonitorV2WithActionsInput is used internally by genqlient +type __saveMonitorV2WithActionsInput struct { + WorkspaceId string `json:"workspaceId"` + MonitorId *string `json:"monitorId"` + Input MonitorV2Input `json:"input"` + Actions []MonitorV2ActionAndRelationInput `json:"actions"` +} + +// GetWorkspaceId returns __saveMonitorV2WithActionsInput.WorkspaceId, and is useful for accessing the field via an interface. +func (v *__saveMonitorV2WithActionsInput) GetWorkspaceId() string { return v.WorkspaceId } + +// GetMonitorId returns __saveMonitorV2WithActionsInput.MonitorId, and is useful for accessing the field via an interface. +func (v *__saveMonitorV2WithActionsInput) GetMonitorId() *string { return v.MonitorId } + +// GetInput returns __saveMonitorV2WithActionsInput.Input, and is useful for accessing the field via an interface. +func (v *__saveMonitorV2WithActionsInput) GetInput() MonitorV2Input { return v.Input } + +// GetActions returns __saveMonitorV2WithActionsInput.Actions, and is useful for accessing the field via an interface. +func (v *__saveMonitorV2WithActionsInput) GetActions() []MonitorV2ActionAndRelationInput { + return v.Actions +} + // __saveSourceDatasetInput is used internally by genqlient type __saveSourceDatasetInput struct { WorkspaceId string `json:"workspaceId"` @@ -11271,7 +11438,7 @@ func (v *saveDatasetResponse) GetDataset() *saveDatasetDatasetDatasetSaveResult // saveMonitorV2RelationsResponse is returned by saveMonitorV2Relations on success. type saveMonitorV2RelationsResponse struct { - // saveMonitorV2Relations replaces all monitor relations (MonitorV2ActionRule, ActionDestinationLink) + // Replaces all monitor relations (MonitorV2ActionRule, ActionDestinationLink) // for the provided monitor with the provided list of actionRules and destinationLinks. // Shared Actions can't be mutated through this call other than attaching it to the monitor, so you will need to used // saveActionWithDestinationLinks to mutate sharedAction's links to the destinations. @@ -11284,6 +11451,24 @@ type saveMonitorV2RelationsResponse struct { // GetMonitorV2 returns saveMonitorV2RelationsResponse.MonitorV2, and is useful for accessing the field via an interface. func (v *saveMonitorV2RelationsResponse) GetMonitorV2() MonitorV2 { return v.MonitorV2 } +// saveMonitorV2WithActionsResponse is returned by saveMonitorV2WithActions on success. +type saveMonitorV2WithActionsResponse struct { + // SaveMonitorV2WithActions builds on the primitives of createMonitorV2, updateMonitorV2, + // createMonitorV2Action, updateMonitorV2Action, deleteMonitorV2Action, and saveMonitorV2Relations. + // The intent is to provide a one-shot API for easier use in terraforming and a more transactional + // API for the front-end. The way this function works is as follows. + // + // If monitorId is set, the input field is considered an update to an existing monitor. Otherwise, + // this is treated as a creation. + // + // The actions list is the ordered list the user is creating the actions. It can include existing + // actions (typically already-created shared actions) or actions to be created (typically inline). + MonitorV2 MonitorV2 `json:"monitorV2"` +} + +// GetMonitorV2 returns saveMonitorV2WithActionsResponse.MonitorV2, and is useful for accessing the field via an interface. +func (v *saveMonitorV2WithActionsResponse) GetMonitorV2() MonitorV2 { return v.MonitorV2 } + // saveSourceDatasetDatasetDatasetSaveResult includes the requested fields of the GraphQL type DatasetSaveResult. type saveSourceDatasetDatasetDatasetSaveResult struct { // this is what you got out when saving @@ -12860,6 +13045,7 @@ fragment MonitorV2Definition on MonitorV2Definition { } lookbackTime dataStabilizationDelay + maxAlertsPerHour groupings { ... MonitorV2Column } @@ -12872,6 +13058,9 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { levels sendEndNotifications sendRemindersInterval + definition { + ... MonitorV2ActionDefinition + } } fragment StageQuery on StageQuery { id @@ -12914,6 +13103,16 @@ fragment MonitorV2Scheduling on MonitorV2Scheduling { ... MonitorV2TransformSchedule } } +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + email { + ... MonitorV2EmailAction + } + webhook { + ... MonitorV2WebhookAction + } +} fragment MonitorV2CountRule on MonitorV2CountRule { compareValues { ... MonitorV2Comparison @@ -12954,6 +13153,22 @@ fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { freshnessGoal } +fragment MonitorV2EmailAction on MonitorV2EmailAction { + users + addresses + subject + body + fragments +} +fragment MonitorV2WebhookAction on MonitorV2WebhookAction { + headers { + ... MonitorV2WebhookHeader + } + body + fragments + url + method +} fragment MonitorV2Comparison on MonitorV2Comparison { compareFn compareValue { @@ -12975,6 +13190,10 @@ fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { dstFields targetDataset } +fragment MonitorV2WebhookHeader on MonitorV2WebhookHeader { + header + value +} fragment PrimitiveValue on PrimitiveValue { bool float64 @@ -12985,6 +13204,7 @@ fragment PrimitiveValue on PrimitiveValue { } ` +// @genclient(for: "MonitorV2ComparisonExpressionInput.conditions", omitempty: true) func createMonitorV2( ctx context.Context, client graphql.Client, @@ -16298,6 +16518,7 @@ fragment MonitorV2Definition on MonitorV2Definition { } lookbackTime dataStabilizationDelay + maxAlertsPerHour groupings { ... MonitorV2Column } @@ -16310,6 +16531,9 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { levels sendEndNotifications sendRemindersInterval + definition { + ... MonitorV2ActionDefinition + } } fragment StageQuery on StageQuery { id @@ -16352,6 +16576,16 @@ fragment MonitorV2Scheduling on MonitorV2Scheduling { ... MonitorV2TransformSchedule } } +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + email { + ... MonitorV2EmailAction + } + webhook { + ... MonitorV2WebhookAction + } +} fragment MonitorV2CountRule on MonitorV2CountRule { compareValues { ... MonitorV2Comparison @@ -16392,6 +16626,22 @@ fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { freshnessGoal } +fragment MonitorV2EmailAction on MonitorV2EmailAction { + users + addresses + subject + body + fragments +} +fragment MonitorV2WebhookAction on MonitorV2WebhookAction { + headers { + ... MonitorV2WebhookHeader + } + body + fragments + url + method +} fragment MonitorV2Comparison on MonitorV2Comparison { compareFn compareValue { @@ -16413,6 +16663,10 @@ fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { dstFields targetDataset } +fragment MonitorV2WebhookHeader on MonitorV2WebhookHeader { + header + value +} fragment PrimitiveValue on PrimitiveValue { bool float64 @@ -17909,6 +18163,7 @@ fragment MonitorV2Definition on MonitorV2Definition { } lookbackTime dataStabilizationDelay + maxAlertsPerHour groupings { ... MonitorV2Column } @@ -17921,6 +18176,9 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { levels sendEndNotifications sendRemindersInterval + definition { + ... MonitorV2ActionDefinition + } } fragment StageQuery on StageQuery { id @@ -17963,6 +18221,16 @@ fragment MonitorV2Scheduling on MonitorV2Scheduling { ... MonitorV2TransformSchedule } } +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + email { + ... MonitorV2EmailAction + } + webhook { + ... MonitorV2WebhookAction + } +} fragment MonitorV2CountRule on MonitorV2CountRule { compareValues { ... MonitorV2Comparison @@ -18003,6 +18271,22 @@ fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { freshnessGoal } +fragment MonitorV2EmailAction on MonitorV2EmailAction { + users + addresses + subject + body + fragments +} +fragment MonitorV2WebhookAction on MonitorV2WebhookAction { + headers { + ... MonitorV2WebhookHeader + } + body + fragments + url + method +} fragment MonitorV2Comparison on MonitorV2Comparison { compareFn compareValue { @@ -18024,6 +18308,10 @@ fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { dstFields targetDataset } +fragment MonitorV2WebhookHeader on MonitorV2WebhookHeader { + header + value +} fragment PrimitiveValue on PrimitiveValue { bool float64 @@ -18478,6 +18766,7 @@ fragment MonitorV2Definition on MonitorV2Definition { } lookbackTime dataStabilizationDelay + maxAlertsPerHour groupings { ... MonitorV2Column } @@ -18490,6 +18779,9 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { levels sendEndNotifications sendRemindersInterval + definition { + ... MonitorV2ActionDefinition + } } fragment StageQuery on StageQuery { id @@ -18532,6 +18824,16 @@ fragment MonitorV2Scheduling on MonitorV2Scheduling { ... MonitorV2TransformSchedule } } +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + email { + ... MonitorV2EmailAction + } + webhook { + ... MonitorV2WebhookAction + } +} fragment MonitorV2CountRule on MonitorV2CountRule { compareValues { ... MonitorV2Comparison @@ -18572,6 +18874,22 @@ fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { freshnessGoal } +fragment MonitorV2EmailAction on MonitorV2EmailAction { + users + addresses + subject + body + fragments +} +fragment MonitorV2WebhookAction on MonitorV2WebhookAction { + headers { + ... MonitorV2WebhookHeader + } + body + fragments + url + method +} fragment MonitorV2Comparison on MonitorV2Comparison { compareFn compareValue { @@ -18593,6 +18911,10 @@ fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { dstFields targetDataset } +fragment MonitorV2WebhookHeader on MonitorV2WebhookHeader { + header + value +} fragment PrimitiveValue on PrimitiveValue { bool float64 @@ -18631,6 +18953,235 @@ func saveMonitorV2Relations( return &data, err } +// The query or mutation executed by saveMonitorV2WithActions. +const saveMonitorV2WithActions_Operation = ` +mutation saveMonitorV2WithActions ($workspaceId: ObjectId!, $monitorId: ObjectId, $input: MonitorV2Input!, $actions: [MonitorV2ActionAndRelationInput!]) { + monitorV2: saveMonitorV2WithActions(workspaceId: $workspaceId, monitorId: $monitorId, input: $input, actions: $actions) { + ... MonitorV2 + } +} +fragment MonitorV2 on MonitorV2 { + id + workspaceId + createdBy + createdDate + name + iconUrl + description + managedById + folderId + rollupStatus + ruleKind + definition { + ... MonitorV2Definition + } + actionRules { + ... MonitorV2ActionRule + } +} +fragment MonitorV2Definition on MonitorV2Definition { + inputQuery { + outputStage + stages { + ... StageQuery + } + } + rules { + ... MonitorV2Rule + } + lookbackTime + dataStabilizationDelay + maxAlertsPerHour + groupings { + ... MonitorV2Column + } + scheduling { + ... MonitorV2Scheduling + } +} +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels + sendEndNotifications + sendRemindersInterval + definition { + ... MonitorV2ActionDefinition + } +} +fragment StageQuery on StageQuery { + id + pipeline + params + layout + input { + inputName + inputRole + datasetId + datasetPath + stageId + } +} +fragment MonitorV2Rule on MonitorV2Rule { + level + count { + ... MonitorV2CountRule + } + threshold { + ... MonitorV2ThresholdRule + } + promote { + ... MonitorV2PromoteRule + } +} +fragment MonitorV2Column on MonitorV2Column { + linkColumn { + ... MonitorV2LinkColumn + } + columnPath { + ... MonitorV2ColumnPath + } +} +fragment MonitorV2Scheduling on MonitorV2Scheduling { + interval { + ... MonitorV2IntervalSchedule + } + transform { + ... MonitorV2TransformSchedule + } +} +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + email { + ... MonitorV2EmailAction + } + webhook { + ... MonitorV2WebhookAction + } +} +fragment MonitorV2CountRule on MonitorV2CountRule { + compareValues { + ... MonitorV2Comparison + } + compareGroups { + ... MonitorV2ColumnComparison + } +} +fragment MonitorV2ThresholdRule on MonitorV2ThresholdRule { + compareValues { + ... MonitorV2Comparison + } + valueColumnName + aggregation + compareGroups { + ... MonitorV2ColumnComparison + } +} +fragment MonitorV2PromoteRule on MonitorV2PromoteRule { + compareColumns { + ... MonitorV2ColumnComparison + } +} +fragment MonitorV2LinkColumn on MonitorV2LinkColumn { + name + meta { + ... MonitorV2LinkColumnMeta + } +} +fragment MonitorV2ColumnPath on MonitorV2ColumnPath { + name + path +} +fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { + interval + randomize +} +fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { + freshnessGoal +} +fragment MonitorV2EmailAction on MonitorV2EmailAction { + users + addresses + subject + body + fragments +} +fragment MonitorV2WebhookAction on MonitorV2WebhookAction { + headers { + ... MonitorV2WebhookHeader + } + body + fragments + url + method +} +fragment MonitorV2Comparison on MonitorV2Comparison { + compareFn + compareValue { + ... PrimitiveValue + } +} +fragment MonitorV2ColumnComparison on MonitorV2ColumnComparison { + column { + ... MonitorV2Column + } + compareValues { + ... MonitorV2Comparison + } +} +fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { + srcFields { + ... MonitorV2ColumnPath + } + dstFields + targetDataset +} +fragment MonitorV2WebhookHeader on MonitorV2WebhookHeader { + header + value +} +fragment PrimitiveValue on PrimitiveValue { + bool + float64 + int64 + string + timestamp + duration +} +` + +func saveMonitorV2WithActions( + ctx context.Context, + client graphql.Client, + workspaceId string, + monitorId *string, + input MonitorV2Input, + actions []MonitorV2ActionAndRelationInput, +) (*saveMonitorV2WithActionsResponse, error) { + req := &graphql.Request{ + OpName: "saveMonitorV2WithActions", + Query: saveMonitorV2WithActions_Operation, + Variables: &__saveMonitorV2WithActionsInput{ + WorkspaceId: workspaceId, + MonitorId: monitorId, + Input: input, + Actions: actions, + }, + } + var err error + + var data saveMonitorV2WithActionsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + // The query or mutation executed by saveSourceDataset. const saveSourceDataset_Operation = ` mutation saveSourceDataset ($workspaceId: ObjectId!, $datasetDefinition: DatasetDefinitionInput!, $sourceTable: SourceTableDefinitionInput!, $dep: DependencyHandlingInput) { @@ -20133,6 +20684,7 @@ fragment MonitorV2Definition on MonitorV2Definition { } lookbackTime dataStabilizationDelay + maxAlertsPerHour groupings { ... MonitorV2Column } @@ -20145,6 +20697,9 @@ fragment MonitorV2ActionRule on MonitorV2ActionRule { levels sendEndNotifications sendRemindersInterval + definition { + ... MonitorV2ActionDefinition + } } fragment StageQuery on StageQuery { id @@ -20187,6 +20742,16 @@ fragment MonitorV2Scheduling on MonitorV2Scheduling { ... MonitorV2TransformSchedule } } +fragment MonitorV2ActionDefinition on MonitorV2ActionDefinition { + inline + type + email { + ... MonitorV2EmailAction + } + webhook { + ... MonitorV2WebhookAction + } +} fragment MonitorV2CountRule on MonitorV2CountRule { compareValues { ... MonitorV2Comparison @@ -20227,6 +20792,22 @@ fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { freshnessGoal } +fragment MonitorV2EmailAction on MonitorV2EmailAction { + users + addresses + subject + body + fragments +} +fragment MonitorV2WebhookAction on MonitorV2WebhookAction { + headers { + ... MonitorV2WebhookHeader + } + body + fragments + url + method +} fragment MonitorV2Comparison on MonitorV2Comparison { compareFn compareValue { @@ -20248,6 +20829,10 @@ fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { dstFields targetDataset } +fragment MonitorV2WebhookHeader on MonitorV2WebhookHeader { + header + value +} fragment PrimitiveValue on PrimitiveValue { bool float64 @@ -20258,6 +20843,7 @@ fragment PrimitiveValue on PrimitiveValue { } ` +// @genclient(for: "MonitorV2ComparisonExpressionInput.conditions", omitempty: true) func updateMonitorV2( ctx context.Context, client graphql.Client, diff --git a/client/meta/monitorv2.go b/client/meta/monitorv2.go index cf62a7e6..a693acc0 100644 --- a/client/meta/monitorv2.go +++ b/client/meta/monitorv2.go @@ -18,6 +18,17 @@ func monitorV2OrError(m monitorV2Response, err error) (*MonitorV2, error) { return &result, nil } +func (client *Client) SaveMonitorV2WithActions( + ctx context.Context, + workspaceId string, + monitorId *string, + input *MonitorV2Input, + actions []MonitorV2ActionAndRelationInput, +) (*MonitorV2, error) { + resp, err := saveMonitorV2WithActions(ctx, client.Gql, workspaceId, monitorId, *input, actions) + return monitorV2OrError(resp, err) +} + func (client *Client) CreateMonitorV2(ctx context.Context, workspaceId string, input *MonitorV2Input) (*MonitorV2, error) { resp, err := createMonitorV2(ctx, client.Gql, workspaceId, *input) return monitorV2OrError(resp, err) diff --git a/docs/data-sources/monitor_v2.md b/docs/data-sources/monitor_v2.md index 2dd0718c..4292d4a7 100644 --- a/docs/data-sources/monitor_v2.md +++ b/docs/data-sources/monitor_v2.md @@ -26,14 +26,15 @@ template and destinations to configure the receiver. ### Optional +- `data_stabilization_delay` (String) expresses the minimum time that should elapse before data is considered "good enough" to evaluate. Choosing a delay really depends on the expectations of latency of data and whether data is expected to arrive later than other data and thus would change previously evaluated results. - `id` (String) Resource ID for this object. +- `max_alerts_per_hour` (Number) overrides the default value of max alerts generated in a single hour before the monitor is deactivated for safety - `name` (String) Monitor name. - `workspace` (String) OID of the workspace this object is contained in. ### Read-Only - `actions` (Block List) The list of shared actions to which this monitor is connected. (see [below for nested schema](#nestedblock--actions)) -- `data_stabilization_delay` (String) expresses the minimum time that should elapse before data is considered "good enough" to evaluate. Choosing a delay really depends on the expectations of latency of data and whether data is expected to arrive later than other data and thus would change previously evaluated results. - `description` (String) A brief description of the monitor. - `groupings` (Block List) Describes the groups that logically separate events/rows/etc from each other. If monitor dataset is resource type and monitor strategy is promote, this field should be either empty or only contain the primary keys of the dataset. (see [below for nested schema](#nestedblock--groupings)) - `icon_url` (String) URL of the monitor icon. @@ -54,7 +55,7 @@ its predecessor. (see [below for nested schema](#nestedblock--stage)) Read-Only: - `levels` (List of String) The alarm level(s) at which this monitor should trigger this shared action. -- `oid` (String) The OID of this shared action. +- `oid` (String) The OID of this shared action. This should be used for existing shared actions. - `send_end_notifications` (Boolean) - `send_reminders_interval` (String) diff --git a/docs/resources/monitor_v2.md b/docs/resources/monitor_v2.md index 19ad7dd5..d705ff18 100644 --- a/docs/resources/monitor_v2.md +++ b/docs/resources/monitor_v2.md @@ -41,6 +41,7 @@ its predecessor. (see [below for nested schema](#nestedblock--stage)) - `groupings` (Block List) Describes the groups that logically separate events/rows/etc from each other. If monitor dataset is resource type and monitor strategy is promote, this field should be either empty or only contain the primary keys of the dataset. (see [below for nested schema](#nestedblock--groupings)) - `icon_url` (String) URL of the monitor icon. - `lookback_time` (String) optionally describes a duration that must be satisifed by this monitor. It applies to all rules, but is only applicable to rule kinds that utilize it. +- `max_alerts_per_hour` (Number) overrides the default value of max alerts generated in a single hour before the monitor is deactivated for safety - `scheduling` (Block List, Max: 1) Holds information about when the monitor should evaluate. The types of scheduling (interval, transform) are exclusive. If ommitted, defaults to transform. (see [below for nested schema](#nestedblock--scheduling)) ### Read-Only @@ -315,16 +316,67 @@ a stage preceding the last stage. The last stage is an output stage by default. ### Nested Schema for `actions` -Required: - -- `oid` (String) The OID of this shared action. - Optional: +- `action` (Block List, Max: 1) This value should be used for creating inline private actions. (see [below for nested schema](#nestedblock--actions--action)) - `levels` (List of String) The alarm level(s) at which this monitor should trigger this shared action. +- `oid` (String) The OID of this shared action. This should be used for existing shared actions. - `send_end_notifications` (Boolean) If true, notifications will be sent if the monitor stops triggering. - `send_reminders_interval` (String) Determines how frequently you will be reminded of an ongoing alert. + +### Nested Schema for `actions.action` + +Required: + +- `type` (String) + +Optional: + +- `description` (String) +- `email` (Block List, Max: 1) (see [below for nested schema](#nestedblock--actions--action--email)) +- `webhook` (Block List, Max: 1) (see [below for nested schema](#nestedblock--actions--action--webhook)) + + +### Nested Schema for `actions.action.email` + +Required: + +- `subject` (String) + +Optional: + +- `addresses` (List of String) +- `body` (String) +- `fragments` (String) +- `users` (List of String) + + + +### Nested Schema for `actions.action.webhook` + +Required: + +- `body` (String) +- `method` (String) +- `url` (String) + +Optional: + +- `fragments` (String) +- `headers` (Block List) (see [below for nested schema](#nestedblock--actions--action--webhook--headers)) + + +### Nested Schema for `actions.action.webhook.headers` + +Required: + +- `header` (String) +- `value` (String) + + + + ### Nested Schema for `groupings` diff --git a/observe/data_source_monitor_v2.go b/observe/data_source_monitor_v2.go index 65aae640..19beefc0 100644 --- a/observe/data_source_monitor_v2.go +++ b/observe/data_source_monitor_v2.go @@ -186,11 +186,18 @@ func dataSourceMonitorV2() *schema.Resource { Computed: true, Description: descriptions.Get("monitorv2", "schema", "lookback_time"), }, - "data_stabilization_delay": { // Duration + "data_stabilization_delay": { // Int64 Type: schema.TypeString, Computed: true, + Optional: true, Description: descriptions.Get("monitorv2", "schema", "data_stabilization_delay"), }, + "max_alerts_per_hour": { //Int64 + Type: schema.TypeInt, + Computed: true, + Optional: true, + Description: descriptions.Get("monitorv2", "schema", "max_alerts_per_hour"), + }, "groupings": { // [MonitorV2ColumnInput!] Type: schema.TypeList, Optional: true, diff --git a/observe/descriptions/monitorv2.yaml b/observe/descriptions/monitorv2.yaml index adcae20e..3dbb2314 100644 --- a/observe/descriptions/monitorv2.yaml +++ b/observe/descriptions/monitorv2.yaml @@ -40,6 +40,8 @@ schema: optionally describes a duration that must be satisifed by this monitor. It applies to all rules, but is only applicable to rule kinds that utilize it. data_stabilization_delay: | expresses the minimum time that should elapse before data is considered "good enough" to evaluate. Choosing a delay really depends on the expectations of latency of data and whether data is expected to arrive later than other data and thus would change previously evaluated results. + max_alerts_per_hour: | + overrides the default value of max alerts generated in a single hour before the monitor is deactivated for safety groupings: | Describes the groups that logically separate events/rows/etc from each other. If monitor dataset is resource type and monitor strategy is promote, this field should be either empty or only contain the primary keys of the dataset. scheduling: @@ -98,7 +100,9 @@ schema: description: | The list of shared actions to which this monitor is connected. oid: | - The OID of this shared action. + The OID of this shared action. This should be used for existing shared actions. + action: | + This value should be used for creating inline private actions. levels: | The alarm level(s) at which this monitor should trigger this shared action. send_end_notifications: | diff --git a/observe/resource_monitor_v2.go b/observe/resource_monitor_v2.go index fbe108cd..c1192010 100644 --- a/observe/resource_monitor_v2.go +++ b/observe/resource_monitor_v2.go @@ -203,6 +203,11 @@ func resourceMonitorV2() *schema.Resource { DiffSuppressFunc: diffSuppressTimeDurationZeroDistinctFromEmpty, Description: descriptions.Get("monitorv2", "schema", "data_stabilization_delay"), }, + "max_alerts_per_hour": { //Int64 + Type: schema.TypeInt, + Optional: true, + Description: descriptions.Get("monitorv2", "schema", "max_alerts_per_hour"), + }, "groupings": { // [MonitorV2ColumnInput!] Type: schema.TypeList, Optional: true, @@ -243,12 +248,48 @@ func resourceMonitorV2() *schema.Resource { Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "oid": { // ObjectId! + "oid": { // ObjectId Type: schema.TypeString, - Required: true, + Optional: true, ValidateDiagFunc: validateOID(oid.TypeMonitorV2Action), Description: descriptions.Get("monitorv2", "schema", "actions", "oid"), }, + "action": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: descriptions.Get("monitorv2", "schema", "actions", "action"), + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // fields of MonitorV2ActionInput + "type": { // MonitorV2ActionType! + Type: schema.TypeString, + ValidateDiagFunc: validateEnums(gql.AllMonitorV2ActionTypes), + Required: true, + }, + "email": { // MonitorV2EmailDestinationInput + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + // note: ExactlyOneOf doesn't work because it cannot be referenced through + // `actions` which is not a MaxItems=1 configuration. + //ExactlyOneOf: []string{"actions.0.action.0.email", "actions.0.action.0.webhook"}, + Elem: monitorV2EmailActionInput(), + }, + "webhook": { // MonitorV2WebhookDestinationInput + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + //ExactlyOneOf: []string{"actions.0.action.0.email", "actions.0.action.0.webhook"}, + Elem: monitorV2WebhookActionInput(), + }, + "description": { // String + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, "levels": { // [MonitorV2AlarmLevel!] Type: schema.TypeList, Optional: true, @@ -425,15 +466,16 @@ func resourceMonitorV2Create(ctx context.Context, data *schema.ResourceData, met return diags } - id, _ := oid.NewOID(data.Get("workspace").(string)) - result, err := client.CreateMonitorV2(ctx, id.Id, input) - if err != nil { - return diag.Errorf("failed to create monitor: %s", err.Error()) + actions, diags := newMonitorV2ActionAndRelationInputs(data) + if diags.HasError() { + return diags } - result, err = relateMonitorV2ToActions(ctx, result.Id, data, client) + wid, _ := oid.NewOID(data.Get("workspace").(string)) + + result, err := client.SaveMonitorV2WithActions(ctx, wid.Id, nil, input, actions) if err != nil { - return diags + return diag.Errorf("failed to create monitor: %s", err.Error()) } data.SetId(result.Id) @@ -448,7 +490,16 @@ func resourceMonitorV2Update(ctx context.Context, data *schema.ResourceData, met return diags } - _, err := client.UpdateMonitorV2(ctx, data.Id(), input) + actions, diags := newMonitorV2ActionAndRelationInputs(data) + if diags.HasError() { + return diags + } + + // TODO: do we require workspace here? + wid, _ := oid.NewOID(data.Get("workspace").(string)) + mid := data.Id() + + _, err := client.SaveMonitorV2WithActions(ctx, wid.Id, &mid, input, actions) if err != nil { if gql.HasErrorCode(err, "NOT_FOUND") { diags = resourceMonitorV2Create(ctx, data, meta) @@ -460,11 +511,6 @@ func resourceMonitorV2Update(ctx context.Context, data *schema.ResourceData, met return diag.Errorf("failed to update monitor: %s", err.Error()) } - _, err = relateMonitorV2ToActions(ctx, data.Id(), data, client) - if err != nil { - return diag.Errorf("failed to update monitor: %s", err.Error()) - } - return append(diags, resourceMonitorV2Read(ctx, data, meta)...) } @@ -524,6 +570,12 @@ func resourceMonitorV2Read(ctx context.Context, data *schema.ResourceData, meta } } + if monitor.Definition.MaxAlertsPerHour != nil { + if err := data.Set("max_alerts_per_hour", monitor.Definition.MaxAlertsPerHour); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + } + if monitor.Definition.Groupings != nil { if err := data.Set("groupings", monitorV2FlattenGroupings(monitor.Definition.Groupings)); err != nil { diags = append(diags, diag.FromErr(err)...) @@ -537,7 +589,7 @@ func resourceMonitorV2Read(ctx context.Context, data *schema.ResourceData, meta } if len(monitor.ActionRules) > 0 { - if err := data.Set("actions", monitorV2FlattenActionRules(monitor.ActionRules)); err != nil { + if err := data.Set("actions", monitorV2FlattenActionRules(ctx, client, monitor.ActionRules)); err != nil { diags = append(diags, diag.FromErr(err)...) } } @@ -577,20 +629,47 @@ func monitorV2FlattenRule(gqlRule gql.MonitorV2Rule) interface{} { return rule } -func monitorV2FlattenActionRules(gqlActionRules []gql.MonitorV2ActionRule) []interface{} { +func monitorV2FlattenActionRules(ctx context.Context, client *observe.Client, gqlActionRules []gql.MonitorV2ActionRule) []interface{} { var actionRules []interface{} for _, gqlActionRule := range gqlActionRules { - actionRules = append(actionRules, monitorV2FlattenActionRule(gqlActionRule)) + actionRules = append(actionRules, monitorV2FlattenActionRule(ctx, client, gqlActionRule)) } return actionRules } -func monitorV2FlattenActionRule(gqlActionRule gql.MonitorV2ActionRule) interface{} { - rules := map[string]interface{}{ - "oid": oid.MonitorV2ActionOid(gqlActionRule.ActionID).String(), +func monitorV2FlattenActionRule(ctx context.Context, client *observe.Client, gqlActionRule gql.MonitorV2ActionRule) interface{} { + rules := map[string]interface{}{} + + if gqlActionRule.Definition.Inline == nil || !*gqlActionRule.Definition.Inline { + // This is a shared action. It is assumed the monitor was created with the action id set + // and so that's what we return in the resource. + rules["oid"] = oid.MonitorV2ActionOid(gqlActionRule.ActionID).String() + } else { + actMap := map[string]any{} + rules["action"] = []any{actMap} + + // This is an inline/private action. We don't (yet?) get everything we want to populate in the + // response so we have to do a read to get this. + action, err := client.GetMonitorV2Action(ctx, oid.MonitorV2ActionOid(gqlActionRule.ActionID).String()) + if err != nil { + return rules + } + + actMap["type"] = toSnake(string(action.GetType())) + + if action.Email != nil { + actMap["email"] = monitorV2FlattenEmailAction(*action.Email) + } + if action.Webhook != nil { + actMap["webhook"] = monitorV2FlattenWebhookAction(*action.Webhook) + } + if action.Description != nil { + actMap["description"] = *action.Description + } } + if len(gqlActionRule.Levels) > 0 { - levels := make([]interface{}, 0) + levels := make([]interface{}, 0, len(gqlActionRule.Levels)) for _, level := range gqlActionRule.Levels { levels = append(levels, toSnake(string(level))) } @@ -770,6 +849,22 @@ func monitorV2FlattenTransformSchedule(gqlTransformSchedule gql.MonitorV2Transfo return []interface{}{transformSchedule} } +func newMonitorV2ActionAndRelationInputs(data *schema.ResourceData) (actions []gql.MonitorV2ActionAndRelationInput, diags diag.Diagnostics) { + inActions, ok := data.GetOk("actions") + if !ok { + return nil, diags + } + for i := range inActions.([]interface{}) { + actionRelation, err := newMonitorV2ActionAndRelation(fmt.Sprintf("actions.%d.", i), data) + if err != nil { + return nil, err + } + actions = append(actions, *actionRelation) + } + + return +} + func newMonitorV2Input(data *schema.ResourceData) (input *gql.MonitorV2Input, diags diag.Diagnostics) { // required definitionInput, diags := newMonitorV2DefinitionInput(data) @@ -831,6 +926,10 @@ func newMonitorV2DefinitionInput(data *schema.ResourceData) (defnInput *gql.Moni dataStabilizationDelay, _ := types.ParseDurationScalar(v.(string)) defnInput.DataStabilizationDelay = dataStabilizationDelay } + if v, ok := data.GetOk("max_alerts_per_hour"); ok { + defnInput.MaxAlertsPerHour = types.Int64Scalar(v.(int)).Ptr() + } + if _, ok := data.GetOk("groupings"); ok { groupings := make([]gql.MonitorV2ColumnInput, 0) for i := range data.Get("groupings").([]interface{}) { @@ -1178,53 +1277,42 @@ func newMonitorV2PrimitiveValue(path string, data *schema.ResourceData, ret *gql return nil } -func relateMonitorV2ToActions(ctx context.Context, monitorId string, data *schema.ResourceData, client *observe.Client) (*gql.MonitorV2, error) { - var actionRelations []gql.ActionRelationInput - if _, ok := data.GetOk("actions"); ok { - actionRelations = make([]gql.ActionRelationInput, 0) - for i := range data.Get("actions").([]interface{}) { - actionRule, err := newMonitorV2ActionRuleInput(fmt.Sprintf("actions.%d.", i), data) - if err != nil { - return nil, err - } - actionRelations = append(actionRelations, gql.ActionRelationInput{ - ActionRule: *actionRule, - }) +func newMonitorV2ActionAndRelation(path string, data *schema.ResourceData) (*gql.MonitorV2ActionAndRelationInput, diag.Diagnostics) { + var result gql.MonitorV2ActionAndRelationInput + + actionPath := fmt.Sprintf("%saction.0", path) + if _, ok := data.GetOk(actionPath); ok { + if actInput, err := newMonitorV2ActionInput(fmt.Sprintf("%s.", actionPath), data); err != nil { + return nil, err + } else { + // override default instantiation and force these to be private (inline) actions. + var inline = true + actInput.Inline = &inline + result.Action = actInput } - } - return client.SaveMonitorV2Relations(ctx, monitorId, actionRelations) -} - -func newMonitorV2ActionRuleInput(path string, data *schema.ResourceData) (*gql.MonitorV2ActionRuleInput, error) { - // required - actOID, err := oid.NewOID(data.Get(fmt.Sprintf("%soid", path)).(string)) - if err != nil { - return nil, err - } - - // instantiation - act := &gql.MonitorV2ActionRuleInput{ - ActionID: actOID.Id, + } else { + actOID, _ := oid.NewOID(data.Get(fmt.Sprintf("%soid", path)).(string)) + result.ActionID = &actOID.Id } // optional if _, ok := data.GetOk(fmt.Sprintf("%slevels", path)); ok { - act.Levels = make([]gql.MonitorV2AlarmLevel, 0) + result.Levels = make([]gql.MonitorV2AlarmLevel, 0) for i := range data.Get(fmt.Sprintf("%slevels", path)).([]interface{}) { - act.Levels = append(act.Levels, gql.MonitorV2AlarmLevel(toCamel(data.Get(fmt.Sprintf("%slevels.%d", path, i)).(string)))) + result.Levels = append(result.Levels, gql.MonitorV2AlarmLevel(toCamel(data.Get(fmt.Sprintf("%slevels.%d", path, i)).(string)))) } } if v, ok := data.GetOk(fmt.Sprintf("%ssend_end_notifications", path)); ok { boolVal := v.(bool) - act.SendEndNotifications = &boolVal + result.SendEndNotifications = &boolVal } if v, ok := data.GetOk(fmt.Sprintf("%ssend_reminders_interval", path)); ok { stringVal := v.(string) interval, _ := types.ParseDurationScalar(stringVal) - act.SendRemindersInterval = interval + result.SendRemindersInterval = interval } - return act, nil + return &result, nil } diff --git a/observe/resource_monitor_v2_action.go b/observe/resource_monitor_v2_action.go index 08a8ee7a..e04a7d47 100644 --- a/observe/resource_monitor_v2_action.go +++ b/observe/resource_monitor_v2_action.go @@ -150,7 +150,7 @@ func monitorV2WebhookHeaderInput() *schema.Resource { func resourceMonitorV2ActionCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { client := meta.(*observe.Client) - actInput, diags := newMonitorV2ActionInput(data) + actInput, diags := newMonitorV2ActionInput("", data) if diags.HasError() { return diags } @@ -168,7 +168,7 @@ func resourceMonitorV2ActionCreate(ctx context.Context, data *schema.ResourceDat func resourceMonitorV2ActionUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { client := meta.(*observe.Client) - actInput, diags := newMonitorV2ActionInput(data) + actInput, diags := newMonitorV2ActionInput("", data) if diags.HasError() { return diags } @@ -306,35 +306,37 @@ func monitorV2FlattenWebhookHeader(gqlHeader gql.MonitorV2WebhookHeader) interfa return header } -func newMonitorV2ActionInput(data *schema.ResourceData) (input *gql.MonitorV2ActionInput, diags diag.Diagnostics) { +func newMonitorV2ActionInput(path string, data *schema.ResourceData) (input *gql.MonitorV2ActionInput, diags diag.Diagnostics) { // required - actionType := toCamel(data.Get("type").(string)) - name := data.Get("name").(string) + actionType := toCamel(data.Get(fmt.Sprintf("%stype", path)).(string)) // instantiation - inlineVal := false + var inline = false // default behavior is that explicit action creation is for shared (non-inline) actions only input = &gql.MonitorV2ActionInput{ Type: meta.MonitorV2ActionType(actionType), - Name: name, - Inline: &inlineVal, // we are not currently allowing inline actions + Inline: &inline, + } + + if name, ok := data.GetOk(fmt.Sprintf("%sname", path)); ok { + input.Name = name.(string) } // optionals - if _, ok := data.GetOk("email"); ok { - email, diags := newMonitorV2EmailActionInput(data, "email.0.") + if _, ok := data.GetOk(fmt.Sprintf("%semail", path)); ok { + email, diags := newMonitorV2EmailActionInput(data, fmt.Sprintf("%semail.0.", path)) if diags.HasError() { return nil, diags } input.Email = email } if _, ok := data.GetOk("webhook"); ok { - webhook, diags := newMonitorV2WebhookActionInput(data, "webhook.0.") + webhook, diags := newMonitorV2WebhookActionInput(data, fmt.Sprintf("%swebhook.0.", path)) if diags.HasError() { return nil, diags } input.Webhook = webhook } - if v, ok := data.GetOk("description"); ok { + if v, ok := data.GetOk(fmt.Sprintf("%sdescription", path)); ok { description := v.(string) input.Description = &description } diff --git a/observe/resource_monitor_v2_test.go b/observe/resource_monitor_v2_test.go index e5851992..19268139 100644 --- a/observe/resource_monitor_v2_test.go +++ b/observe/resource_monitor_v2_test.go @@ -205,3 +205,190 @@ func TestAccObserveMonitorV2Promote(t *testing.T) { }, }) } + +func TestAccObserveMonitorV2MultipleActionsEmailViaOneShot(t *testing.T) { + randomPrefix := acctest.RandomWithPrefix("tf") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(monitorV2ConfigPreamble+` + resource "observe_monitor_v2" "first" { + workspace = data.observe_workspace.default.oid + rule_kind = "count" + name = "%[1]s" + lookback_time = "30m" + inputs = { + "test" = observe_datastream.test.dataset + } + stage { + pipeline = <<-EOF + colmake kind:"test", description:"test" + EOF + output_stage = true + } + stage { + pipeline = <<-EOF + filter kind ~ "test" + EOF + } + rules { + level = "informational" + count { + compare_values { + compare_fn = "greater" + value_int64 = [0] + } + } + } + scheduling { + transform { + freshness_goal = "15m" + } + } + max_alerts_per_hour = 99 + actions { + action { + type = "email" + email { + subject = "somebody once told me" + body = "the world is gonna roll me" + fragments = jsonencode({ + foo = "bar" + }) + addresses = ["test@observeinc.com"] + users = [data.observe_user.system.oid] + } + description = "an interesting description 1" + } + levels = ["informational"] + send_end_notifications = true + send_reminders_interval = "10m" + } + actions { + action { + type = "email" + email { + subject = "never gonna give you up" + body = "never gonna let you down" + fragments = jsonencode({ + fizz = "buzz" + }) + addresses = ["test@observeinc.com"] + users = [data.observe_user.system.oid] + } + description = "an interesting description 2" + } + levels = ["informational"] + send_end_notifications = false + send_reminders_interval = "20m" + } + } + + data "observe_user" "system" { + email = "%[2]s" + } + `, randomPrefix, systemUser()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("observe_monitor_v2.first", "workspace"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "name", randomPrefix), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "lookback_time", "30m0s"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "max_alerts_per_hour", "99"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.0.action.0.description", "an interesting description 1"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.0.action.0.type", "email"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.1.action.0.description", "an interesting description 2"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.0.action.0.type", "email"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.0.send_reminders_interval", "10m0s"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.1.send_reminders_interval", "20m0s"), + ), + }, + // now test update + { + Config: fmt.Sprintf(monitorV2ConfigPreamble+` + resource "observe_monitor_v2" "first" { + workspace = data.observe_workspace.default.oid + rule_kind = "count" + name = "%[1]s" + lookback_time = "15m" + inputs = { + "test" = observe_datastream.test.dataset + } + stage { + pipeline = <<-EOF + colmake kind:"test", description:"test" + EOF + output_stage = true + } + stage { + pipeline = <<-EOF + filter kind ~ "test" + EOF + } + rules { + level = "informational" + count { + compare_values { + compare_fn = "greater" + value_int64 = [0] + } + } + } + scheduling { + transform { + freshness_goal = "15m" + } + } + max_alerts_per_hour = 99 + actions { + action { + type = "email" + email { + subject = "somebody once told me" + body = "the world is gonna roll me" + fragments = jsonencode({ + foo = "bar" + }) + addresses = ["test@observeinc.com"] + users = [data.observe_user.system.oid] + } + description = "an interesting description 1" + } + levels = ["informational"] + send_end_notifications = true + send_reminders_interval = "11m" + } + actions { + action { + type = "email" + email { + subject = "never gonna give you up" + body = "never gonna let you down" + fragments = jsonencode({ + fizz = "buzz" + }) + addresses = ["test@observeinc.com"] + users = [data.observe_user.system.oid] + } + description = "an interesting description 2" + } + levels = ["informational"] + send_end_notifications = false + send_reminders_interval = "22m" + } + } + + data "observe_user" "system" { + email = "%[2]s" + } + `, randomPrefix, systemUser()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("observe_monitor_v2.first", "lookback_time", "15m0s"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.0.send_reminders_interval", "11m0s"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.1.send_reminders_interval", "22m0s"), + ), + }, + }, + }) +}