flapjack 0.6.39 → 0.6.40

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 (66) hide show
  1. data/.gitignore +2 -2
  2. data/Gemfile +5 -1
  3. data/README.md +3 -2
  4. data/Rakefile +2 -1
  5. data/bin/flapjack +2 -2
  6. data/bin/flapjack-nagios-receiver +2 -8
  7. data/bin/flapjack-populator +11 -11
  8. data/etc/flapjack_config.yaml.example +28 -0
  9. data/features/steps/events_steps.rb +1 -1
  10. data/features/steps/notifications_steps.rb +7 -4
  11. data/features/support/env.rb +17 -6
  12. data/flapjack.gemspec +1 -0
  13. data/lib/flapjack/api.rb +72 -28
  14. data/lib/flapjack/configuration.rb +9 -1
  15. data/lib/flapjack/coordinator.rb +138 -162
  16. data/lib/flapjack/data/contact.rb +3 -1
  17. data/lib/flapjack/data/entity.rb +10 -1
  18. data/lib/flapjack/data/entity_check.rb +19 -21
  19. data/lib/flapjack/data/event.rb +26 -27
  20. data/lib/flapjack/data/message.rb +45 -0
  21. data/lib/flapjack/data/notification.rb +49 -0
  22. data/lib/flapjack/executive.rb +53 -74
  23. data/lib/flapjack/filters/acknowledgement.rb +14 -11
  24. data/lib/flapjack/jabber.rb +84 -18
  25. data/lib/flapjack/notification/email.rb +67 -37
  26. data/lib/flapjack/notification/sms.rb +40 -28
  27. data/lib/flapjack/oobetet.rb +1 -1
  28. data/lib/flapjack/pagerduty.rb +24 -15
  29. data/lib/flapjack/patches.rb +3 -1
  30. data/lib/flapjack/pikelet.rb +51 -20
  31. data/lib/flapjack/rack_logger.rb +8 -0
  32. data/lib/flapjack/version.rb +1 -1
  33. data/lib/flapjack/web.rb +51 -27
  34. data/spec/lib/flapjack/api_spec.rb +28 -3
  35. data/spec/lib/flapjack/coordinator_spec.rb +69 -43
  36. data/spec/lib/flapjack/data/contact_spec.rb +17 -9
  37. data/spec/lib/flapjack/data/entity_check_spec.rb +0 -25
  38. data/spec/lib/flapjack/data/entity_spec.rb +4 -0
  39. data/spec/lib/flapjack/data/global_spec.rb +6 -0
  40. data/spec/lib/flapjack/data/message_spec.rb +6 -0
  41. data/spec/lib/flapjack/data/notification_spec.rb +6 -0
  42. data/spec/lib/flapjack/executive_spec.rb +2 -2
  43. data/spec/lib/flapjack/jabber_spec.rb +8 -9
  44. data/spec/lib/flapjack/pagerduty_spec.rb +53 -45
  45. data/spec/lib/flapjack/utility_spec.rb +55 -0
  46. data/spec/lib/flapjack/web_spec.rb +7 -5
  47. data/tasks/events.rake +26 -59
  48. data/tasks/profile.rake +366 -0
  49. metadata +30 -19
  50. data/lib/flapjack/notification/common.rb +0 -23
  51. data/lib/flapjack/persistence/couch.rb +0 -5
  52. data/lib/flapjack/persistence/couch/connection.rb +0 -66
  53. data/lib/flapjack/persistence/couch/couch.rb +0 -63
  54. data/lib/flapjack/persistence/data_mapper.rb +0 -3
  55. data/lib/flapjack/persistence/data_mapper/data_mapper.rb +0 -67
  56. data/lib/flapjack/persistence/data_mapper/models/check.rb +0 -90
  57. data/lib/flapjack/persistence/data_mapper/models/check_template.rb +0 -20
  58. data/lib/flapjack/persistence/data_mapper/models/event.rb +0 -19
  59. data/lib/flapjack/persistence/data_mapper/models/node.rb +0 -18
  60. data/lib/flapjack/persistence/data_mapper/models/related_check.rb +0 -15
  61. data/lib/flapjack/persistence/sqlite3.rb +0 -3
  62. data/lib/flapjack/persistence/sqlite3/sqlite3.rb +0 -166
  63. data/lib/flapjack/transports/beanstalkd.rb +0 -50
  64. data/lib/flapjack/transports/result.rb +0 -58
  65. data/lib/flapjack/worker/application.rb +0 -121
  66. data/lib/flapjack/worker/cli.rb +0 -49
