interferon 0.0.20 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|