interferon 0.0.20 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +4 -3
- data/README.md +1 -1
- data/lib/interferon/alert.rb +0 -1
- data/lib/interferon/alert_dsl.rb +40 -15
- data/lib/interferon/destinations/datadog.rb +141 -76
- data/lib/interferon/version.rb +1 -1
- data/lib/interferon.rb +27 -8
- data/spec/lib/interferon/destinations/datadog_spec.rb +51 -36
- data/spec/lib/interferon_spec.rb +86 -33
- metadata +2 -4
- data/spec/helpers/dsl_helper.rb +0 -48
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
This repo contains the interferon gem.
|
6
6
|
This gem enables you to store your alerts configuration in code.
|
7
7
|
You should create your own repository, with a `Gemfile` which imports the interferon gem.
|
8
|
-
For an example of such a repository, along with example configuration and alerts files, see https://www.github.com/airbnb/
|
8
|
+
For an example of such a repository, along with example configuration and alerts files, see https://www.github.com/airbnb/alerts
|
9
9
|
|
10
10
|
## Running This Gem ##
|
11
11
|
|
data/lib/interferon/alert.rb
CHANGED
data/lib/interferon/alert_dsl.rb
CHANGED
@@ -19,12 +19,12 @@ module Interferon
|
|
19
19
|
if val.nil? && block.nil?
|
20
20
|
f = instance_variable_get(field)
|
21
21
|
f.nil? ? default : f
|
22
|
-
elsif val.nil?
|
23
|
-
instance_variable_set(field, block.call)
|
24
|
-
elsif block.nil?
|
25
|
-
instance_variable_set(field, val)
|
26
|
-
else
|
22
|
+
elsif !val.nil? && !block.nil?
|
27
23
|
raise ArgumentError, "You must pass either a value or a block but not both to #{field}"
|
24
|
+
else
|
25
|
+
f = val.nil? ? block.call : val
|
26
|
+
f = yield(f) if block_given?
|
27
|
+
instance_variable_set(field, f)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
@@ -33,19 +33,31 @@ module Interferon
|
|
33
33
|
include DSLMixin
|
34
34
|
|
35
35
|
def name(v = nil, &block)
|
36
|
-
get_or_set(:@name, v, block, '')
|
36
|
+
get_or_set(:@name, v, block, '') { |val| val.strip }
|
37
37
|
end
|
38
38
|
|
39
39
|
def message(v = nil, &block)
|
40
40
|
get_or_set(:@message, v, block, '')
|
41
41
|
end
|
42
42
|
|
43
|
-
def
|
44
|
-
get_or_set(:@
|
43
|
+
def monitor_type(v = nil, &block)
|
44
|
+
get_or_set(:@monitor_type, v, block, 'metric alert')
|
45
45
|
end
|
46
46
|
|
47
|
-
def
|
48
|
-
get_or_set(:@
|
47
|
+
def applies(v = nil, &block)
|
48
|
+
get_or_set(:@applies, v, block, false)
|
49
|
+
end
|
50
|
+
|
51
|
+
def silenced(v = nil, &block)
|
52
|
+
get_or_set(:@silenced, v, block, {}) do |val|
|
53
|
+
if val.is_a? Hash
|
54
|
+
val
|
55
|
+
elsif val == true
|
56
|
+
{ "*" => nil }
|
57
|
+
else
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
end
|
49
61
|
end
|
50
62
|
|
51
63
|
def is_work_hour?(args = {})
|
@@ -62,15 +74,24 @@ module Interferon
|
|
62
74
|
end
|
63
75
|
|
64
76
|
def no_data_timeframe(v = nil, &block)
|
65
|
-
get_or_set(:@no_data_timeframe, v, block,
|
77
|
+
get_or_set(:@no_data_timeframe, v, block, nil)
|
66
78
|
end
|
67
79
|
|
68
80
|
def timeout(v = nil, &block)
|
69
|
-
get_or_set(:@timeout, v, block,
|
81
|
+
get_or_set(:@timeout, v, block, nil)
|
70
82
|
end
|
71
83
|
|
72
|
-
def
|
73
|
-
|
84
|
+
def timeout_h
|
85
|
+
# timeout is in seconds, but set it to 1 hour at least
|
86
|
+
timeout ? [1, timeout.to_i / 3600].max : nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def thresholds(v = nil, &block)
|
90
|
+
get_or_set(:@thresholds, v, block, nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
def require_full_window(v = nil, &block)
|
94
|
+
get_or_set(:@require_full_window, v, block, nil)
|
74
95
|
end
|
75
96
|
|
76
97
|
def notify(v = nil)
|
@@ -92,13 +113,17 @@ module Interferon
|
|
92
113
|
def groups(v = nil, &block)
|
93
114
|
get_or_set(:@groups, v, block, [])
|
94
115
|
end
|
116
|
+
|
117
|
+
def audit(v = nil, &block)
|
118
|
+
get_or_set(:@audit, v, block, true)
|
119
|
+
end
|
95
120
|
end
|
96
121
|
|
97
122
|
class MetricDSL
|
98
123
|
include DSLMixin
|
99
124
|
|
100
125
|
def datadog_query(v = nil, &block)
|
101
|
-
get_or_set(:@datadog_query, v, block, '')
|
126
|
+
get_or_set(:@datadog_query, v, block, '') { |val| val.strip }
|
102
127
|
end
|
103
128
|
end
|
104
129
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'diffy'
|
2
2
|
require 'dogapi'
|
3
|
+
require 'parallel'
|
3
4
|
require 'set'
|
5
|
+
require 'thread'
|
4
6
|
|
5
7
|
Diffy::Diff.default_format = :text
|
6
8
|
|
@@ -37,8 +39,11 @@ module Interferon::Destinations
|
|
37
39
|
@existing_alerts = nil
|
38
40
|
@dry_run = options['dry_run']
|
39
41
|
|
40
|
-
#
|
42
|
+
# Datadog communication threads
|
41
43
|
@concurrency = options['concurrency'] || 10
|
44
|
+
# Fetch page size
|
45
|
+
@page_size = options['page_size'] || 1000
|
46
|
+
|
42
47
|
# configure retries
|
43
48
|
@retries = options['retries'] || 3
|
44
49
|
|
@@ -66,25 +71,39 @@ module Interferon::Destinations
|
|
66
71
|
end
|
67
72
|
|
68
73
|
def get_existing_alerts
|
69
|
-
|
74
|
+
alerts = Queue.new
|
75
|
+
has_more = true
|
76
|
+
|
77
|
+
Parallel.map_with_index(-> { has_more || Parallel::Stop },
|
78
|
+
in_threads: @concurrency) do |_, page|
|
79
|
+
successful = false
|
80
|
+
@retries.downto(0) do
|
81
|
+
resp = @dog.get_all_monitors(page: page, page_size: @page_size)
|
82
|
+
code = resp[0].to_i
|
83
|
+
if code != 200
|
84
|
+
log.info("Failed to retrieve existing alerts from datadog. #{code}: #{resp[1].inspect}")
|
85
|
+
else
|
86
|
+
alerts_page = resp[1]
|
87
|
+
if alerts_page.length < @page_size
|
88
|
+
has_more = false
|
89
|
+
end
|
90
|
+
alerts_page.map { |alert| alerts.push(alert) }
|
91
|
+
successful = true
|
92
|
+
break
|
93
|
+
end
|
94
|
+
end
|
70
95
|
|
71
|
-
|
72
|
-
|
73
|
-
|
96
|
+
if !successful
|
97
|
+
# Out of retries
|
98
|
+
raise "Retries exceeded for fetching data from datadog."
|
99
|
+
end
|
74
100
|
end
|
75
|
-
|
101
|
+
alerts.size.times.map { alerts.pop }
|
76
102
|
end
|
77
103
|
|
78
104
|
def existing_alerts
|
79
105
|
unless @existing_alerts
|
80
|
-
|
81
|
-
begin
|
82
|
-
alerts = get_existing_alerts
|
83
|
-
rescue
|
84
|
-
retries -= 1
|
85
|
-
retry if retries >= 0
|
86
|
-
raise
|
87
|
-
end
|
106
|
+
alerts = get_existing_alerts
|
88
107
|
|
89
108
|
# key alerts by name
|
90
109
|
@existing_alerts = {}
|
@@ -108,83 +127,43 @@ module Interferon::Destinations
|
|
108
127
|
]
|
109
128
|
end
|
110
129
|
|
111
|
-
|
130
|
+
@existing_alerts
|
112
131
|
end
|
113
132
|
|
114
133
|
def create_alert(alert, people)
|
115
134
|
# create a message which includes the notifications
|
135
|
+
# Datadog may have a race condition where alerts created in a bad state may be triggered
|
136
|
+
# during the dry-run creation process. Delete people from dry-run alerts to avoid this
|
116
137
|
message = generate_message(alert['message'], people)
|
117
138
|
|
118
139
|
# create the hash of options to send to datadog
|
119
|
-
|
120
|
-
:
|
121
|
-
:message => message,
|
122
|
-
:silenced => false,
|
140
|
+
alert_options = {
|
141
|
+
:notify_audit => alert['notify']['audit'],
|
123
142
|
:notify_no_data => alert['notify_no_data'],
|
124
|
-
:
|
143
|
+
:no_data_timeframe => alert['no_data_timeframe'],
|
144
|
+
:silenced => alert['silenced'],
|
145
|
+
:timeout_h => alert['timeout_h'],
|
125
146
|
}
|
126
147
|
|
127
|
-
if
|
128
|
-
|
129
|
-
# during the dry-run creation process. Delete people from dry-run alerts to avoid this
|
130
|
-
alert_opts[:message] = generate_message(alert['message'], [])
|
148
|
+
if !alert['require_full_window'].nil?
|
149
|
+
alert_options[:require_full_window] = alert['require_full_window']
|
131
150
|
end
|
132
151
|
|
133
|
-
|
134
|
-
|
135
|
-
alert_opts[:silenced] = true
|
152
|
+
if !alert['thresholds'].nil?
|
153
|
+
alert_options[:thresholds] = alert['thresholds']
|
136
154
|
end
|
137
155
|
|
138
|
-
|
139
|
-
# (this feature is supported, even though it's not documented)
|
140
|
-
alert_opts[:no_data_timeframe] = alert['no_data_timeframe'] if alert['no_data_timeframe']
|
141
|
-
|
142
|
-
# timeout is in seconds, but set it to 1 hour at least
|
143
|
-
alert_opts[:timeout_h] = [1, (alert['timeout'].to_i / 3600)].max if alert['timeout']
|
144
|
-
|
145
|
-
datadog_query = alert['metric']['datadog_query'].strip
|
156
|
+
datadog_query = alert['metric']['datadog_query']
|
146
157
|
existing_alert = existing_alerts[alert['name']]
|
147
158
|
|
148
159
|
# new alert, create it
|
149
160
|
if existing_alert.nil?
|
150
161
|
action = :creating
|
151
|
-
|
152
|
-
new_alert_text = "Query: #{datadog_query} Message: #{message.split().join(' ')}"
|
153
|
-
log.info("creating new alert #{alert['name']}: #{new_alert_text}")
|
154
|
-
|
155
|
-
resp = @dog.alert(
|
156
|
-
alert['metric']['datadog_query'].strip,
|
157
|
-
alert_opts,
|
158
|
-
)
|
159
|
-
|
160
|
-
# existing alert, modify it
|
162
|
+
resp = create_datadog_alert(alert, datadog_query, message, alert_options)
|
161
163
|
else
|
164
|
+
# existing alert, modify it
|
162
165
|
action = :updating
|
163
|
-
|
164
|
-
id = existing_alert['id'][0]
|
165
|
-
|
166
|
-
new_alert_text = "Query:\n#{datadog_query}\nMessage:\n#{message}"
|
167
|
-
existing_alert_text = "Query:\n#{existing_alert['query']}\nMessage:\n#{existing_alert['message']}\n"
|
168
|
-
diff = Diffy::Diff.new(existing_alert_text, new_alert_text, :context=>1)
|
169
|
-
log.info("updating existing alert #{id} (#{alert['name']}): #{diff}")
|
170
|
-
|
171
|
-
if @dry_run
|
172
|
-
resp = @dog.alert(
|
173
|
-
alert['metric']['datadog_query'].strip,
|
174
|
-
alert_opts,
|
175
|
-
)
|
176
|
-
else
|
177
|
-
resp = @dog.update_alert(
|
178
|
-
id,
|
179
|
-
alert['metric']['datadog_query'].strip,
|
180
|
-
alert_opts
|
181
|
-
)
|
182
|
-
# Unmute existing alerts that have been unsilenced.
|
183
|
-
# Datadog does not allow updates to silencing via the update_alert API call.
|
184
|
-
if existing_alert['silenced'] && !alert_opts[:silenced]
|
185
|
-
@dog.unmute_monitor(id)
|
186
|
-
end
|
187
|
-
end
|
166
|
+
resp = update_datadog_alert(alert, datadog_query, message, alert_options, existing_alert)
|
188
167
|
end
|
189
168
|
|
190
169
|
# log whenever we've encountered errors
|
@@ -196,14 +175,100 @@ module Interferon::Destinations
|
|
196
175
|
# assume this was a success
|
197
176
|
@stats[:alerts_created] += 1 if action == :creating
|
198
177
|
@stats[:alerts_updated] += 1 if action == :updating
|
199
|
-
@stats[:alerts_silenced] += 1 if
|
178
|
+
@stats[:alerts_silenced] += 1 if !alert_options[:silenced].empty?
|
200
179
|
end
|
201
180
|
|
202
181
|
id = resp[1].nil? ? nil : [resp[1]['id']]
|
203
182
|
# lets key alerts by their name
|
204
|
-
|
183
|
+
[alert['name'], id]
|
184
|
+
end
|
185
|
+
|
186
|
+
def create_datadog_alert(alert, datadog_query, message, alert_options)
|
187
|
+
@stats[:alerts_to_be_created] += 1
|
188
|
+
new_alert_text = <<-EOM
|
189
|
+
Query:
|
190
|
+
#{datadog_query}
|
191
|
+
Message:
|
192
|
+
#{message}
|
193
|
+
Options:
|
194
|
+
#{alert_options}
|
195
|
+
EOM
|
196
|
+
log.info("creating new alert #{alert['name']}: #{new_alert_text}")
|
197
|
+
|
198
|
+
@dog.monitor(
|
199
|
+
alert['monitor_type'],
|
200
|
+
datadog_query,
|
201
|
+
:name => alert['name'],
|
202
|
+
:message => @dry_run ? generate_message(alert, []) : message,
|
203
|
+
:options => alert_options,
|
204
|
+
)
|
205
|
+
end
|
206
|
+
|
207
|
+
def update_datadog_alert(alert, datadog_query, message, alert_options, existing_alert)
|
208
|
+
@stats[:alerts_to_be_updated] += 1
|
209
|
+
id = existing_alert['id'][0]
|
210
|
+
|
211
|
+
new_alert_text = <<-EOM.strip
|
212
|
+
Query:
|
213
|
+
#{datadog_query.strip}
|
214
|
+
Message:
|
215
|
+
#{message.strip}
|
216
|
+
Options:
|
217
|
+
#{alert_options}
|
218
|
+
EOM
|
219
|
+
existing_alert_text = <<-EOM.strip
|
220
|
+
Query:
|
221
|
+
#{existing_alert['query'].strip}
|
222
|
+
Message:
|
223
|
+
#{existing_alert['message'].strip}
|
224
|
+
Options:
|
225
|
+
#{alert_options}
|
226
|
+
EOM
|
227
|
+
diff = Diffy::Diff.new(existing_alert_text, new_alert_text, :context=>1)
|
228
|
+
log.info("updating existing alert #{id} (#{alert['name']}):\n#{diff}")
|
229
|
+
|
230
|
+
if @dry_run
|
231
|
+
resp = @dog.monitor(
|
232
|
+
alert['monitor_type'],
|
233
|
+
datadog_query,
|
234
|
+
:name => alert['name'],
|
235
|
+
:message => generate_message(alert, []),
|
236
|
+
:options => alert_options,
|
237
|
+
)
|
238
|
+
else
|
239
|
+
if alert['monitor_type'] == existing_alert['type']
|
240
|
+
resp = @dog.update_monitor(
|
241
|
+
id,
|
242
|
+
datadog_query,
|
243
|
+
:name => alert['name'],
|
244
|
+
:message => message,
|
245
|
+
:options => alert_options,
|
246
|
+
)
|
247
|
+
|
248
|
+
# Unmute existing alerts that have been unsilenced.
|
249
|
+
# Datadog does not allow updates to silencing via the update_alert API call.
|
250
|
+
if !existing_alert['options']['silenced'].empty? && alert_options[:silenced].empty?
|
251
|
+
@dog.unmute_monitor(id)
|
252
|
+
end
|
253
|
+
else
|
254
|
+
# Need to recreate alert with new monitor type
|
255
|
+
resp = @dog.delete_monitor(id)
|
256
|
+
code = resp[0].to_i
|
257
|
+
if !(code >= 300 || code == -1)
|
258
|
+
resp = @dog.monitor(
|
259
|
+
alert['monitor_type'],
|
260
|
+
datadog_query,
|
261
|
+
:name => alert['name'],
|
262
|
+
:message => message,
|
263
|
+
:options => alert_options,
|
264
|
+
)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
resp
|
205
269
|
end
|
206
270
|
|
271
|
+
|
207
272
|
def remove_alert(alert)
|
208
273
|
if alert['message'].include?(ALERT_KEY)
|
209
274
|
@stats[:alerts_to_be_deleted] += 1
|
@@ -211,7 +276,7 @@ module Interferon::Destinations
|
|
211
276
|
|
212
277
|
if !@dry_run
|
213
278
|
alert['id'].each do |alert_id|
|
214
|
-
resp = @dog.
|
279
|
+
resp = @dog.delete_monitor(alert_id)
|
215
280
|
code = resp[0].to_i
|
216
281
|
log_datadog_response_code(resp, code, :deleting)
|
217
282
|
|
@@ -244,7 +309,7 @@ module Interferon::Destinations
|
|
244
309
|
def remove_alert_by_id(alert_id)
|
245
310
|
# This should only be used by dry-run to clean up created dry-run alerts
|
246
311
|
log.debug("deleting alert, id: #{alert_id}")
|
247
|
-
resp = @dog.
|
312
|
+
resp = @dog.delete_monitor(alert_id)
|
248
313
|
code = resp[0].to_i
|
249
314
|
log_datadog_response_code(resp, code, :deleting)
|
250
315
|
end
|
@@ -263,7 +328,7 @@ module Interferon::Destinations
|
|
263
328
|
statsd.gauge('datadog.api.client_error', 1, :tags => ["alert:#{alert}"])
|
264
329
|
statsd.gauge('datadog.api.success', 0, :tags => ["alert:#{alert}"])
|
265
330
|
log.error("client error while #{action} alert '#{alert['name']}';" \
|
266
|
-
" query was '#{alert['metric']['datadog_query']
|
331
|
+
" query was '#{alert['metric']['datadog_query']}'" \
|
267
332
|
" response was #{resp[0]}:'#{resp[1].inspect}'")
|
268
333
|
end
|
269
334
|
|
@@ -275,7 +340,7 @@ module Interferon::Destinations
|
|
275
340
|
statsd.gauge('datadog.api.client_error', 0, :tags => ["alert:#{alert}"])
|
276
341
|
statsd.gauge('datadog.api.success', 0, :tags => ["alert:#{alert}"])
|
277
342
|
log.error("unknown error while #{action} alert '#{alert['name']}':" \
|
278
|
-
" query was '#{alert['metric']['datadog_query']
|
343
|
+
" query was '#{alert['metric']['datadog_query']}'" \
|
279
344
|
" response was #{resp[0]}:'#{resp[1].inspect}'")
|
280
345
|
end
|
281
346
|
else
|
data/lib/interferon/version.rb
CHANGED
data/lib/interferon.rb
CHANGED
@@ -391,27 +391,46 @@ module Interferon
|
|
391
391
|
end
|
392
392
|
end
|
393
393
|
|
394
|
+
def self.normalize_monitor_type(monitor_type)
|
395
|
+
# Convert 'query alert' type to 'metric alert' type. They can used interchangeably when
|
396
|
+
# submitting monitors to Datadog. Datadog will automatically do the conversion to 'query
|
397
|
+
# alert' for a "complex" query that includes multiple metrics/tags while using 'metric alert'
|
398
|
+
# for monitors that include a single scope/metric.
|
399
|
+
monitor_type == 'query alert' ? 'metric alert' : monitor_type
|
400
|
+
end
|
401
|
+
|
394
402
|
def self.same_alerts(dest, alert_people_pair, alert_api_json)
|
395
403
|
alert, people = alert_people_pair
|
396
404
|
|
397
405
|
prev_alert = {
|
406
|
+
:monitor_type => normalize_monitor_type(alert_api_json['type']),
|
398
407
|
:query => alert_api_json['query'].strip,
|
399
408
|
:message => alert_api_json['message'].strip,
|
400
|
-
:notify_no_data => alert_api_json['notify_no_data'],
|
401
|
-
:
|
402
|
-
:
|
403
|
-
:
|
409
|
+
:notify_no_data => alert_api_json['options']['notify_no_data'],
|
410
|
+
:notify_audit => alert_api_json['options']['notify_audit'],
|
411
|
+
:no_data_timeframe => alert_api_json['options']['no_data_timeframe'],
|
412
|
+
:silenced => alert_api_json['options']['silenced'],
|
413
|
+
:thresholds => alert_api_json['options']['thresholds'],
|
414
|
+
:timeout_h => alert_api_json['options']['timeout_h'],
|
404
415
|
}
|
405
416
|
|
406
417
|
new_alert = {
|
407
|
-
:
|
418
|
+
:monitor_type => normalize_monitor_type(alert['monitor_type']),
|
419
|
+
:query => alert['metric']['datadog_query'],
|
408
420
|
:message => dest.generate_message(alert['message'], people).strip,
|
409
421
|
:notify_no_data => alert['notify_no_data'],
|
410
|
-
:
|
411
|
-
:
|
412
|
-
:
|
422
|
+
:notify_audit => alert['notify']['audit'],
|
423
|
+
:no_data_timeframe => alert['no_data_timeframe'],
|
424
|
+
:silenced => alert['silenced'],
|
425
|
+
:thresholds => alert['thresholds'],
|
426
|
+
:timeout_h => alert['timeout_h']
|
413
427
|
}
|
414
428
|
|
429
|
+
if !alert['require_full_window'].nil?
|
430
|
+
pre_alert[:require_full_window] = alert_api_json['options']['require_full_window']
|
431
|
+
new_alert[:require_full_window] = alert['require_full_window']
|
432
|
+
end
|
433
|
+
|
415
434
|
prev_alert == new_alert
|
416
435
|
end
|
417
436
|
|
@@ -25,85 +25,100 @@ describe Interferon::Destinations::Datadog do
|
|
25
25
|
'name' => 'Test Alert',
|
26
26
|
'message' => "Test Message",
|
27
27
|
'metric' => { 'datadog_query' => 'avg:metric{*}' },
|
28
|
-
'silenced' =>
|
29
|
-
'
|
28
|
+
'silenced' => {},
|
29
|
+
'notify' => {},
|
30
30
|
}
|
31
31
|
}
|
32
32
|
let(:mock_people) { ['foo', 'bar', 'baz'] }
|
33
|
+
let(:mock_response) {
|
34
|
+
{
|
35
|
+
"Test Alert" => {
|
36
|
+
"id" => 567,
|
37
|
+
"name" => 'Test Alert',
|
38
|
+
'message' => "Test Message",
|
39
|
+
"query" => 'avg:metric{*}',
|
40
|
+
"options" => {
|
41
|
+
"silenced" => {}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
33
46
|
|
34
47
|
describe ".get_existing_alerts" do
|
35
|
-
it "calls dogapi
|
36
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
48
|
+
it "calls dogapi get_all_monitors" do
|
49
|
+
expect_any_instance_of(Dogapi::Client).to receive(:get_all_monitors).and_return([200, []])
|
37
50
|
datadog.get_existing_alerts
|
38
51
|
end
|
39
52
|
end
|
40
53
|
|
41
54
|
describe ".existing_alerts" do
|
42
|
-
it "retries dogapi
|
55
|
+
it "retries dogapi get_all_monitors" do
|
43
56
|
return_vals = [[400, ""]] * (retries + 1)
|
44
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
57
|
+
expect_any_instance_of(Dogapi::Client).to receive(:get_all_monitors).and_return(*return_vals)
|
45
58
|
expect { datadog.existing_alerts }.to raise_error RuntimeError
|
46
59
|
end
|
47
60
|
end
|
48
61
|
|
49
62
|
describe ".create_alert" do
|
50
|
-
it "calls dogapi
|
51
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
63
|
+
it "calls dogapi monitor" do
|
64
|
+
expect_any_instance_of(Dogapi::Client).to receive(:monitor).and_return([200, ""])
|
52
65
|
expect(datadog).to receive(:existing_alerts).and_return({})
|
53
66
|
datadog.create_alert(mock_alert, mock_people)
|
54
67
|
end
|
55
68
|
|
56
|
-
it "calls dogapi
|
57
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
58
|
-
expect(datadog).to receive(:existing_alerts).and_return(
|
59
|
-
{
|
60
|
-
"Test Alert" => {
|
61
|
-
"id" => 567,
|
62
|
-
"name" => 'Test Alert',
|
63
|
-
}
|
64
|
-
}
|
65
|
-
)
|
69
|
+
it "calls dogapi update_monitor when alert name is found" do
|
70
|
+
expect_any_instance_of(Dogapi::Client).to receive(:update_monitor).and_return([200, ""])
|
71
|
+
expect(datadog).to receive(:existing_alerts).and_return(mock_response)
|
66
72
|
datadog.create_alert(mock_alert, mock_people)
|
67
73
|
end
|
68
74
|
|
69
|
-
it "
|
70
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
)
|
75
|
+
it "calls dogapi to delete and recreate when alert name is found" do
|
76
|
+
expect_any_instance_of(Dogapi::Client).to receive(:delete_monitor).and_return([200, ""])
|
77
|
+
expect_any_instance_of(Dogapi::Client).to receive(:monitor).and_return([200, ""])
|
78
|
+
mock_response["Test Alert"]["type"] = "event_alert"
|
79
|
+
expect(datadog).to receive(:existing_alerts).and_return(mock_response)
|
80
|
+
datadog.create_alert(mock_alert, mock_people)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "calls dogapi to unmute when exiting alert is muted" do
|
84
|
+
expect_any_instance_of(Dogapi::Client).to receive(:update_monitor).and_return([200, ""])
|
85
|
+
expect_any_instance_of(Dogapi::Client).to receive(:unmute_monitor).and_return([200, ""])
|
86
|
+
mock_response["Test Alert"]["options"]["silenced"] = { '*' => nil }
|
87
|
+
expect(datadog).to receive(:existing_alerts).and_return(mock_response)
|
88
|
+
datadog.create_alert(mock_alert, mock_people)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "always calls monitor in dry-run" do
|
92
|
+
expect_any_instance_of(Dogapi::Client).to receive(:monitor).and_return([200, ""])
|
93
|
+
expect(datadog_dry_run).to receive(:existing_alerts).and_return(mock_response)
|
79
94
|
datadog_dry_run.create_alert(mock_alert, mock_people)
|
80
95
|
end
|
81
96
|
end
|
82
97
|
|
83
98
|
describe ".remove_alert" do
|
84
|
-
it "calls dogapi
|
99
|
+
it "calls dogapi delete_monitor with the correct alert id" do
|
85
100
|
mock_alert["message"] += Interferon::Destinations::Datadog::ALERT_KEY
|
86
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
101
|
+
expect_any_instance_of(Dogapi::Client).to receive(:delete_monitor).
|
87
102
|
with(mock_alert_id).and_return([200, ""])
|
88
103
|
datadog.remove_alert(mock_alert)
|
89
104
|
end
|
90
105
|
|
91
|
-
it "does not call dogapi
|
106
|
+
it "does not call dogapi delete_monitor in dry_run" do
|
92
107
|
mock_alert["message"] += Interferon::Destinations::Datadog::ALERT_KEY
|
93
|
-
expect_any_instance_of(Dogapi::Client).to_not receive(:
|
108
|
+
expect_any_instance_of(Dogapi::Client).to_not receive(:delete_monitor)
|
94
109
|
datadog_dry_run.remove_alert(mock_alert)
|
95
110
|
end
|
96
111
|
|
97
|
-
it "does not call dogapi
|
98
|
-
expect_any_instance_of(Dogapi::Client).to_not receive(:
|
112
|
+
it "does not call dogapi delete_monitor when ALERT_KEY is missing" do
|
113
|
+
expect_any_instance_of(Dogapi::Client).to_not receive(:delete_monitor)
|
99
114
|
datadog.remove_alert(mock_alert)
|
100
115
|
end
|
101
116
|
|
102
117
|
end
|
103
118
|
|
104
119
|
describe ".remove_alert_by_id" do
|
105
|
-
it "calls dogapi
|
106
|
-
expect_any_instance_of(Dogapi::Client).to receive(:
|
120
|
+
it "calls dogapi delete_monitor" do
|
121
|
+
expect_any_instance_of(Dogapi::Client).to receive(:delete_monitor).
|
107
122
|
with(mock_alert_id).and_return([200, ""])
|
108
123
|
datadog.remove_alert_by_id(mock_alert_id)
|
109
124
|
end
|
data/spec/lib/interferon_spec.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'helpers/mock_alert'
|
3
|
-
require 'helpers/dsl_helper'
|
4
3
|
require 'interferon/destinations/datadog'
|
5
4
|
|
6
5
|
include Interferon
|
@@ -26,22 +25,33 @@ describe Interferon::Interferon do
|
|
26
25
|
end
|
27
26
|
|
28
27
|
it "detects a change if alert notify_no_data is different" do
|
29
|
-
alert1 = create_test_alert('name1', 'testquery1', 'message1', false)
|
30
|
-
alert2 = mock_alert_json(
|
28
|
+
alert1 = create_test_alert('name1', 'testquery1', 'message1', { :notify_no_data => false })
|
29
|
+
alert2 = mock_alert_json(
|
30
|
+
'name2',
|
31
|
+
'testquery2',
|
32
|
+
'message2',
|
33
|
+
nil,
|
34
|
+
[1],
|
35
|
+
{ :notify_no_data => true }
|
36
|
+
)
|
31
37
|
|
32
38
|
expect(Interferon::Interferon.same_alerts(dest, [alert1, []], alert2)).to be false
|
33
39
|
end
|
34
40
|
|
35
41
|
it "detects a change if alert silenced is different" do
|
36
|
-
alert1 = create_test_alert('name1', 'testquery1', 'message1',
|
37
|
-
alert2 = mock_alert_json('name2', 'testquery2', 'message2',
|
42
|
+
alert1 = create_test_alert('name1', 'testquery1', 'message1', { :silenced => true })
|
43
|
+
alert2 = mock_alert_json('name2', 'testquery2', 'message2', nil, [1], { :silenced => {} })
|
38
44
|
|
39
45
|
expect(Interferon::Interferon.same_alerts(dest, [alert1, []], alert2)).to be false
|
40
46
|
end
|
41
47
|
|
42
48
|
it "does not detect a change when alert datadog query and message are the same" do
|
43
49
|
alert1 = create_test_alert('name1', 'testquery1', 'message1')
|
44
|
-
alert2 = mock_alert_json(
|
50
|
+
alert2 = mock_alert_json(
|
51
|
+
'name1',
|
52
|
+
'testquery1',
|
53
|
+
"message1\nThis alert was created via the alerts framework"
|
54
|
+
)
|
45
55
|
|
46
56
|
expect(Interferon::Interferon.same_alerts(dest, [alert1, []], alert2)).to be true
|
47
57
|
end
|
@@ -62,7 +72,12 @@ describe Interferon::Interferon do
|
|
62
72
|
expect(dest).not_to receive(:create_alert)
|
63
73
|
expect(dest).not_to receive(:remove_alert_by_id)
|
64
74
|
|
65
|
-
interferon.update_alerts_on_destination(
|
75
|
+
interferon.update_alerts_on_destination(
|
76
|
+
dest,
|
77
|
+
['host'],
|
78
|
+
[alerts['name1'], alerts['name2']],
|
79
|
+
{}
|
80
|
+
)
|
66
81
|
end
|
67
82
|
|
68
83
|
it 'runs added alerts' do
|
@@ -71,7 +86,12 @@ describe Interferon::Interferon do
|
|
71
86
|
expect(dest).to receive(:create_alert).once.and_call_original
|
72
87
|
expect(dest).to receive(:remove_alert_by_id).with('3').once
|
73
88
|
|
74
|
-
interferon.update_alerts_on_destination(
|
89
|
+
interferon.update_alerts_on_destination(
|
90
|
+
dest,
|
91
|
+
['host'],
|
92
|
+
[alerts['name1'], alerts['name2'], added],
|
93
|
+
{}
|
94
|
+
)
|
75
95
|
end
|
76
96
|
|
77
97
|
it 'runs updated alerts' do
|
@@ -89,7 +109,7 @@ describe Interferon::Interferon do
|
|
89
109
|
end
|
90
110
|
|
91
111
|
it 'deletes duplicate old alerts' do
|
92
|
-
alert1 = mock_alert_json('name1', 'testquery1', '',
|
112
|
+
alert1 = mock_alert_json('name1', 'testquery1', '', nil, [1, 2, 3])
|
93
113
|
alert2 = mock_alert_json('name2', 'testquery2', '')
|
94
114
|
existing_alerts = {'name1' => alert1, 'name2' => alert2}
|
95
115
|
dest = MockDest.new(existing_alerts)
|
@@ -104,7 +124,7 @@ describe Interferon::Interferon do
|
|
104
124
|
end
|
105
125
|
|
106
126
|
it 'deletes duplicate old alerts when creating new alert' do
|
107
|
-
alert1 = mock_alert_json('name1', 'testquery1', '',
|
127
|
+
alert1 = mock_alert_json('name1', 'testquery1', '', nil, [1, 2, 3])
|
108
128
|
alert2 = mock_alert_json('name2', 'testquery2', '')
|
109
129
|
existing_alerts = {'name1' => alert1, 'name2' => alert2}
|
110
130
|
dest = MockDest.new(existing_alerts)
|
@@ -116,7 +136,7 @@ describe Interferon::Interferon do
|
|
116
136
|
|
117
137
|
# Since we change id to nil we will not be attempting to delete duplicate alerts
|
118
138
|
# during dry run
|
119
|
-
|
139
|
+
expect(dest).to_not receive(:remove_alert).with(existing_alerts['name1'])
|
120
140
|
expect(dest).to receive(:remove_alert).with(existing_alerts['name2'])
|
121
141
|
|
122
142
|
interferon.update_alerts_on_destination(dest, ['host'], [added], {})
|
@@ -147,7 +167,12 @@ describe Interferon::Interferon do
|
|
147
167
|
expect(dest).to receive(:create_alert).once.and_call_original
|
148
168
|
expect(dest).not_to receive(:remove_alert_by_id).with('3')
|
149
169
|
|
150
|
-
interferon.update_alerts_on_destination(
|
170
|
+
interferon.update_alerts_on_destination(
|
171
|
+
dest,
|
172
|
+
['host'],
|
173
|
+
[alerts['name1'], alerts['name2'], added],
|
174
|
+
{}
|
175
|
+
)
|
151
176
|
end
|
152
177
|
|
153
178
|
it 'runs updated alerts' do
|
@@ -167,7 +192,7 @@ describe Interferon::Interferon do
|
|
167
192
|
end
|
168
193
|
|
169
194
|
it 'deletes duplicate old alerts' do
|
170
|
-
alert1 = mock_alert_json('name1', 'testquery1', '',
|
195
|
+
alert1 = mock_alert_json('name1', 'testquery1', '', nil, [1, 2, 3])
|
171
196
|
alert2 = mock_alert_json('name2', 'testquery2', '')
|
172
197
|
existing_alerts = {'name1' => alert1, 'name2' => alert2}
|
173
198
|
dest = MockDest.new(existing_alerts)
|
@@ -182,7 +207,7 @@ describe Interferon::Interferon do
|
|
182
207
|
end
|
183
208
|
|
184
209
|
it 'deletes duplicate old alerts when creating new alert' do
|
185
|
-
alert1 = mock_alert_json('name1', 'testquery1', '',
|
210
|
+
alert1 = mock_alert_json('name1', 'testquery1', '', nil, [1, 2, 3])
|
186
211
|
alert2 = mock_alert_json('name2', 'testquery2', '')
|
187
212
|
existing_alerts = {'name1' => alert1, 'name2' => alert2}
|
188
213
|
dest = MockDest.new(existing_alerts)
|
@@ -190,7 +215,13 @@ describe Interferon::Interferon do
|
|
190
215
|
|
191
216
|
added = create_test_alert('name1', 'testquery1', '')
|
192
217
|
|
193
|
-
expect(dest).to receive(:remove_alert).with(mock_alert_json(
|
218
|
+
expect(dest).to receive(:remove_alert).with(mock_alert_json(
|
219
|
+
'name1',
|
220
|
+
'testquery1',
|
221
|
+
'',
|
222
|
+
nil,
|
223
|
+
[2, 3]
|
224
|
+
))
|
194
225
|
expect(dest).to receive(:remove_alert).with(existing_alerts['name2'])
|
195
226
|
|
196
227
|
interferon.update_alerts_on_destination(dest, ['host'], [added], {})
|
@@ -204,8 +235,6 @@ describe Interferon::Interferon do
|
|
204
235
|
end
|
205
236
|
|
206
237
|
class MockDest < Interferon::Destinations::Datadog
|
207
|
-
@existing_alerts
|
208
|
-
|
209
238
|
def initialize(the_existing_alerts)
|
210
239
|
@existing_alerts = the_existing_alerts
|
211
240
|
end
|
@@ -221,29 +250,53 @@ describe Interferon::Interferon do
|
|
221
250
|
end
|
222
251
|
end
|
223
252
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
253
|
+
DEFAULT_OPTIONS = {
|
254
|
+
'notify_audit' => true,
|
255
|
+
'notify_no_data' => false,
|
256
|
+
'silenced' => {},
|
257
|
+
'thresholds' => nil,
|
258
|
+
'no_data_timeframe' => nil,
|
259
|
+
'require_full_window' => nil,
|
260
|
+
'timeout' => nil,
|
261
|
+
}
|
262
|
+
|
263
|
+
def mock_alert_json(name, datadog_query, message, type="metric alert", id=nil, options={})
|
264
|
+
options = DEFAULT_OPTIONS.merge(options)
|
265
|
+
|
266
|
+
{
|
267
|
+
'name'=> name,
|
268
|
+
'query' => datadog_query,
|
269
|
+
'type' => type,
|
270
|
+
'message' => message,
|
271
|
+
'id' => id.nil? ? [name[-1]] : id,
|
272
|
+
'options' => options,
|
231
273
|
}
|
232
274
|
end
|
233
275
|
|
234
|
-
def create_test_alert(name, datadog_query, message,
|
235
|
-
|
236
|
-
|
276
|
+
def create_test_alert(name, datadog_query, message, options={})
|
277
|
+
options = DEFAULT_OPTIONS.merge(options)
|
278
|
+
|
279
|
+
alert_dsl = AlertDSL.new({})
|
280
|
+
|
281
|
+
metric_dsl = MetricDSL.new({})
|
237
282
|
metric_dsl.datadog_query(datadog_query)
|
238
|
-
alert_dsl.metric
|
283
|
+
alert_dsl.instance_variable_set(:@metric, metric_dsl)
|
284
|
+
|
285
|
+
notify_dsl = NotifyDSL.new({})
|
286
|
+
notify_dsl.groups(['a'])
|
287
|
+
alert_dsl.instance_variable_set(:@notify, notify_dsl)
|
288
|
+
|
239
289
|
alert_dsl.name(name)
|
240
290
|
alert_dsl.applies(true)
|
241
291
|
alert_dsl.message(message)
|
242
|
-
|
243
|
-
alert_dsl.
|
244
|
-
|
245
|
-
|
246
|
-
alert_dsl.
|
292
|
+
|
293
|
+
alert_dsl.no_data_timeframe(options['no_data_timeframe'])
|
294
|
+
alert_dsl.notify_no_data(options['notify_no_data'])
|
295
|
+
alert_dsl.require_full_window(options['require_full_window'])
|
296
|
+
alert_dsl.thresholds(options['thresholds'])
|
297
|
+
alert_dsl.timeout(options['timeout'])
|
298
|
+
alert_dsl.silenced(options['silenced'])
|
299
|
+
|
247
300
|
MockAlert.new(alert_dsl)
|
248
301
|
end
|
249
302
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: interferon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2017-04-
|
13
|
+
date: 2017-04-27 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: dogapi
|
@@ -231,7 +231,6 @@ files:
|
|
231
231
|
- spec/fixtures/loaders2/test_sources/order_test_source.rb
|
232
232
|
- spec/fixtures/loaders2/test_sources/secondary_source.rb
|
233
233
|
- spec/fixtures/loaders2/test_sources/test_source.rb
|
234
|
-
- spec/helpers/dsl_helper.rb
|
235
234
|
- spec/helpers/loader_helper.rb
|
236
235
|
- spec/helpers/logging_helper.rb
|
237
236
|
- spec/helpers/mock_alert.rb
|
@@ -276,7 +275,6 @@ test_files:
|
|
276
275
|
- spec/fixtures/loaders2/test_sources/order_test_source.rb
|
277
276
|
- spec/fixtures/loaders2/test_sources/secondary_source.rb
|
278
277
|
- spec/fixtures/loaders2/test_sources/test_source.rb
|
279
|
-
- spec/helpers/dsl_helper.rb
|
280
278
|
- spec/helpers/loader_helper.rb
|
281
279
|
- spec/helpers/logging_helper.rb
|
282
280
|
- spec/helpers/mock_alert.rb
|
data/spec/helpers/dsl_helper.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
module Interferon
|
2
|
-
module MockDSLMixin
|
3
|
-
def initialize
|
4
|
-
end
|
5
|
-
|
6
|
-
def get_or_set(field, val, block, default)
|
7
|
-
@hash ||= Hash.new
|
8
|
-
if val.nil?
|
9
|
-
f = @hash[field]
|
10
|
-
f.nil? ? default : f
|
11
|
-
else
|
12
|
-
@hash[field] = val
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
class MockAlertDSL < AlertDSL
|
18
|
-
include MockDSLMixin
|
19
|
-
|
20
|
-
def notify(v = nil)
|
21
|
-
get_or_set(:notify, v, nil, nil)
|
22
|
-
end
|
23
|
-
|
24
|
-
def metric(v = nil)
|
25
|
-
get_or_set(:metric, v, nil, nil)
|
26
|
-
end
|
27
|
-
|
28
|
-
def id(v = nil, &block)
|
29
|
-
get_or_set(:@id, v, block, '')
|
30
|
-
end
|
31
|
-
|
32
|
-
def silenced(v = nil, &block)
|
33
|
-
get_or_set(:@silenced, v, block, false)
|
34
|
-
end
|
35
|
-
|
36
|
-
def silenced_until(v = nil, &block)
|
37
|
-
get_or_set(:@silenced_until, v && Time.parse(v), block, Time.at(0))
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
class MockNotifyDSL < NotifyDSL
|
42
|
-
include MockDSLMixin
|
43
|
-
end
|
44
|
-
|
45
|
-
class MockMetricDSL < MetricDSL
|
46
|
-
include MockDSLMixin
|
47
|
-
end
|
48
|
-
end
|