@@ -18,7 +18,8 @@ describe Flapjack::Web, :sinatra => true, :redis => true do
18
18
  let(:entity_check) { mock(Flapjack::Data::EntityCheck) }
19
19
 
20
20
  before(:each) do
21
- Flapjack::Web.class_variable_set('@@redis', @redis)
21
+ Flapjack::RedisPool.should_receive(:new).and_return(@redis)
22
+ Flapjack::Web.bootstrap(:config => {})
22
23
  end
23
24
 
24
25
  def expect_stats
@@ -169,10 +170,8 @@ describe Flapjack::Web, :sinatra => true, :redis => true do
169
170
  last_response.status.should == 302
170
171
  end
171
172
 
172
- # FIXME: how to support the patch http method? ... also, is putting the post data into the url the
173
- # way to go here?
174
173
  it "updates a scheduled maintenance period for an entity check" do
175
- t = Time.now.to_i
174
+ t = Time.new.to_i
176
175
 
177
176
  start_time = t - (24 * 60 * 60)
178
177
 
@@ -182,10 +181,13 @@ describe Flapjack::Web, :sinatra => true, :redis => true do
182
181
  Flapjack::Data::EntityCheck.should_receive(:for_entity).
183
182
  with(entity, 'ping', :redis => @redis).and_return(entity_check)
184
183
 
184
+ Chronic.should_receive(:parse).with('now').and_return(t)
185
+
185
186
  entity_check.should_receive(:update_scheduled_maintenance).
186
187
  with(start_time, {:end_time => t})
187
188
 
188
- patch "/scheduled_maintenances/#{entity_name_esc}/ping", {"start_time" => start_time, "end_time" => 'now'}
189
+ patch "/scheduled_maintenances/#{entity_name_esc}/ping",
190
+ {"start_time" => start_time, "end_time" => 'now'}
189
191
  last_response.status.should == 302
190
192
  end
191
193
 
data/tasks/events.rake CHANGED
@@ -1,7 +1,4 @@
1
-
2
1
  require 'redis'
3
- require 'yaml'
4
- require 'yajl'
5
2
 
6
3
  namespace :events do
7
4
 
@@ -9,76 +6,46 @@ namespace :events do
9
6
  desc "send events to trigger some notifications"
10
7
  task :test_notification do
11
8
 
12
- # config file reading stuff ...
13
- # FIXME: move this into somewhere reusable
14
- FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'development'
15
- config_file = File.join('etc', 'flapjack_config.yaml')
16
- if File.file?(config_file)
17
- config = YAML::load(File.open(config_file))
18
- else
19
- puts "Could not find 'etc/flapjack_config.yaml'"
20
- exit(false)
21
- end
22
- config_env = config[FLAPJACK_ENV]
23
- @redis_host = config_env['redis']['host'] || 'localhost'
24
- @redis_port = config_env['redis']['port'] || '6379'
25
- @redis_path = config_env['redis']['path'] || nil
26
- @redis_db = config_env['redis']['db'] || 0
27
-
28
- if config_env.nil? || config_env.empty?
29
- puts "No config data for environment '#{FLAPJACK_ENV}'"
30
- exit(false)
31
- end
32
-
33
9
  # add lib to the default include path
34
10
  unless $:.include?(File.dirname(__FILE__) + '/../lib/')
35
11
  $: << File.dirname(__FILE__) + '/../lib'
36
12
  end
37
13
 
38
- def get_redis_connection
39
- if @redis_path
40
- redis = Redis.new(:db => @redis_db, :path => @redis_path)
41
- else
42
- redis = Redis.new(:db => @redis_db, :host => @redis_host, :port => @redis_port)
43
- end
44
- redis
45
- end
14
+ require 'flapjack/configuration'
15
+ require 'flapjack/data/event'
46
16
 
