flapjack 0.6.61 → 0.7.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.
Files changed (38) hide show
  1. data/Gemfile +2 -1
  2. data/README.md +8 -4
  3. data/features/events.feature +269 -146
  4. data/features/notification_rules.feature +93 -0
  5. data/features/steps/events_steps.rb +162 -21
  6. data/features/steps/notifications_steps.rb +1 -1
  7. data/features/steps/time_travel_steps.rb +30 -19
  8. data/features/support/env.rb +71 -1
  9. data/flapjack.gemspec +3 -0
  10. data/lib/flapjack/data/contact.rb +256 -57
  11. data/lib/flapjack/data/entity.rb +2 -1
  12. data/lib/flapjack/data/entity_check.rb +22 -7
  13. data/lib/flapjack/data/global.rb +1 -0
  14. data/lib/flapjack/data/message.rb +2 -0
  15. data/lib/flapjack/data/notification_rule.rb +172 -0
  16. data/lib/flapjack/data/tag.rb +7 -2
  17. data/lib/flapjack/data/tag_set.rb +16 -0
  18. data/lib/flapjack/executive.rb +147 -13
  19. data/lib/flapjack/filters/delays.rb +21 -9
  20. data/lib/flapjack/gateways/api.rb +407 -27
  21. data/lib/flapjack/gateways/pagerduty.rb +1 -1
  22. data/lib/flapjack/gateways/web.rb +50 -22
  23. data/lib/flapjack/gateways/web/views/self_stats.haml +2 -0
  24. data/lib/flapjack/utility.rb +10 -0
  25. data/lib/flapjack/version.rb +1 -1
  26. data/spec/lib/flapjack/data/contact_spec.rb +103 -6
  27. data/spec/lib/flapjack/data/global_spec.rb +2 -0
  28. data/spec/lib/flapjack/data/message_spec.rb +6 -0
  29. data/spec/lib/flapjack/data/notification_rule_spec.rb +22 -0
  30. data/spec/lib/flapjack/data/notification_spec.rb +6 -0
  31. data/spec/lib/flapjack/gateways/api_spec.rb +727 -4
  32. data/spec/lib/flapjack/gateways/jabber_spec.rb +1 -0
  33. data/spec/lib/flapjack/gateways/web_spec.rb +11 -1
  34. data/spec/spec_helper.rb +10 -0
  35. data/tmp/notification_rules.rb +73 -0
  36. data/tmp/test_json_post.rb +16 -0
  37. data/tmp/test_notification_rules_api.rb +170 -0
  38. metadata +59 -2
@@ -6,6 +6,12 @@ require 'flapjack/filters/base'
6
6
  module Flapjack
7
7
  module Filters
8
8
 
9
+ # * If the service event’s state is a failure, and the time since the last state change
10
+ # is below a threshold (e.g. 30 seconds), then don't alert
11
+ # * If the service event’s state is a failure, and the time since the last alert is below a
12
+ # threshold (5 minutes), and the last notification state is the same as the current state, then don’t alert
13
+ #
14
+ # OLD:
9
15
  # * If the service event’s state is a failure, and the time since the ok => failure state change
10
16
  # is below a threshold (e.g. 30 seconds), then don't alert
11
17
  # * If the service event’s state is a failure, and the time since the last alert is below a
@@ -26,28 +32,34 @@ module Flapjack
26
32
 
27
33
  if entity_check.failed?
28
34
  last_problem_alert = entity_check.last_problem_notification
35
+ last_warning_alert = entity_check.last_warning_notification
36
+ last_critical_alert = entity_check.last_critical_notification
29
37
  last_change = entity_check.last_change
30
38
  last_notification = entity_check.last_notification
31
- last_alert_type = last_notification[:type]
39
+ last_alert_state = last_notification[:type]
32
40
  last_alert_timestamp = last_notification[:timestamp]
33
41
 
34
- current_failure_duration = current_time - last_change
42
+ current_state_duration = current_time - last_change
35
43
  time_since_last_alert = current_time - last_problem_alert unless last_problem_alert.nil?
