flapjack 0.6.61 → 0.7.0

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