47
- # creates an event object and adds it to the events list in redis
48
- # 'entity' => entity,
49
- # 'check' => check,
50
- # 'type' => 'service',
51
- # 'state' => state,
52
- # 'summary' => check_output,
53
- # 'time' => timestamp,
54
- def create_event(event)
55
- redis = get_redis_connection
56
- evt = Yajl::Encoder.encode(event)
57
- puts "sending #{evt}"
58
- redis.rpush('events', evt)
17
+ FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'development'
18
+ config_file = File.join('etc', 'flapjack_config.yaml')
19
+ @config, @redis_config = Flapjack::Configuration.new.load( config_file )
20
+
21
+ if @config.nil? || @config.empty?
22
+ puts "No config data for environment '#{FLAPJACK_ENV}' found in '#{config_file}'"
23
+ exit(false)
59
24
  end
60
25
 
61
- create_event( 'entity' => 'clientx-app-01',
62
- 'check' => 'ping',
63
- 'type' => 'service',
64
- 'state' => 'ok',
65
- 'summary' => 'testing' )
26
+ redis = Redis.new(@redis_config)
27
+
28
+ Flapjack::Data::Event.add({'entity' => 'clientx-app-01',
29
+ 'check' => 'ping',
30
+ 'type' => 'service',
31
+ 'state' => 'ok',
32
+ 'summary' => 'testing'}, :redis => redis)
66
33
 
67
34
  sleep(8)
68
35
 
69
- create_event( 'entity' => 'clientx-app-01',
70
- 'check' => 'ping',
71
- 'type' => 'service',
72
- 'state' => 'critical',
73
- 'summary' => 'testing' )
36
+ Flapjack::Data::Event.add({'entity' => 'clientx-app-01',
37
+ 'check' => 'ping',
38
+ 'type' => 'service',
39
+ 'state' => 'critical',
40
+ 'summary' => 'testing'}, :redis => redis)
74
41
 
75
42
  sleep(8)
76
43
 
77
- create_event( 'entity' => 'clientx-app-01',
78
- 'check' => 'ping',
79
- 'type' => 'service',
80
- 'state' => 'ok',
81
- 'summary' => 'testing' )
44
+ Flapjack::Data::Event.add({'entity' => 'clientx-app-01',
45
+ 'check' => 'ping',
46
+ 'type' => 'service',
47
+ 'state' => 'ok',
48
+ 'summary' => 'testing'}, :redis => redis)
82
49
 
83
50
  end
84
51
 
