flapjack 0.7.7 → 0.7.8

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  ## Flapjack Changelog
2
2
 
3
- # 0.7.7 - 2012-05-22
3
+ # 0.7.8 - 2013-05-30
4
+ - Feature: support multiline check output (thanks @bs-github) gh-100 (@jessereynolds)
5
+ - Bug: notification rule with no entities or entity tags not being allowed gh-193 (@jessereynolds)
6
+ - Bug: correct jabber alias in example acknowledgement strings gh-189 (@jessereynolds)
7
+ - Bug: entity lists in Web UI should be sorted alphabetically gh-195 (@jessereynolds)
8
+
9
+ # 0.7.7 - 2013-05-22
4
10
  - Bug: relax notification rule validations somewhat gh-185 (@jessereynolds)
5
11
  - Chore: log notification rule validation errors from api gh-183 (@jessereynolds)
6
12
 
@@ -25,11 +25,11 @@ def process_input(opts)
25
25
  skip unless line
26
26
  split_line = line.split("\t")
27
27
 
28
- object_type, timestamp, entity, check, state, check_time, check_latency, check_output, check_perfdata = split_line
28
+ object_type, timestamp, entity, check, state, check_time, check_latency, check_output, check_perfdata, check_long_output = split_line
29
29
 
30
30
  case
31
- when split_line.length != 9
32
- puts "ERROR - rejecting this line as it doesn't split into 9 tab separated strings: [#{line}]"
31
+ when ! (split_line.length > 8)
32
+ puts "ERROR - rejecting this line as it doesn't split into at least 9 tab separated strings: [#{line}]"
33
33
  next
34
34
  when (timestamp !~ /^\d+$/)
35
35
  puts "ERROR - rejecting this line as second string doesn't look like a timestamp: [#{line}]"
@@ -39,16 +39,18 @@ def process_input(opts)
39
39
  next
40
40
  end
41
41
 
42
- puts "#{object_type}, #{timestamp}, #{entity}, #{check}, #{state}, #{check_output}"
42
+ puts "#{object_type}, #{timestamp}, #{entity}, #{check}, #{state}, #{check_output}, #{check_long_output}"
43
43
 
44
44
  state = 'ok' if state.downcase == 'up'
45
45
  state = 'critical' if state.downcase == 'down'
46
+ details = check_long_output ? check_long_output.gsub(/\\n/, "\n") : nil
46
47
  event = {
47
48
  'entity' => entity,
48
49
  'check' => check,
49
50
  'type' => 'service',
50
51
  'state' => state,
51
52
  'summary' => check_output,
53
+ 'details' => details,
52
54
  'timestamp' => timestamp,
53
55
  }.to_json
54
56
  redis.lpush 'events', event
@@ -63,7 +65,7 @@ def main(opts)
63
65
  redis = Redis.new(opts[:redis_options])
64
66
  while true
65
67
  process_input(:redis => redis, :fifo => fifo)
66
- puts "Whoops, restarting main loop in 10 seconds"
68
+ puts "Whoops with the fifo, restarting main loop in 10 seconds"
67
69
  sleep 10
68
70
  end
69
71
  end
@@ -6,22 +6,45 @@ Feature: Notification rules on a per contact basis
6
6
  | id | first_name | last_name | email | sms | timezone |
7
7
  | 1 | Malak | Al-Musawi | malak@example.com | +61400000001 | Asia/Baghdad |
8
8
  | 2 | Imani | Farooq | imani@example.com | +61400000002 | Europe/Moscow |
9
+ | 3 | Vera | Дурейко | vera@example.com | +61400000003 | Europe/Paris |
9
10
 
10
11
  And the following entities exist:
11
12
  | id | name | contacts |
12
13
  | 1 | foo | 1 |
13
- | 2 | bar | 1,2 |
14
- | 3 | baz | 1 |
14
+ | 2 | bar | 1,2,3 |
15
+ | 3 | baz | 1,3 |
16
+ | 4 | buf | 1,2,3 |
15
17
 
