flapjack 0.6.39 → 0.6.40

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