36
44
  @log.debug("Filter: Delays: last_problem_alert: #{last_problem_alert.to_s}, " +
37
- "last_change: #{last_change.to_s}, " +
38
- "current_failure_duration: #{current_failure_duration}, " +
39
- "time_since_last_alert: #{time_since_last_alert.to_s}")
40
- if (current_failure_duration < failure_delay)
45
+ "last_change: #{last_change.inspect}, " +
46
+ "current_state_duration: #{current_state_duration.inspect}, " +
47
+ "time_since_last_alert: #{time_since_last_alert.inspect}, " +
48
+ "last_alert_state: [#{last_alert_state.inspect}], " +
49
+ "event.state: [#{event.state.inspect}], " +
50
+ "last_alert_state == event.state ? #{last_alert_state.to_s == event.state}")
51
+ if (current_state_duration < failure_delay)
41
52
  result = true
42
53
  @log.debug("Filter: Delays: blocking because duration of current failure " +
43
- "(#{current_failure_duration}) is less than failure_delay (#{failure_delay})")
54
+ "(#{current_state_duration}) is less than failure_delay (#{failure_delay})")
44
55
  elsif !last_problem_alert.nil? && (time_since_last_alert < resend_delay) &&
45
- (last_alert_type !~ /recovery/i)
56
+ (last_alert_state.to_s == event.state)
46
57
 
47
58
  result = true
48
59
  @log.debug("Filter: Delays: blocking because time since last alert for " +
49
60
  "current problem (#{time_since_last_alert}) is less than " +
50
- "resend_delay (#{resend_delay}) and last alert type (#{last_alert_type}) was not a recovery")
61
+ "resend_delay (#{resend_delay}) and last alert state (#{last_alert_state}) " +
62
+ "is equal to current event state (#{event.state})")
51
63
  else
52
64
  @log.debug("Filter: Delays: not blocking because neither of the time comparison " +
53
65
  "conditions were met")
@@ -48,22 +48,15 @@ module Flapjack
48
48
  module Gateways
49
49
 
50
50
  class API < Sinatra::Base
51
-
52
51
  set :show_exceptions, false
53
52
 
54
- if defined?(FLAPJACK_ENV) && 'test'.eql?(FLAPJACK_ENV)
55
- # expose test errors properly
56
- set :raise_errors, true
57
- else
58
- # doesn't work with Rack::Test unless we wrap tests in EM.synchrony blocks
59
- rescue_exception = Proc.new { |env, exception|
60
- @logger.error exception.message
61
- @logger.error exception.backtrace.join("\n")
62
- [503, {}, {:errors => [exception.message]}.to_json]
63
- }
53
+ rescue_exception = Proc.new { |env, exception|
54
+ @logger.error exception.message
55
+ @logger.error exception.backtrace.join("\n")
56
+ [503, {}, {:errors => [exception.message]}.to_json]
57
+ }
58
+ use Rack::FiberPool, :size => 25, :rescue_exception => rescue_exception
64
59
 
65
- use Rack::FiberPool, :size => 25, :rescue_exception => rescue_exception
66
- end
67
60
  use Rack::MethodOverride
68
61
  use Rack::JsonParamsParser
69
62
 
@@ -316,24 +309,395 @@ module Flapjack
316
309
  content_type :json
317
310
 
318
311
  errors = []
319
- ret = nil
320
312
 
321
- contacts = params[:contacts]
322
- if contacts && contacts.is_a?(Enumerable) && contacts.any? {|c| !c['id'].nil?}
323
- Flapjack::Data::Contact.delete_all(:redis => redis)
324
- contacts.each do |contact|
325
- unless contact['id']
326
- logger.warn "Contact not imported as it has no id: #{contact.inspect}"
327
- next
313
+ contacts_data = params[:contacts]
314
+ if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
315
+ errors << "No valid contacts were submitted"
316
+ else
317
+ # stringifying as integer string params are automatically integered,
318
+ # but our redis ids are strings
319
+ contacts_data_ids = contacts_data.reject {|c| c['id'].nil? }.
320
+ map {|co| co['id'].to_s }
321
+
322
+ if contacts_data_ids.empty?
323
+ errors << "No contacts with IDs were submitted"
324
+ else
325
+ contacts = Flapjack::Data::Contact.all(:redis => redis)
326
+ contacts_h = hashify(*contacts) {|c| [c.id, c] }
327
+ contacts_ids = contacts_h.keys
328
+
329
+ # delete contacts not found in the bulk list
330
+ (contacts_ids - contacts_data_ids).each do |contact_to_delete_id|
331
+ contact_to_delete = contacts.detect {|c| c.id == contact_to_delete_id }
332
+ contact_to_delete.delete!
333
+ end
334
+
335
+ # add or update contacts found in the bulk list
336
+ contacts_data.reject {|cd| cd['id'].nil? }.each do |contact_data|
337
+ if contacts_ids.include?(contact_data['id'].to_s)
338
+ contacts_h[contact_data['id'].to_s].update(contact_data)
339
+ else
340
+ Flapjack::Data::Contact.add(contact_data, :redis => redis)
341
+ end
328
342
  end