16
18
  And user 1 has the following notification intervals:
17
19
  | email | sms |
18
20
  | 15 | 60 |
19
21
 
22
+ And user 2 has the following notification intervals:
23
+ | email | sms |
24
+ | 15 | 60 |
25
+
26
+ And user 3 has the following notification intervals:
27
+ | email | sms |
28
+ | 15 | 60 |
29
+
20
30
  And user 1 has the following notification rules:
21
- | id | entities | entity_tags | warning_media | critical_media | warning_blackhole | critical_blackhole | time_restrictions |
22
- | 1 | foo | | email | sms,email | | | 8-18 weekdays |
23
- | 2 | bar | | | sms,email | true | | |
24
- | 3 | baz | | email | sms,email | | | |
31
+ | entities | entity_tags | warning_media | critical_media | warning_blackhole | critical_blackhole | time_restrictions |
32
+ | foo | | email | sms,email | | | 8-18 weekdays |
33
+ | bar | | | sms,email | true | | |
34
+ | baz | | email | sms,email | | | |
35
+
36
+ And user 2 has the following notification rules:
37
+ | entities | entity_tags | warning_media | critical_media | warning_blackhole | critical_blackhole | time_restrictions |
38
+ | | | email | email | | | |
39
+ | | | sms | sms | | | |
40
+ | bar | | email | email,sms | | | |
41
+
42
+ And user 3 has the following notification rules:
43
+ | entities | entity_tags | warning_media | critical_media | warning_blackhole | critical_blackhole | time_restrictions |
44
+ | buf | | email | email | | | |
45
+ | buf | | sms | sms | | | |
46
+ | | | email | email | | | |
47
+ | baz | | sms | sms | | | |
25
48
 
26
49
  @time_restrictions @time
27
50
  Scenario: Alerts only during specified time restrictions
@@ -152,3 +175,58 @@ Feature: Notification rules on a per contact basis
152
175
  And a critical event is received
153
176
  Then 3 email alerts should be queued for malak@example.com
154
177
  And 3 sms alerts should be queued for +61400000001
178
+
179
+ @time
180
+ Scenario: Contact with only entity specific rules should not be notified for other entities they are a contact for
181
+ Given the check is check 'ping' on entity 'buf'
182
+ And the check is in an ok state
183
+ When a critical event is received
184
+ And 1 minute passes
185
+ And a critical event is received
186
+ Then no email alerts should be queued for malak@example.com
187
+
188
+ @time
189
+ Scenario: Contact with entity specific rules and general rules should be notified for other entities they are a contact for
190
+ Given the check is check 'ping' on entity 'buf'
191
+ And the check is in an ok state
192
+ When a critical event is received
193
+ And 1 minute passes
194
+ And a critical event is received
195
+ Then 1 email alert should be queued for imani@example.com
196
+
197
+ @time
198
+ Scenario: Mutiple rules for an entity should be additive
199
+ Given the check is check 'ping' on entity 'buf'
200
+ And the check is in an ok state
201
+ When a critical event is received
202
+ And 1 minute passes
203
+ And a critical event is received
204
+ Then 1 email alert should be queued for vera@example.com
205
+ Then 1 sms alert should be queued for +61400000003
206
+
207
+ @time
208
+ Scenario: Multiple general rules should be additive
209
+ Given the check is check 'ping' on entity 'buf'
210
+ And the check is in an ok state
211
+ When a critical event is received
212
+ And 1 minute passes
213
+ And a critical event is received
214
+ Then 1 email alert should be queued for imani@example.com
215
+ Then 1 sms alert should be queued for +61400000002
216
+
217
+ @time
218
+ Scenario: An entity specific rule should override general rules
219
+ Given the check is check 'ping' on entity 'baz'
220
+ And the check is in an ok state
221
+ When a critical event is received
222
+ And 1 minute passes
223
+ And a critical event is received
224
+ Then 0 email alerts should be queued for vera@example.com
225
+ Then 1 sms alert should be queued for +61400000003
226
+
227
+ @time
228
+ Scenario: A blackhole rule on an entity should override another matching entity specific rule
229
+
230
+ @time
231
+ Scenario: A blackhole rule on an entity should override another matching general rule
232
+
@@ -94,6 +94,7 @@ module Flapjack
94
94
  event = { 'type' => 'action',
95
95
  'state' => 'test_notifications',
96
96
  'summary' => options['summary'],
97
+ 'details' => options['details'],
97
98
  'entity' => entity.name,
98
99
  'check' => check
99
100
  }