@@ -0,0 +1,366 @@
1
+ namespace :profile do
2
+
3
+ require 'fileutils'
4
+ require 'flapjack/configuration'
5
+
6
+ FLAPJACK_ROOT = File.join(File.dirname(__FILE__), '..')
7
+ FLAPJACK_CONFIG = File.join(FLAPJACK_ROOT, 'etc', 'flapjack_config.yaml')
8
+
9
+ FLAPJACK_PROFILER = ENV['FLAPJACK_PROFILER'] || 'rubyprof'
10
+ port = ENV['FLAPJACK_PROFILER'].to_i
11
+ FLAPJACK_PORT = ((port > 1024) && (port <= 65535)) ? port : 8075
12
+
13
+ REPETITIONS = 10
14
+ RESQUE_REPETITIONS = 2
15
+
16
+ require 'ruby-prof'
17
+
18
+ def profile_pikelet(klass, name, config, redis_options, &block)
19
+ redis = Redis.new(redis_options.merge(:driver => 'ruby'))
20
+ check_db_empty(:redis => redis, :redis_options => redis_options)
21
+ setup_baseline_data(:redis => redis)
22
+
23
+ EM.synchrony do
24
+ RubyProf.start
25
+ pikelet = klass.new
26
+ pikelet.bootstrap(:config => config,
27
+ :redis_config => redis_options)
28
+
29
+ EM.defer(block, proc {
30
+ pikelet.stop
31
+ pikelet.add_shutdown_event(:redis => redis)
32
+ })
33
+
34
+ pikelet.main
35
+ pikelet.cleanup
36
+ result = RubyProf.stop
37
+ result.eliminate_methods!([/Class::Thread/, /Deferrable/])
38
+ printer = RubyProf::MultiPrinter.new(result)
39
+ output_dir = File.join('tmp', 'profiles')
40
+ FileUtils.mkdir_p(output_dir)
41
+ printer.print(:path => output_dir, :profile => name)
42
+ EM.stop
43
+ end
44
+
45
+ empty_db(:redis => redis)
46
+ redis.quit
47
+ end
48
+
49
+ # rubyprof doesn't like the mail gem, possibly due to treetop -- crashes
50
+ # with "stack level too deep" errors when generating if the profiling
51
+ # runs over 3 or more mails.
52
+ def profile_resque(cfg_name, config, redis_options, &block)
53
+ redis = Redis.new(redis_options.merge(:driver => 'ruby'))
54
+ check_db_empty(:redis => redis, :redis_options => redis_options)
55
+ setup_baseline_data(:redis => redis)
56
+
57
+ ::Resque.redis = redis
58
+
59
+ EM.synchrony do
60
+ FlapjackProfileResque.instance_eval {
61
+
62
+ class << self
63
+
64
+ alias_method :orig_perform, :perform
65
+
66
+ # NB: this is very brittle in the case of exceptions; Resque swallows them,
67
+ # so you'll need to turn on the worker's verbose switches below to see what's
68
+ # really going on
69
+ def perform(notification)
70
+ r = nil
71
+ begin
72
+ count = if FlapjackProfileResque.class_variable_defined?('@@profile_count')
73
+ FlapjackProfileResque.class_variable_get('@@profile_count')
74
+ else
75
+ FlapjackProfileResque.class_variable_set('@@profile_count', 0)
76
+ end
77
+
78
+ RubyProf.send( (count.zero? ? :start : :resume) )
79
+ r = orig_perform(notification)
80
+ RubyProf.pause
81
+
82
+ count += 1
83
+ FlapjackProfileResque.class_variable_set('@@profile_count', count)
84
+ rescue Exception => e
85
+ puts e.message
86
+ end
87
+ r
88
+ end
89
+ end
90
+
91
+ }
92
+
93
+ FlapjackProfileResque.bootstrap(:config => config)
94
+
95
+ worker = EM::Resque::Worker.new(config['queue'])
96
+ worker.verbose = true
97
+ worker.very_verbose = true
98
+
99
+ EM.defer(block)
100
+
101
+ worker.work(0.1) {|job|
102
+ if FlapjackProfileResque.class_variable_defined?('@@profile_count') &&
103
+ (FlapjackProfileResque.class_variable_get('@@profile_count') >= RESQUE_REPETITIONS)
104
+ job.worker.shutdown
105
+ end
106
+ }
107
+
108
+ result = RubyProf.stop
109
+ printer = RubyProf::MultiPrinter.new(result)
110
+ output_dir = File.join('tmp', 'profiles')
111
+ FileUtils.mkdir_p(output_dir)
112
+ printer.print(:path => output_dir, :profile => cfg_name)
113
+
114
+ EM.stop
115
+ end
116
+
117
+ empty_db(:redis => redis)
118
+ redis.quit
119
+ end
120
+
121
+ def profile_thin(klass, name, config, redis_options, &block)
122
+ redis = Redis.new(redis_options.merge(:driver => 'ruby'))
123
+ check_db_empty(:redis => redis, :redis_options => redis_options)
124
+ setup_baseline_data(:redis => redis)
125
+
126
+ Thin::Logging.silent = true
127
+
128
+ EM.synchrony do
129
+ output_dir = File.join('tmp', 'profiles')
130
+ FileUtils.mkdir_p(output_dir)
131
+
132
+ profile_klass = Class.new(klass)
133
+ profile_klass.instance_eval {
134
+ before do
135
+ RubyProf.send( (profile_klass.class_variable_defined?('@@profiling') ? :resume : :start) )
136
+ profile_klass.class_variable_set('@@profiling', true)
137
+ end
138
+ after { RubyProf.pause }
139
+ }
140
+
141
+ profile_klass.bootstrap(:config => config, :redis_config => redis_options)
142
+
143
+ server = Thin::Server.new('0.0.0.0', FLAPJACK_PORT,
144
+ profile_klass, :signals => false)
145
+
146
+ server.start
147
+
148
+ EM.defer(block, proc {
149
+ result = RubyProf.stop
150
+ server.stop!
151
+ Fiber.new {
152
+ profile_klass.cleanup
153
+ }
154
+ printer = RubyProf::MultiPrinter.new(result)
155
+ printer.print(:path => output_dir, :profile => name)
156
+ EM.stop
157
+ })
158
+ end
159
+
160
+ empty_db(:redis => redis)
161
+ redis.quit
162
+ end
163
+
164
+ ### utility methods
165
+
166
+ def load_config
167
+ config_env, redis_options = Flapjack::Configuration.new.
168
+ load(FLAPJACK_CONFIG)
169
+ if config_env.nil? || config_env.empty?
170
+ puts "No config data for environment '#{FLAPJACK_ENV}' " +
171
+ "found in '#{FLAPJACK_CONFIG}'"
172
+ exit(false)
173
+ end
174
+
175
+ return config_env, redis_options
176
+ end
177
+
178
+ def check_db_empty(options = {})
179
+ redis = options[:redis]
180
+ redis_options = options[:redis_options]
181
+
182
+ # DBSIZE can return > 0 with expired keys -- but that's fine, we only
183
+ # want to run against an explicitly empty DB. If this fails against the
184
+ # intended Redis DB, the user can FLUSHDB it manually
185
+ db_size = redis.dbsize.to_i
186
+ if db_size > 0
187
+ db = redis_options['db']
188
+ puts "The Redis database has a non-zero DBSIZE (#{db_size}) -- "
189
+ "profiling will destroy data. Use 'SELECT #{db}; FLUSHDB' in " +
190
+ 'redis-cli if you want to profile using this database.'
191
+ puts "[redis options] #{options[:redis].inspect}\nExiting..."
192
+ exit(false)
193
+ end
194
+ end
195
+
196
+ # this adds a default entity and contact, so that the profiling methods
197
+ # will actually trigger enough code to be useful
198
+ def setup_baseline_data(options = {})
199
+ entity = {"id" => "2000",
200
+ "name" => "clientx-app-01",
201
+ "contacts" => ["1000"]}
202
+
203
+ Flapjack::Data::Entity.add(entity, :redis => options[:redis])
204
+
205
+ contact = {'id' => '1000',
206
+ 'first_name' => 'John',
207
+ 'last_name' => 'Smith',
208
+ 'email' => 'jsmith@example.com',
209
+ 'media' => {
210
+ 'email' => 'jsmith@example.com'
211
+ }}
212
+
213
+ Flapjack::Data::Contact.add(contact, :redis => options[:redis])
214
+ end
215
+
216
+ def empty_db(options = {})
217
+ redis = options[:redis]
218
+ redis.flushdb
219
+ end
220
+
221
+ ## end utility methods
222
+
223
+ desc "profile executive with rubyprof"
224
+ task :executive do
225
+
226
+ require 'flapjack/executive'
227
+ require 'flapjack/data/event'
228
+
229
+ FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'profile'
230
+ config_env, redis_options = load_config
231
+ profile_pikelet(Flapjack::Executive, 'executive', config_env['executive'],
232
+ redis_options) {
233
+
234
+ # this executes in a separate thread, so no Fibery stuff is allowed
235
+ redis = Redis.new(redis_options.merge(:driver => 'ruby'))
236
+
237
+ REPETITIONS.times do |n|
238
+ Flapjack::Data::Event.add({'entity' => 'clientx-app-01',
239
+ 'check' => 'ping',
240
+ 'type' => 'service',
241
+ 'state' => (n ? 'ok' : 'critical'),
242
+ 'summary' => 'testing'},
243
+ :redis => redis)
244
+ end
245
+ redis.quit
246
+ }
247
+ end
248
+
249
+ # NB: you'll need to access a real jabber server for this; if external events
250
+ # come in from that then runs will not be comparable
251
+ desc "profile jabber gateway with rubyprof"
252
+ task :jabber do
253
+
254
+ require 'flapjack/jabber'
255
+ require 'flapjack/data/contact'
256
+ require 'flapjack/data/event'
257
+ require 'flapjack/data/notification'
258
+
259
+ FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'profile'
260
+ config_env, redis_options = load_config
261
+ profile_pikelet(Flapjack::Jabber, 'jabber', config_env['jabber_gateway'],
262
+ redis_options) {
263
+
264
+ # this executes in a separate thread, so no Fibery stuff is allowed
265
+ redis = Redis.new(redis_options.merge(:driver => 'ruby'))
266
+
267
+ event = Flapjack::Data::Event.new('type' => 'service',
268
+ 'state' => 'critical',
269
+ 'summary' => '100% packet loss',
270
+ 'entity' => 'clientx-app-01',
271
+ 'check' => 'ping')
272
+ notification = Flapjack::Data::Notification.for_event(event)
273
+
274
+ contact = Flapjack::Data::Contact.find_by_id('1000', :redis => redis)
275
+
276
+ REPETITIONS.times do |n|
277
+ notification.messages(:contacts => [contact]).each do |msg|
278
+ contents = msg.contents
279
+ contents['event_count'] = n
280
+ redis.rpush(config_env['jabber_gateway']['queue'],
281
+ Yajl::Encoder.encode(contents))
282
+ end
283
+ end
284
+
285
+ redis.quit
286
+ }
287
+ end
288
+
289
+ # NB: you'll need an external email server set up for this (whether it's
290
+ # mailtrap or a real server)
291
+ desc "profile email notifier with rubyprof"
292
+ task :email do
293
+
294
+ require 'eventmachine'
295
+ # the redis/synchrony gems need to be required in this particular order, see
296
+ # the redis-rb README for details
297
+ require 'hiredis'
298
+ require 'em-synchrony'
299
+ require 'redis/connection/synchrony'
300
+ require 'redis'
301
+ require 'em-resque'
302
+ require 'em-resque/worker'
303
+
304
+ require 'flapjack/patches'
305
+ require 'flapjack/redis_pool'
306
+ require 'flapjack/notification/email'
307
+
308
+ require 'flapjack/data/contact'
309
+ require 'flapjack/data/event'
310
+ require 'flapjack/data/notification'
311
+
312
+ FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'profile'
313
+ config_env, redis_options = load_config
314
+
315
+ FlapjackProfileResque = Class.new(Flapjack::Notification::Email)
316
+
317
+ profile_resque('email', config_env['email_notifier'], redis_options) {
318
+
319
+ # this executes in a separate thread, so no Fibery stuff is allowed
320
+ redis = Redis.new(redis_options.merge(:driver => 'ruby'))
321
+
322
+ event = Flapjack::Data::Event.new('type' => 'service',
323
+ 'state' => 'critical',
324
+ 'summary' => '100% packet loss',
325
+ 'entity' => 'clientx-app-01',
326
+ 'check' => 'ping')
327
+ notification = Flapjack::Data::Notification.for_event(event)
328
+
329
+ contact = Flapjack::Data::Contact.find_by_id('1000', :redis => redis)
330
+
331
+ RESQUE_REPETITIONS.times do
332
+ notification.messages(:contacts => [contact]).each do |msg|
333
+ Resque.enqueue_to(config_env['email_notifier']['queue'],
334
+ FlapjackProfileResque, msg.contents)
335
+ end
336
+ end
337
+
338
+ redis.quit
339
+ }
340
+ end
341
+
342
+ # Of course, if external requests come to this server then different runs will
343
+ # not be comparable
344
+ desc "profile web server with rubyprof"
345
+ task :web do
346
+
347
+ require 'net/http'
348
+ require 'uri'
349
+
350
+ require 'flapjack/web'
351
+
352
+ FLAPJACK_ENV = ENV['FLAPJACK_ENV'] || 'profile'
353
+ config_env, redis_options = load_config
354
+ profile_thin(Flapjack::Web, 'web', config_env['web'], redis_options) {
355
+ uri = URI.parse("http://127.0.0.1:#{FLAPJACK_PORT}/")
356
+
357
+ http = Net::HTTP.new(uri.host, uri.port)
358
+
359
+ REPETITIONS.times do |n|
360
+ request = Net::HTTP::Get.new(uri.request_uri)
361
+ response = http.request(request)
362
+ end
363
+ }
364
+ end
365
+
366
+ end