329
- Flapjack::Data::Contact.add(contact, :redis => redis)
330
343
  end
331
- ret = 200
332
- else
333
- ret = 403
334
- errors << "No valid contacts were submitted"
335
344
  end
336
- errors.empty? ? ret : [ret, {}, {:errors => [errors]}.to_json]
345
+ errors.empty? ? 200 : [403, {}, {:errors => [errors]}.to_json]
346
+ end
347
+
348
+ # Returns all the contacts
349
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts
350
+ get '/contacts' do
351
+ content_type :json
352
+ Flapjack::Data::Contact.all(:redis => redis).to_json
353
+ end
354
+
355
+ # Returns the core information about the specified contact
356
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id
357
+ get '/contacts/:contact_id' do
358
+ content_type :json
359
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
360
+ if contact.nil?
361
+ logger.warn "contact not found with id #{params[:contact_id]}"
362
+ status 404
363
+ return
364
+ end
365
+ contact.to_json
366
+ end
367
+
368
+ # Lists this contact's notification rules
369
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules
370
+ get '/contacts/:contact_id/notification_rules' do
371
+ content_type :json
372
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
373
+ if contact.nil?
374
+ logger.warn "contact not found with id #{params[:contact_id]}"
375
+ status 404
376
+ return
377
+ end
378
+ contact.notification_rules.to_json
379
+ end
380
+
381
+ # Get the specified notification rule for this user
382
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_notification_rules_id
383
+ get '/notification_rules/:rule_id' do
384
+ content_type :json
385
+
386
+ rule = Flapjack::Data::NotificationRule.find_by_id(params[:rule_id], :redis => redis)
387
+ if rule.nil?
388
+ logger.warn("Unable to find a notification rule with id [#{params[:rule_id]}]")
389
+ status 404
390
+ return
391
+ end
392
+ rule.to_json
393
+ end
394
+
395
+ # Creates a notification rule for a contact
396
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-post_contacts_id_notification_rules
397
+ post '/notification_rules' do
398
+ # # NB if parameters are correctly passed, we shouldn't mandate a request format
399
+ # pass unless 'application/json'.eql?(request.content_type)
400
+ content_type :json
401
+
402
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
403
+ if contact.nil?
404
+ logger.warn "contact not found with id #{params[:contact_id]}"
405
+ status 404
406
+ return
407
+ end
408
+ if params[:id]
409
+ logger.warn "post cannot be used for update, do a put instead"
410
+ status 403
411
+ return
412
+ end
413
+
414
+ rule_data = hashify(:entities, :entity_tags,
415
+ :warning_media, :critical_media, :time_restrictions,
416
+ :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
417
+ rule = contact.add_notification_rule(rule_data)
418
+ rule.to_json
419
+ end
420
+
421
+ # Updates a notification rule
422
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
423
+ put('/notification_rules/:id') do
424
+ # # NB if parameters are correctly passed, we shouldn't mandate a request format
425
+ # pass unless 'application/json'.eql?(request.content_type)
426
+ content_type :json
427
+
428
+ ret = nil
429
+
430
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
431
+ if contact.nil?
432
+ logger.warn "contact not found with id #{params[:contact_id]}"
433
+ status 404
434
+ return
435
+ end
436
+ rule = Flapjack::Data::NotificationRule.find_by_id(params[:id], :redis => redis)
437
+ if rule.nil?
438
+ logger.warn "rule not found with id #{params[:id]}"
439
+ status 404
440
+ return
441
+ end
442
+
443
+ rule_data = hashify(:entities, :entity_tags,
444
+ :warning_media, :critical_media, :time_restrictions,
445
+ :warning_blackhole, :critical_blackhole) {|k| [k, params[k]]}
446
+ rule.update(rule_data)
447
+ rule.to_json
448
+ end
449
+
450
+ # Deletes a notification rule
451
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_notification_rules_id
452
+ delete('/notification_rules/:id') do
453
+ rule = Flapjack::Data::NotificationRule.find_by_id(params[:id], :redis => redis)
454
+ if rule.nil?
455
+ status 404
456
+ return
457
+ end
458
+ contact = Flapjack::Data::Contact.find_by_id(rule.contact_id, :redis => redis)
459
+ if contact.nil?
460
+ logger.warn "contact not found with id #{rule.contact_id}"
461
+ status 404
462
+ return
463
+ end
464
+ contact.delete_notification_rule(rule)
465
+ status 204
466
+ end
467
+
468
+ # Returns the media of a contact
469
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media
470
+ get '/contacts/:contact_id/media' do
471
+ content_type :json
472
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
473
+ if contact.nil?
474
+ status 404
475
+ return
476
+ end
477
+ media = contact.media
478
+ media_intervals = contact.media_intervals
479
+ media_addr_int = hashify(*media.keys) {|k|
480
+ [k, {'address' => media[k],
481
+ 'interval' => media_intervals[k] }]
482
+ }
483
+ media_addr_int.to_json
484
+ end
485
+
486
+ # Returns the specified media of a contact
487
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_media_media
488
+ get('/contacts/:contact_id/media/:media_id') do
489
+ content_type :json
490
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
491
+ if contact.nil?
492
+ status 404
493
+ return
494
+ end
495
+ if contact.media[params[:media_id]].nil?
496
+ status 404
497
+ return
498
+ end
499
+ { 'address' => contact.media[params[:media_id]],
500
+ 'interval' => contact.media_intervals[params[:media_id]] }.to_json
501
+ end
502
+
503
+ # Creates or updates a media of a contact
504
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_media_media
505
+ put('/contacts/:contact_id/media/:media_id') do
506
+ content_type :json
507
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
508
+ if contact.nil?
509
+ status 404
510
+ return
511
+ end
512
+ if params[:address].nil? || params[:interval].nil?
513
+ status 403
514
+ return
515
+ end
516
+ contact.set_address_for_media(params[:media_id], params[:address])
517
+ contact.set_interval_for_media(params[:media_id], params[:interval])
518
+
519
+ { 'address' => contact.media[params[:media_id]],
520
+ 'interval' => contact.media_intervals[params[:media_id]] }.to_json
521
+ end
522
+
523
+ # delete a media of a contact
524
+ delete('/contacts/:contact_id/media/:media_id') do
525
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
526
+ if contact.nil?
527
+ status 404
528
+ return
529
+ end
530
+ contact.remove_media(params[:media_id])
531
+ status 204
532
+ end
533
+
534
+ # Returns the timezone of a contact
535
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id_timezone
536
+ get('/contacts/:contact_id/timezone') do
537
+ content_type :json
538
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
539
+ if contact.nil?
540
+ status 404
541
+ return
542
+ end
543
+ contact.timezone.name.to_json
544
+ end
545
+
546
+ # Sets the timezone of a contact
547
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
548
+ put('/contacts/:contact_id/timezone') do
549
+ content_type :json
550
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
551
+ if contact.nil?
552
+ status 404
553
+ return
554
+ end
555
+ contact.timezone = params[:timezone]
556
+ contact.timezone.name.to_json
557
+ end
558
+
559
+ # Removes the timezone of a contact
560
+ # https://github.com/flpjck/flapjack/wiki/API#wiki-put_contacts_id_timezone
561
+ delete('/contacts/:contact_id/timezone') do
562
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
563
+ if contact.nil?
564
+ status 404
565
+ return
566
+ end
567
+ contact.timezone = nil
568
+ status 204
569
+ end
570
+
571
+ post '/contacts/:contact_id/tags' do
572
+ content_type :json
573
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
574
+ if contact.nil?
575
+ status 404
576
+ return
577
+ end
578
+ tags = params[:tag]
579
+ if tags.nil? || tags.empty?
580
+ status 403
581
+ return
582
+ end
583
+ tags = [tags] unless tags.respond_to?(:each)
584
+ contact.add_tags(*tags)
585
+ contact.tags.to_json
586
+ end
587
+
588
+ post '/contacts/:contact_id/entity_tags' do
589
+ content_type :json
590
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
591
+ if contact.nil?
592
+ status 404
593
+ return
594
+ end
595
+ contact.entities.map {|e| e[:entity]}.each do |entity|
596
+ next unless tags = params[:entity][entity.name]
597
+ entity.add_tags(*tags)
598
+ end
599
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
600
+ [et[:entity].name, et[:tags]]
601
+ }
602
+ contact_ent_tag.to_json
603
+ end
604
+
605
+ delete '/contacts/:contact_id/tags' do
606
+ content_type :json
607
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
608
+ if contact.nil?
609
+ status 404
610
+ return
611
+ end
612
+ tags = params[:tag]
613
+ if tags.nil? || tags.empty?
614
+ status 403
615
+ return
616
+ end
617
+ tags = [tags] unless tags.respond_to?(:each)
618
+ contact.delete_tags(*tags)
619
+ status 204
620
+ end
621
+
622
+ delete '/contacts/:contact_id/entity_tags' do
623
+ content_type :json
624
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
625
+ if contact.nil?
626
+ status 404
627
+ return
628
+ end
629
+ contact.entities.map {|e| e[:entity]}.each do |entity|
630
+ next unless tags = params[:entity][entity.name]
631
+ entity.delete_tags(*tags)
632
+ end
633
+ status 204
634
+ end
635
+
636
+ get '/contacts/:contact_id/tags' do
637
+ content_type :json
638
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
639
+ if contact.nil?
640
+ status 404
641
+ return
642
+ end
643
+ contact.tags.to_json
644
+ end
645
+
646
+ get '/contacts/:contact_id/entity_tags' do
647
+ content_type :json
648
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
649
+ if contact.nil?
650
+ status 404
651
+ return
652
+ end
653
+ contact_ent_tag = hashify(*contact.entities(:tags => true)) {|et|
654
+ [et[:entity].name, et[:tags]]
655
+ }
656
+ contact_ent_tag.to_json
657
+ end
658
+
659
+ post '/entities/:entity/tags' do
660
+ content_type :json
661
+ entity = Flapjack::Data::Entity.find_by_name(params[:entity], :redis => redis)
662
+ if entity.nil?
663
+ status 404
664
+ return
665
+ end
666
+ tags = params[:tag]
667
+ if tags.nil? || tags.empty?
668
+ status 403
669
+ return
670
+ end
671
+ tags = [tags] unless tags.respond_to?(:each)
672
+ entity.add_tags(*tags)
673
+ entity.tags.to_json
674
+ end
675
+
676
+ delete '/entities/:entity/tags' do
677
+ content_type :json
678
+ entity = Flapjack::Data::Entity.find_by_name(params[:entity], :redis => redis)
679
+ if entity.nil?
680
+ status 404
681
+ return
682
+ end
683
+ tags = params[:tag]
684
+ if tags.nil? || tags.empty?
685
+ status 403
686
+ return
687
+ end
688
+ tags = [tags] unless tags.respond_to?(:each)
689
+ entity.delete_tags(*tags)
690
+ status 204
691
+ end
692
+
693
+ get '/entities/:entity/tags' do
694
+ content_type :json
695
+ entity = Flapjack::Data::Entity.find_by_name(params[:entity], :redis => redis)
696
+ if entity.nil?
697
+ status 404
698
+ return
699
+ end
700
+ entity.tags.to_json
337
701
  end
338
702
 
339
703
  not_found do
@@ -366,6 +730,22 @@ module Flapjack
366
730
  nil
367
731
  end
368
732
 
733
+ # The passed block will be provided each value from the args
734
+ # and must return array pairs [key, value] representing members of
735
+ # the hash this method returns. Keys should be unique -- if they're
736
+ # not, the earlier pair for that key will be overwritten.
737
+ def hashify(*args, &block)
738
+ key_value_pairs = args.map {|a| yield(a) }
739
+
740
+ # if using Ruby 1.9,
741
+ # Hash[ key_value_pairs ]
742
+ # is all that's needed, but for Ruby 1.8 compatability, these must
743
+ # be flattened and the resulting array unpacked. flatten(1) only
744
+ # flattens the arrays constructed in the block, it won't mess up
745
+ # any values (or keys) that are themselves arrays.
746
+ Hash[ *( key_value_pairs.flatten(1) )]
747
+ end
748
+
369
749
  end
370
750
 
371
751
  end