@@ -221,6 +222,7 @@ module Flapjack
221
222
  timestamp = options[:timestamp] || Time.now.to_i
222
223
  client = options[:client]
223
224
  summary = options[:summary]
225
+ details = options[:details]
224
226
  count = options[:count]
225
227
 
226
228
  # Note the current state (for speedy lookups)
@@ -233,6 +235,7 @@ module Flapjack
233
235
  @redis.rpush("#{@key}:states", timestamp)
234
236
  @redis.set("#{@key}:#{timestamp}:state", state)
235
237
  @redis.set("#{@key}:#{timestamp}:summary", summary) if summary
238
+ @redis.set("#{@key}:#{timestamp}:details", details) if details
236
239
  @redis.set("#{@key}:#{timestamp}:count", count) if count
237
240
 
238
241
  @redis.zadd("#{@key}:sorted_state_timestamps", timestamp, timestamp)
@@ -349,6 +352,11 @@ module Flapjack
349
352
  @redis.get("#{@key}:#{timestamp}:summary")
350
353
  end
351
354
 
355
+ def details
356
+ timestamp = @redis.lindex("#{@key}:states", -1)
357
+ @redis.get("#{@key}:#{timestamp}:details")
358
+ end
359
+
352
360
  # Returns a list of states for this entity check, sorted by timestamp.
353
361
  #
354
362
  # start_time and end_time should be passed as integer timestamps; these timestamps
@@ -368,7 +376,8 @@ module Flapjack
368
376
  state_data = state_ts.collect {|ts|
369
377
  {:timestamp => ts.to_i,
370
378
  :state => r.get("#{@key}:#{ts}:state"),
371
- :summary => r.get("#{@key}:#{ts}:summary")}
379
+ :summary => r.get("#{@key}:#{ts}:summary"),
380
+ :details => r.get("#{@key}:#{ts}:details")}
372
381
  }
373
382
  end
374
383
 
@@ -392,7 +401,8 @@ module Flapjack
392
401
  return if ts.nil? || ts.empty?
393
402
  {:timestamp => ts.first.to_i,
394
403
  :state => @redis.get("#{@key}:#{ts.first}:state"),
395
- :summary => @redis.get("#{@key}:#{ts.first}:summary")}
404
+ :summary => @redis.get("#{@key}:#{ts.first}:summary"),
405
+ :details => @redis.get("#{@key}:#{ts.first}:details")}
396
406
  end
397
407
 
398
408
  # Returns a list of maintenance periods (either unscheduled or scheduled) for this
@@ -126,6 +126,10 @@ module Flapjack
126
126
  @attrs['summary']
127
127
  end
128
128
 
129
+ def details
130
+ @attrs['details']
131
+ end
132
+
129
133
  def time
130
134
  return unless @attrs['time']
131
135
  @attrs['time'].to_i
@@ -39,6 +39,7 @@ module Flapjack
39
39
  @contents ||= {'event_id' => event.id,
40
40
  'state' => event.state,
41
41
  'summary' => event.summary,
42
+ 'details' => event.details,
42
43
  'time' => event.time,
43
44
  'duration' => event.duration || nil,
44
45
  'notification_type' => type,
@@ -218,13 +218,13 @@ module Flapjack
218
218
  d[:entity_tags].all? {|et| et.is_a?(String)} ) } =>
219
219
  "entity_tags must be a list of strings",
220
220
 
221
- proc { (d.has_key?(:entities) &&
222
- d[:entities].is_a?(Array) &&
223
- (d[:entities].size > 0)) ||
224
- (d.has_key?(:entity_tags) &&
225
- d[:entity_tags].is_a?(Array) &&
226
- (d[:entity_tags].size > 0)) } =>
227
- "entities or entity tags must have at least one value",
221
+ #proc { (d.has_key?(:entities) &&
222
+ # d[:entities].is_a?(Array) &&
223
+ # (d[:entities].size > 0)) ||
224
+ # (d.has_key?(:entity_tags) &&
225
+ # d[:entity_tags].is_a?(Array) &&
226
+ # (d[:entity_tags].size > 0)) } =>
227
+ #"entities or entity tags must have at least one value",
228
228
 
229
229
  proc { !d.has_key?(:time_restrictions) ||
230
230
  ( d[:time_restrictions].nil? ||
@@ -208,7 +208,7 @@ module Flapjack
208
208
  if event.state != event.previous_state
209
209
  entity_check.update_state(event.state, :timestamp => timestamp,
210
210
  :summary => event.summary, :client => event.client,
211
- :count => @event_count)
211
+ :count => @event_count, :details => event.details)
212
212
  end
213
213
 
214
214
  # No state change, and event is ok, so no need to run through filters
@@ -314,16 +314,23 @@ module Flapjack
314
314
  # don't consider notification rules if the contact has none
315
315
 
316
316
  tuple = messages.map do |message|
317
- @logger.debug "considering message for contact: #{message.contact.id} #{message.medium} #{message.notification.event.id} #{message.notification.event.state}"
318
317
  rules = message.contact.notification_rules
319
- @logger.debug "found #{rules.length} rules for this message's contact"
320
318
  event_id = message.notification.event.id
319
+
320
+ @logger.debug "considering message for contact: #{message.contact.id} #{message.medium} #{event_id} #{message.notification.event.state}"
321
+ @logger.debug "found #{rules.length} rules for this message's contact"
322
+
321
323
  options = {}
322
324
  options[:no_rules_for_contact] = true if rules.empty?
325
+
323
326
  # filter based on entity, tags, severity, time of day
324
327
  matchers = rules.find_all do |rule|
325
328
  rule.match_entity?(event_id) && rule_occurring_now?(rule, :contact => message.contact)
326
329
  end
330
+ @logger.debug "#{matchers.length} matchers remain for this message:"
331
+ matchers.each {|matcher|
332
+ @logger.debug "matcher: #{matcher.to_json}"
333
+ }
327
334
  [message, matchers, options]
328
335
  end
329
336
 
@@ -332,19 +339,26 @@ module Flapjack
332
339
 
333
340
  @logger.debug "apply_notification_rules: num messages after entity and time matching: #{tuple.size}"
334
341
 
335
- # delete the matcher for all entities if there are more specific matchers
342
+ # delete any matchers for all entities if there are more specific matchers
336
343
  tuple = tuple.map do |message, matchers, options|
344
+ num_matchers = matchers.length
337
345
  if matchers.length > 1
346
+ @logger.debug("general removal when entity specific: #{matchers.length} matchers")
338
347
  have_specific = matchers.detect do |matcher|
339
- matcher.entities or matcher.entity_tags
348
+ ( matcher.entities && !matcher.entities.empty? ) or
349
+ ( matcher.entity_tags && !matcher.entity_tags.empty? )
340
350
  end
341
351
  if have_specific
342
- # delete the rule for all entities
352
+ # delete any rules for all entities
343
353
  matchers.reject! do |matcher|
344
- matcher.entities.nil? && matcher.entity_tags.nil?
354
+ ( matcher.entities.nil? || matcher.entities.empty? ) &&
355
+ ( matcher.entity_tags.nil? || matcher.entity_tags.empty? )
345
356
  end
346
357
  end
347
358
  end
359
+ if num_matchers != matchers.length
360
+ @logger.debug("apply_notification_rules: removal of general matchers when entity specific matchers are present: number of matchers changed from #{num_matchers} to #{matchers.length} for message id: #{message.contact.id}, medium: #{message.medium}")
361
+ end
348
362
  [message, matchers, options]
349
363
  end
350
364
 
@@ -361,6 +375,7 @@ module Flapjack
361
375
  state = message.notification.event.state
362
376
  max_notified_severity = message.notification.max_notified_severity
363
377
 
378
+ @logger.debug "apply_notification_rules severity-media constraints: considering message: #{message.contact.id} #{message.medium} #{state}"
364
379
  # use EntityCheck#max_notified_severity_of_current_failure
365
380
  # as calculated prior to updating the last_notification* keys
366
381
  # if it's a higher severity than the current state
@@ -371,10 +386,13 @@ module Flapjack
371
386
  when [state, max_notified_severity].include?('warning')
372
387
  severity = 'warning'
373
388
  end
374
- options[:no_rules_for_contact] ||
375
- matchers.any? {|matcher|
389
+
390
+ no_rules_for_contact = !!options[:no_rules_for_contact]
391
+ sev_media_matchers = matchers.any? {|matcher|
376
392
  (matcher.media_for_severity(severity) || []).include?(message.medium)
377
393
  }
394
+ @logger.debug "apply_notification_rules severity-media constraints: no_rules_for_contact: #{no_rules_for_contact}, sev_media_matchers: #{sev_media_matchers}"
395
+ no_rules_for_contact || sev_media_matchers
378
396
  end
379
397
 
380
398
  @logger.debug "apply_notification_rules: num messages after pruning for severity-media constraints: #{tuple.size}"
@@ -32,6 +32,7 @@ module Flapjack
32
32
  @contact_last_name = notification['contact_last_name']
33
33
  @state = notification['state']
34
34
  @summary = notification['summary']
35
+ @details = notification['details']
35
36
  @time = notification['time']
36
37
  @entity_name, @check = notification['event_id'].split(':')
37
38
 
@@ -33,6 +33,11 @@
33
33
  %td
34
34
  %strong Summary
35
35
  %td= @summary
36
+ - if @details
37
+ %tr
38
+ %td
39
+ %strong Details
40
+ %td= @details
36
41
  %tr
37
42
  %td
38
43
  %strong Time
@@ -128,7 +128,7 @@ module Flapjack
128
128
  dur = nil
129
129
 
130
130
  if comment.nil? || (comment.length == 0)
131
- error = "please provide a comment, eg \"flapjack: ACKID #{$1} AL looking\""
131
+ error = "please provide a comment, eg \"#{@config['alias']}: ACKID #{$1} AL looking\""
132
132
  elsif duration_str
133
133
  # a fairly liberal match above, we'll let chronic_duration do the heavy lifting
134
134
  dur = ChronicDuration.parse(duration_str)
@@ -180,6 +180,7 @@ module Flapjack
180
180
  @check_last_update = entity_check.last_update
181
181
  @check_last_change = last_change
182
182
  @check_summary = entity_check.summary
183
+ @check_details = entity_check.details
183
184
  @last_notifications = entity_check.last_notifications_of_each_type
184
185
  @scheduled_maintenances = entity_check.maintenances(nil, nil, :scheduled => true)
185
186
  @acknowledgement_id = entity_check.failed? ?
@@ -34,6 +34,7 @@
34
34
  - if @current_scheduled_maintenance
35
35
  %h4 (Scheduled Maintenance - #{@current_scheduled_maintenance[:summary]})
36
36
  %h3 Output: #{@check_summary}
37
+ %p #{@check_details}
37
38
  %table{:class => "table table-hover table-condensed"}
38
39
  %tr
39
40
  %td Last state change:
@@ -17,7 +17,7 @@
17
17
  %tr
18
18
  %th Name
19
19
  %th Email
20
- - @contacts.sort_by {|c| [c.last_name, c.first_name] }.each do |contact|
20
+ - @contacts.sort_by {|c| [c.first_name, c.last_name] }.each do |contact|
21
21
  %tr
22
22
  %td
23
23
  - link = "/contacts/#{contact.id}"
@@ -15,7 +15,7 @@
15
15
  %p #{@count_failing_entities} failing out of #{@count_all_entities}
16
16
  %table{:class => "table table-bordered table-hover table-condensed"}
17
17
  - if @entities.length > 0
18
- - @entities.each do |entity|
18
+ - @entities.sort.each do |entity|
19
19
  %tr
20
20
  %td
21
21
  - link = "/entity/#{CGI.escape(entity)}"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  module Flapjack
4
- VERSION = "0.7.7"
4
+ VERSION = "0.7.8"
5
5
  end
@@ -95,8 +95,7 @@ describe Flapjack::Data::NotificationRule, :redis => true do
95
95
  context 'validation' do
96
96
 
97
97
  it "fails to add a notification rule with invalid data" do
98
- rule_data[:entities] = []
99
- rule_data[:entity_tags] = []
98
+ rule_data[:entities] = [1, {}]
100
99
  rule = Flapjack::Data::NotificationRule.add(rule_data, :redis => @redis)
101
100
  rule.should be_nil
102
101
  end
@@ -112,4 +111,4 @@ describe Flapjack::Data::NotificationRule, :redis => true do
112
111
 
113
112
  end
114
113
 
115
- end
114
+ end
@@ -45,12 +45,14 @@ describe Flapjack::Data::Notification, :redis => true do
45
45
  event.should_receive(:id).and_return('example.com:ping')
46
46
  event.should_receive(:state).and_return('ok')
47
47
  event.should_receive(:summary).and_return('Shiny & happy')
48
+ event.should_receive(:details).and_return('Really Shiny & happy')
48
49
  event.should_receive(:time).and_return(t)
49
50
  event.should_receive(:duration).and_return(nil)
50
51
 
51
52
  notification.contents.should == {'event_id' => 'example.com:ping',
52
53
  'state' => 'ok',
53
54
  'summary' => 'Shiny & happy',
55
+ 'details' => 'Really Shiny & happy',
54
56
  'time' => t,
55
57
  'duration' => nil,
56
58
  'notification_type' => 'problem',
@@ -58,4 +60,4 @@ describe Flapjack::Data::Notification, :redis => true do
58
60
 
59
61
  end
60
62
 
61
- end
63
+ end
@@ -122,6 +122,7 @@ describe Flapjack::Gateways::Web, :sinatra => true, :logger => true do
122
122
  entity_check.should_receive(:last_update).and_return(time - (3 * 60 * 60))
123
123
  entity_check.should_receive(:last_change).and_return(time - (3 * 60 * 60))
124
124
  entity_check.should_receive(:summary).and_return('all good')
125
+ entity_check.should_receive(:details).and_return('seriously, all very wonderful')
125
126
  entity_check.should_receive(:last_notifications_of_each_type).and_return(last_notifications)
126
127
  entity_check.should_receive(:maintenances).with(nil, nil, :scheduled => true).and_return([])
127
128
  entity_check.should_receive(:failed?).and_return(false)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flapjack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.7
4
+ version: 0.7.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-05-22 00:00:00.000000000 Z
14
+ date: 2013-05-30 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: dante
@@ -553,7 +553,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
553
553
  version: '0'
554
554
  segments:
555
555
  - 0
556
- hash: 4574905656823869764
556
+ hash: 3433474727648053171
557
557
  required_rubygems_version: !ruby/object:Gem::Requirement
558
558
  none: false
559
559
  requirements:
@@ -562,7 +562,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
562
562
  version: '0'
563
563
  segments:
564
564
  - 0
565
- hash: 4574905656823869764
565
+ hash: 3433474727648053171
566
566
  requirements: []
567
567
  rubyforge_project:
568
568
  rubygems_version: 1.8.23