babysitter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ .rvmrc
2
+ .rspec
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in babysitter.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Lonely Planet Online
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,88 @@
1
+ # Babysitter
2
+
3
+ A ruby gem that uses [Fozzie](http://github.com/lonelyplanet/fozzie) to report progress on long-running tasks.
4
+ When provided with a Logger it will output statistics of progress to the logs.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem "babysitter"
11
+
12
+ And then execute:
13
+
14
+ $ bundle install
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install babysitter
19
+
20
+ ## Use
21
+
22
+
23
+ ### Configuration
24
+
25
+ Babysitter.configure do |c|
26
+ c.logger = MyApp.logger
27
+ end
28
+
29
+ The default logger does nothing, override if you want to see log output.
30
+
31
+ ### Amazon Simple Notification Service Integration
32
+
33
+ Babysitter can also make use of Amazons Simple Notification Service to provide notifications of when exceptions occur.
34
+
35
+ Babysitter.configure do |c|
36
+ c.enable_simple_notification_service(
37
+ topic_arn: "my-topic-arn",
38
+ credentials: -> {
39
+ access_key_id: "YOUR_ACCESS_KEY_ID",
40
+ secret_address_key: "YOUR_SECRET_ADDRESS_KEY",
41
+ }
42
+ )
43
+ end
44
+
45
+ The credentials should be supplied as a Proc so that they are only requested when they are required. This supports the use of temporary
46
+ credentials with IAM and prevents any credentials fetched at configuration time having expired at some point later when an error occurs.
47
+
48
+ ### Monitoring
49
+
50
+ monitor = Babysitter.monitor("statsd.bucket.name")
51
+ monitor.start("Workername: description") do |tracker|
52
+ things_to_do.each do |work|
53
+ do_some work
54
+ tracker.error(:badness,'Something bad happened') if something_bad?
55
+ tracker.warn(:suspicions,'Something supicious happenedd') if something_bad?
56
+ tracker.inc("Workername: {{count}} tasks completed", 1, counting: :things_to_do) # report progress here
57
+ end
58
+ end
59
+
60
+
61
+ This will send statistics to StatsD in the supplied bucket name and will generate logs like this:
62
+
63
+
64
+ INFO -- : Start: statsd.bucket.name Matcher generating possible combinations
65
+ INFO -- : Done: 100 combinations generated
66
+ INFO -- : Rate: 20746.88796680498 combinations per second
67
+ INFO -- : End: statsd.bucket.name
68
+
69
+ Logging statistics will incremented for bucket names
70
+
71
+ statsd.bucket.name.badness.errors
72
+ statsd.bucket.name.suspicions.warnings
73
+
74
+
75
+ Any exceptions that occur will be logged nicely. Exceptions will abort the process.
76
+
77
+ ## Development
78
+
79
+ $ git clone git@github.com:lonelyplanet/babysitter.git
80
+ $ cd babysitter
81
+
82
+ ## Contributing
83
+
84
+ 1. Fork it
85
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
86
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
87
+ 4. Push to the branch (`git push origin my-new-feature`)
88
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'babysitter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "babysitter"
8
+ gem.version = Babysitter::VERSION
9
+ gem.authors = ["Nicolas Overloop", "Paul Grayson", "Andy Roberts", "Mike Wagg"]
10
+ gem.email = ["noverloop@gmail.com", "paul.grayson@lonelyplanet.com", "coder@onesandthrees.com", "michael@guerillatactics.co.uk"]
11
+ gem.description = %q{Babysits long-running processes and reports progress or failures}
12
+ gem.summary = %q{Babysits long-running processes and reports progress or failures}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency 'fozzie'
20
+ gem.add_dependency 'timecop'
21
+ gem.add_dependency 'aws-sdk'
22
+
23
+ gem.add_development_dependency 'awesome_print'
24
+ gem.add_development_dependency 'rspec'
25
+
26
+ end
@@ -0,0 +1,36 @@
1
+
2
+
3
+ require_relative "babysitter/version"
4
+ require_relative "babysitter/null_logger"
5
+ require_relative "babysitter/configuration"
6
+ require_relative "babysitter/logging"
7
+ require_relative "babysitter/logger_with_stats"
8
+ require_relative "babysitter/tracker"
9
+ require_relative "babysitter/monitor"
10
+ require_relative "babysitter/counter"
11
+ require_relative "babysitter/exception_notifiers"
12
+ require 'fozzie'
13
+
14
+ module Babysitter
15
+
16
+ def self.monitor(*args)
17
+ Monitor.new(*args)
18
+ end
19
+
20
+ def self.configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ def self.configure
25
+ yield configuration
26
+ end
27
+
28
+ def self.logger
29
+ configuration.logger
30
+ end
31
+
32
+ def self.exception_notifiers
33
+ configuration.exception_notifiers
34
+ end
35
+
36
+ end
@@ -0,0 +1,22 @@
1
+ module Babysitter
2
+
3
+ class Configuration
4
+
5
+ attr_writer :logger
6
+ attr_reader :exception_notifiers
7
+
8
+ def initialize
9
+ @exception_notifiers = []
10
+ end
11
+
12
+ def logger
13
+ @logger ||= NullLogger.new
14
+ end
15
+
16
+ def enable_simple_notification_service(opts = {})
17
+ @exception_notifiers << ExceptionNotifiers::SimpleNotificationService.new(opts)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,60 @@
1
+ module Babysitter
2
+ class Counter
3
+ include Logging
4
+
5
+ attr_reader :count, :log_every, :template, :logged_count, :stat_name, :counting
6
+
7
+ attr_accessor :template
8
+
9
+ def initialize(log_every, opts)
10
+ @count = 0
11
+ @logged_count = 0
12
+ @log_every = log_every
13
+ @stat_name = opts.delete(:stat_name)
14
+ @counting = opts.delete(:counting)
15
+ @timer_start = Time.now
16
+ end
17
+
18
+ def inc( template, amount=1, opts={} )
19
+ @template = template
20
+ new_counting = opts.delete(:counting)
21
+ @counting = new_counting unless new_counting.nil?
22
+ log_this_time = block_number(@count) != block_number(@count + amount)
23
+ @count += amount
24
+ log_counter_messsage if log_this_time
25
+ end
26
+
27
+ def block_number(count)
28
+ count / @log_every
29
+ end
30
+
31
+ def final_report?
32
+ !(template.nil? or template.empty?) && count != logged_count
33
+ end
34
+
35
+ def log_counter_messsage
36
+ logger.info( "Done: #{template.gsub("{{count}}", count.to_s)}" )
37
+ send_progress_stats(count - logged_count)
38
+
39
+ rate = (count - logged_count).to_f / (Time.now - @timer_start)
40
+ logger.info( "Rate: #{rate} #{counting} per second" )
41
+ send_rate_stats(rate)
42
+
43
+ @logged_count = count
44
+ @timer_start = Time.now
45
+ end
46
+
47
+ def send_rate_stats(rate)
48
+ Stats.gauge stat_name+[counting, :rate], rate unless stat_name.nil?
49
+ end
50
+
51
+ def send_progress_stats(progress)
52
+ Stats.count stat_name+[counting, :progress], progress unless stat_name.nil?
53
+ end
54
+
55
+ def send_total_stats
56
+ Stats.gauge stat_name+[counting, :total], count
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1 @@
1
+ require_relative "exception_notifiers/simple_notification_service"
@@ -0,0 +1,42 @@
1
+ require 'aws-sdk'
2
+
3
+ module Babysitter
4
+ module ExceptionNotifiers
5
+ class SimpleNotificationService
6
+ def initialize(opts = {})
7
+ @topic_arn = opts.delete(:topic_arn)
8
+ @get_credentials = opts.delete(:credentials)
9
+ raise ArgumentError, "topic_arn is required." if @topic_arn.nil?
10
+ raise ArgumentError, "credentials is required and must be a Proc." if @get_credentials.nil? || !@get_credentials.is_a?(Proc)
11
+
12
+ validate_topic
13
+ end
14
+
15
+ def notify(subject, msg)
16
+ topic.publish(msg, subject: sanitise_subject(subject))
17
+ end
18
+
19
+ private
20
+
21
+ def sns
22
+ AWS::SNS.new(@get_credentials.call)
23
+ end
24
+
25
+ def topic
26
+ sns.topics[@topic_arn]
27
+ end
28
+
29
+ def sanitise_subject(subject)
30
+ sanitised = /\s*([^\x00-\x1F]*)/.match(subject)[1]
31
+
32
+ return "(no subject)" if sanitised.empty?
33
+ return sanitised[0..96] + "..." if sanitised.size > 100
34
+ sanitised
35
+ end
36
+
37
+ def validate_topic
38
+ topic.display_name
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ module Babysitter
2
+ class LoggerWithStats
3
+ include Logging
4
+
5
+ attr_accessor :stat_name_prefix
6
+
7
+ STATS_SUFFIX_BY_METHOD = { warn: :warnings, error: :errors, fatal: :fatals }
8
+
9
+ def initialize(stat_name_prefix)
10
+ @stat_name_prefix = stat_name_prefix
11
+ end
12
+
13
+ def method_missing(meth, *opts)
14
+ unless %w{ info debug error fatal}.include?(meth.to_s)
15
+ super
16
+ return
17
+ end
18
+ stats_suffix_from_method(meth).tap{ |suffix| increment(suffix) if suffix }
19
+ logger.send(meth, *opts)
20
+ end
21
+
22
+ def warn(*opts)
23
+ increment(stats_suffix_from_method(:warn))
24
+ logger.warn(*opts)
25
+ end
26
+
27
+ private
28
+
29
+ def stats_suffix_from_method(meth)
30
+ STATS_SUFFIX_BY_METHOD[meth]
31
+ end
32
+
33
+ def increment(stat_name_suffix)
34
+ Stats.increment full_stat_name(stat_name_suffix)
35
+ end
36
+
37
+ def full_stat_name(stat_name_suffix)
38
+ stat_name_prefix + [stat_name_suffix]
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,9 @@
1
+ module Babysitter
2
+ module Logging
3
+
4
+ def logger
5
+ Babysitter.logger
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ module Babysitter
2
+ class Monitor
3
+ include Logging
4
+
5
+ attr_accessor :stat_name
6
+
7
+ def initialize(stat_name=nil)
8
+ @stat_name = convert_stat_name_to_array(stat_name)
9
+ end
10
+
11
+ def start(msg=nil, log_every=100, &blk)
12
+ raise ArgumentError, "Stats bucket name must not be blank" if stat_name.nil? or stat_name.empty?
13
+ log_msg = format_log_message(msg)
14
+ tracker = Tracker.new(log_every, stat_name)
15
+ logger.info "Start: #{log_msg}"
16
+
17
+ begin
18
+ result = Stats.time_to_do stat_name+[:overall] do
19
+ blk.call(tracker)
20
+ end
21
+ rescue Exception => e
22
+ tracker.final_report rescue nil
23
+ log_exception_details(log_msg, e)
24
+ raise
25
+ end
26
+
27
+ tracker.send_total_stats
28
+ tracker.final_report
29
+ logger.info "End: #{log_msg}"
30
+ result
31
+ end
32
+
33
+ def completed(msg)
34
+ logger.info "Done: #{msg}"
35
+ end
36
+
37
+ private
38
+
39
+ def format_log_message(msg)
40
+ log_msg = stat_name.join('.')
41
+ [log_msg,msg].compact.join(' ')
42
+ end
43
+
44
+ def convert_stat_name_to_array(stat_name)
45
+ stat_name.is_a?(Array) ? stat_name : stat_name.split('.') unless stat_name.nil? or stat_name.empty?
46
+ end
47
+
48
+ def log_exception_details(msg, exception)
49
+ lines = ["Aborting: #{msg} due to exception #{exception.class}: #{exception}"]
50
+ lines.concat(exception.backtrace) if exception.backtrace
51
+
52
+ lines.each { |line| logger.error(line) }
53
+ Babysitter.exception_notifiers.each { |notifier| notifier.notify(exception.class.name, lines.join("\n")) }
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,11 @@
1
+ module Babysitter
2
+
3
+ class NullLogger
4
+
5
+ def method_missing( *args )
6
+ self
7
+ end
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,52 @@
1
+ module Babysitter
2
+ class Tracker
3
+ include Logging
4
+
5
+ attr_reader :counting, :stat_name, :counter
6
+ attr_accessor :log_every
7
+
8
+ def initialize(log_every, stat_name=nil)
9
+ @stat_name = stat_name
10
+ @counting = :iterations
11
+ @log_every = log_every
12
+ @counter = Counter.new(log_every, stat_name: stat_name, counting: counting)
13
+ end
14
+
15
+ def inc(*args)
16
+ counter.inc(*args)
17
+ end
18
+
19
+ def count
20
+ counter.count
21
+ end
22
+
23
+ def final_report
24
+ counter.log_counter_messsage if counter.final_report?
25
+ end
26
+
27
+ def warn(topic_name, message)
28
+ logger_with_stats_for(topic_name).warn(message)
29
+ end
30
+
31
+ def error(topic_name, message)
32
+ logger_with_stats_for(topic_name).error(message)
33
+ end
34
+
35
+ def send_total_stats
36
+ counter.send_total_stats
37
+ end
38
+
39
+ def logger_with_stats_for(topic_name)
40
+ @loggers ||= {}
41
+ @loggers[topic_name] ||= LoggerWithStats.new(stats_prefix_for_topic(topic_name))
42
+ end
43
+
44
+ private
45
+
46
+ def stats_prefix_for_topic(topic_name)
47
+ stat_name+[topic_name]
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,3 @@
1
+ module Babysitter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ module Babysitter
4
+ describe Configuration do
5
+ context 'when initialized' do
6
+
7
+ let (:null_logger) { double( 'null logger' ) }
8
+
9
+ it 'has a default logger set to a new NullLogger' do
10
+ NullLogger.should_receive(:new).and_return( null_logger )
11
+ subject.logger.should === null_logger
12
+ end
13
+
14
+ it 'has no exception notifiers' do
15
+ subject.exception_notifiers.should be_empty
16
+ end
17
+
18
+ end
19
+
20
+ describe 'enabling Amazon simple notification service integration' do
21
+ let (:sns_exception_notifier) { double }
22
+ let (:valid_params) { {
23
+ arbritary_key: "some-value",
24
+ topic_arn: "my-topic-arn"
25
+ } }
26
+
27
+ before :each do
28
+ Babysitter::ExceptionNotifiers::SimpleNotificationService.stub(:new).and_return(sns_exception_notifier)
29
+ end
30
+
31
+ it 'adds an exception notifier' do
32
+ subject.enable_simple_notification_service(valid_params)
33
+
34
+ subject.exception_notifiers.should_not be_empty
35
+ subject.exception_notifiers.first.should eql(sns_exception_notifier)
36
+ end
37
+
38
+ it 'configures the exception notifier' do
39
+ ExceptionNotifiers::SimpleNotificationService.should_receive(:new).with(hash_including(valid_params))
40
+
41
+ subject.enable_simple_notification_service(valid_params)
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ module Babysitter
4
+ module ExceptionNotifiers
5
+ describe SimpleNotificationService do
6
+ subject { SimpleNotificationService.new(valid_opts) }
7
+ let(:get_credentials_lambda) { -> {
8
+ {
9
+ access_key_id: 'an-access-key-id',
10
+ secret_access_key: 'a-secret-address-key',
11
+ }
12
+ } }
13
+ let(:valid_opts) { {
14
+ credentials: get_credentials_lambda,
15
+ topic_arn: 'my-topic-arn'
16
+ } }
17
+ let(:sns) { double :sns, topics: { 'my-topic-arn' => topic } }
18
+ let(:topic) { double :topic, publish: nil, display_name: "A topic" }
19
+ before :each do
20
+ AWS::SNS.stub(:new).and_return(sns)
21
+ end
22
+
23
+ it 'requires a topic_arn' do
24
+ valid_opts.delete :topic_arn
25
+ -> { subject }.should raise_error(ArgumentError, /topic_arn/)
26
+ end
27
+
28
+ it "requires credentials" do
29
+ valid_opts.delete :credentials
30
+ -> { subject }.should raise_error(ArgumentError, /credentials/)
31
+ end
32
+
33
+ it 'requires a block to retrieve AWS credentials' do
34
+ valid_opts[:credentials] = {}
35
+ -> { subject }.should raise_error(ArgumentError, /credentials/)
36
+ end
37
+
38
+ it 'uses the options passed to configure the credentials for sns' do
39
+ AWS::SNS.should_receive(:new).with(get_credentials_lambda.call)
40
+ subject
41
+ end
42
+
43
+ it 'validates the topic by checking it has a display name' do
44
+ topic.should_receive(:display_name)
45
+ subject
46
+ end
47
+
48
+ describe '.notify' do
49
+ let(:message) { "the message" }
50
+ let(:notification_subject) { "the subject" }
51
+
52
+ it 'again uses the options passed to configure the credentials for sns' do
53
+ AWS::SNS.should_receive(:new).with(get_credentials_lambda.call).twice
54
+ subject.notify(notification_subject, message)
55
+ end
56
+
57
+ it 'publishes to the topic specified' do
58
+ topic.should_receive(:publish)
59
+
60
+ subject.notify(notification_subject, message)
61
+ end
62
+
63
+ it 'publishes the message' do
64
+ topic.should_receive(:publish).with(message, hash_including(subject: notification_subject))
65
+
66
+ subject.notify(notification_subject, message)
67
+ end
68
+
69
+ it "shortens the subject to 100 characters if necessary" do
70
+ shortened_subject = 97.times.map { "x" }.join + "..."
71
+ original_subject = 101.times.map { "x" }.join
72
+
73
+ topic.should_receive(:publish).with(message, hash_including(subject: shortened_subject))
74
+
75
+ subject.notify(original_subject, message)
76
+ end
77
+
78
+ it "strips control characters" do
79
+ expected_subject = "this is the subject"
80
+ original_subject = "#{expected_subject}"
81
+ 32.times.each { |code| original_subject << code.chr }
82
+
83
+ topic.should_receive(:publish).with(message, hash_including(subject: expected_subject))
84
+
85
+ subject.notify(original_subject, message)
86
+ end
87
+
88
+ it "strips leading whitespace" do
89
+ expected_subject = "this is the subject"
90
+ original_subject = " #{expected_subject}"
91
+
92
+ topic.should_receive(:publish).with(message, hash_including(subject: expected_subject))
93
+
94
+ subject.notify(original_subject, message)
95
+ end
96
+
97
+ it "handles empty subject" do
98
+ topic.should_receive(:publish).with(message, hash_including(subject: "(no subject)"))
99
+
100
+ subject.notify("", message)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,231 @@
1
+ require 'spec_helper'
2
+
3
+ module Babysitter
4
+ describe Monitor do
5
+ before(:each) do
6
+ Stats.stub!(:count).with(anything, anything)
7
+ Stats.stub!(:gauge).with(anything, anything)
8
+ end
9
+
10
+ context 'when initialized with a dot separated bucket name' do
11
+
12
+ subject{ Monitor.new(bucket_name) }
13
+ let(:bucket_name) { 'my.splendid.bucket.name' }
14
+ let(:start_block) { Proc.new{ block_result } }
15
+ let(:block_result) { double('block result').as_null_object }
16
+ let(:logger) { double('logger').as_null_object }
17
+
18
+ describe '#completed' do
19
+ it 'logs a done message' do
20
+ Monitor.any_instance.stub(:logger).and_return(logger)
21
+ logger.should_receive(:info).with("Done: the completed thing")
22
+ subject.completed('the completed thing')
23
+ end
24
+ end
25
+
26
+ describe '#start' do
27
+
28
+ it 'yields to the block, returning the result' do
29
+ subject.start(&start_block).should === block_result
30
+ end
31
+
32
+ it 'calls Stats.time_to_do with the bucket name' do
33
+ expected_stat = bucket_name.split('.')+[:overall]
34
+ Stats.should_receive(:time_to_do).with(expected_stat)
35
+ subject.start(&start_block)
36
+ end
37
+
38
+ it 'calls logger.info with start message' do
39
+ Monitor.any_instance.stub(:logger).and_return(logger)
40
+ logger.should_receive(:info).with("Start: #{bucket_name}")
41
+ subject.start(&start_block)
42
+ end
43
+
44
+ it 'calls logger.info with end message' do
45
+ Monitor.any_instance.stub(:logger).and_return(logger)
46
+ logger.should_receive(:info).with("End: #{bucket_name}")
47
+ subject.start(&start_block)
48
+ end
49
+
50
+ context 'when the start method is given a message' do
51
+ it 'calls logger.info with start message' do
52
+ Monitor.any_instance.stub(:logger).and_return(logger)
53
+ logger.should_receive(:info).with("Start: #{bucket_name} special message")
54
+ subject.start('special message', &start_block)
55
+ end
56
+
57
+ it 'calls logger.info with end message' do
58
+ Monitor.any_instance.stub(:logger).and_return(logger)
59
+ logger.should_receive(:info).with("End: #{bucket_name} special message")
60
+ subject.start('special message', &start_block)
61
+ end
62
+ end
63
+
64
+ context 'when the block increments the counter twice, each with a count of 5, and identifies counted objects' do
65
+ let(:start_block_two_increments) do
66
+ Proc.new do |counter|
67
+ 2.times{ counter.inc('incrementing by {{count}} things', 5, counting: :things) }
68
+ end
69
+ end
70
+
71
+ it 'calls Stats.count with bucket name plus counted objects, and a count of 10' do
72
+ expected_bucket_name = bucket_name.split('.') + [:things, :total]
73
+ Stats.should_receive(:gauge).with(expected_bucket_name, 10)
74
+ subject.start(&start_block_two_increments)
75
+ end
76
+
77
+ it 'calls logger.info with each done message once' do
78
+ Counter.any_instance.stub(:logger).and_return(logger)
79
+ [5,10].each { |inc| logger.should_receive(:info).with( "Done: incrementing by #{inc} things").once }
80
+ subject.start('short message', 5, &start_block_two_increments)
81
+ end
82
+ end # context 'when the block increments the counter twice, each with a count of 5, and identifies counted objects'
83
+
84
+ context 'when the block increments the counter 7 times, with no amount specified, and no name for counted objects' do
85
+ let(:start_block_seven_increments) do
86
+ Proc.new do |counter|
87
+ 7.times{ counter.inc('incrementing by {{count}} things') }
88
+ end
89
+ end
90
+
91
+ it 'calls Stats.count with bucket name plus iterations, and a count of 7' do
92
+ expected_bucket_name = bucket_name.split('.') + [:iterations, :total]
93
+ Stats.should_receive(:gauge).with(expected_bucket_name, 7)
94
+ subject.start(&start_block_seven_increments)
95
+ end
96
+
97
+ it 'calls logger.info with each done message once' do
98
+ Counter.any_instance.stub(:logger).and_return(logger)
99
+ [5,7].each { |inc| logger.should_receive(:info).with( "Done: incrementing by #{inc} things").once }
100
+ subject.start('short message', 5, &start_block_seven_increments)
101
+ end
102
+ end # context 'when the block increments the counter 7 times, with no amount specified, and no name for counted objects'
103
+
104
+ context 'when logging every 10th call, and the block increments the counter 7 times, each with a count of 9, and identifies counted objects' do
105
+ let(:start_block_three_increments) do
106
+ Proc.new do |counter|
107
+ 7.times{ counter.inc('incrementing by {{count}} things', 9, counting: :things) }
108
+ end
109
+ end
110
+
111
+ it 'calls logger.info with increments 18,27,36,45,54,63' do
112
+ Counter.any_instance.stub(:logger).and_return(logger)
113
+ [18,27,36,45,54,63].each { |inc| logger.should_receive(:info).with( "Done: incrementing by #{inc} things").once }
114
+ subject.start('short message', 10, &start_block_three_increments)
115
+ end
116
+ end # context 'when logging every 10th call, and the block increments the counter 7 times, each with a count of 9, and identifies counted objects' do
117
+
118
+ context "when the block logs a warning" do
119
+ let(:start_block_with_warning) do
120
+ Proc.new do |monitor|
121
+ monitor.warn(:my_warning_bucket, 'my warning message')
122
+ end
123
+ end
124
+ before(:each) do
125
+ Babysitter.stub(:logger).and_return(logger)
126
+ logger.stub!(:warn)
127
+ Stats.stub!(:increment)
128
+ end
129
+
130
+ it 'calls logger.info with the warning message' do
131
+ logger.should_receive(:warn).with( "my warning message")
132
+ subject.start(&start_block_with_warning)
133
+ end
134
+
135
+ it 'calls Stats.count with warning bucket name' do
136
+ expected_bucket_name = bucket_name.split('.') + [:my_warning_bucket, :warnings]
137
+ Stats.should_receive(:increment).with(expected_bucket_name)
138
+ subject.start(&start_block_with_warning)
139
+ end
140
+ end
141
+
142
+ context "when the block logs an error" do
143
+ let(:start_block_with_error) do
144
+ Proc.new do |monitor|
145
+ monitor.error(:my_error_bucket, 'my error message')
146
+ end
147
+ end
148
+ before(:each) do
149
+ Babysitter.stub(:logger).and_return(logger)
150
+ logger.stub!(:error)
151
+ Stats.stub!(:increment)
152
+ end
153
+
154
+ it 'calls logger.error with the error message' do
155
+ logger.should_receive(:error).with( "my error message")
156
+ subject.start(&start_block_with_error)
157
+ end
158
+
159
+ it 'calls Stats.count with error bucket name' do
160
+ expected_bucket_name = bucket_name.split('.') + [:my_error_bucket, :errors]
161
+ Stats.should_receive(:increment).with(expected_bucket_name)
162
+ subject.start(&start_block_with_error)
163
+ end
164
+ end
165
+
166
+ context 'when the block raises an error' do
167
+ let(:error) { RuntimeError.new(error_message) }
168
+ let(:backtrace) { 3.times.map { |i| "Line #{i}"} }
169
+ let(:error_message) { 'A big fat error' }
170
+ let(:expected_message) { "Aborting: #{bucket_name} due to exception RuntimeError: #{error_message}" }
171
+ let(:start_block_with_error) { Proc.new { raise error } }
172
+ before(:each) do
173
+ error.stub(:backtrace).and_return(backtrace)
174
+ Babysitter.stub(:logger).and_return(logger)
175
+ Babysitter.stub(:exception_notifiers).and_return(2.times.map { double notify: nil })
176
+ logger.stub!(:error)
177
+ Stats.stub!(:increment)
178
+ end
179
+
180
+ it 'calls logger.error with the exeption details' do
181
+ logger.should_receive(:error).with(expected_message)
182
+ backtrace.each do |line|
183
+ logger.should_receive(:error).with(/\w*#{line}/)
184
+ end
185
+
186
+ begin
187
+ subject.start(&start_block_with_error)
188
+ rescue
189
+ end
190
+ end
191
+
192
+ it 'calls each exception notifier with the exception details' do
193
+ message = [expected_message].concat(backtrace).join("\n")
194
+
195
+ Babysitter.exception_notifiers.each do |exception_notifier|
196
+ exception_notifier.should_receive(:notify).with('RuntimeError', message)
197
+ end
198
+
199
+ begin
200
+ subject.start(&start_block_with_error)
201
+ rescue
202
+ end
203
+ end
204
+ end
205
+
206
+ context 'when the block increments 2 times at intervals of 2 seconds' do
207
+ let(:start_block_for_timing) do
208
+ Proc.new do |counter|
209
+ 2.times do
210
+ Timecop.travel(Time.now+2) # move on 2 seconds
211
+ counter.inc('doing increment',1)
212
+ end
213
+ end
214
+ end
215
+ before(:each) { Timecop.travel(Time.now) }
216
+ after(:each) { Timecop.return }
217
+
218
+ it 'calculates a rate close to 0.5 per second' do
219
+ Counter.any_instance.should_receive(:send_rate_stats) do |rate|
220
+ rate.should be_within(0.01).of(0.5)
221
+ end
222
+ subject.start(&start_block_for_timing)
223
+ end
224
+ end
225
+
226
+ end
227
+
228
+ end
229
+
230
+ end
231
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ module Babysitter
4
+ describe Tracker do
5
+
6
+ subject{ Tracker.new(log_interval, stat_bucket_prefix) }
7
+ let(:log_interval) { 5 }
8
+ let(:stat_bucket_prefix) { [:my, :stat, :bucket, :prefix] }
9
+ let(:logger) { double('boring old vanilla babysitter logger') }
10
+ before(:each) do
11
+ Babysitter.stub(:logger).and_return(logger)
12
+ end
13
+
14
+ describe '#logger_with_stats_for' do
15
+
16
+ it 'returns the same logger when passed the same symbol twice' do
17
+ l1 = subject.logger_with_stats_for(:lodgings)
18
+ l2 = subject.logger_with_stats_for(:lodgings)
19
+ l1.should be_equal(l2)
20
+ end
21
+
22
+ it 'returns different loggers when passed different symbols' do
23
+ l = subject.logger_with_stats_for(:lodgings)
24
+ p = subject.logger_with_stats_for(:places)
25
+ l.should_not be_equal(p)
26
+ end
27
+
28
+ end # describe '#logger_with_stats' do
29
+
30
+ describe 'logger returned by logger_with_stats_for(some_topic)' do
31
+ subject{ Tracker.new(log_interval, stat_bucket_prefix).logger_with_stats_for(some_topic) }
32
+ let(:some_topic) { :some_topic }
33
+ let(:text_of_the_message) {'the message we want in the logs'}
34
+
35
+ { warn: :warnings, error: :errors, fatal: :fatals }.each do |message_type, stats_bucket_suffix|
36
+ describe "##{message_type}" do
37
+ before(:each) do
38
+ logger.stub(message_type)
39
+ Stats.stub!(:increment)
40
+ end
41
+
42
+ it 'logs the message' do
43
+ logger.should_receive(message_type).with(text_of_the_message)
44
+ subject.send(message_type, text_of_the_message)
45
+ end
46
+
47
+ it 'sends the stats' do
48
+ expected_stats_bucket = stat_bucket_prefix + [some_topic, stats_bucket_suffix]
49
+ Stats.should_receive(:increment).with(expected_stats_bucket)
50
+ subject.send(message_type, text_of_the_message)
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ [:info, :debug].each do |message_type|
57
+ describe "##{message_type}" do
58
+ before(:each) do
59
+ logger.stub(message_type)
60
+ Stats.stub!(:increment)
61
+ end
62
+
63
+ it 'logs the message' do
64
+ logger.should_receive(message_type).with(text_of_the_message)
65
+ subject.send(message_type, text_of_the_message)
66
+ end
67
+
68
+ it 'sends no stats' do
69
+ Stats.should_not_receive(:increment)
70
+ subject.send(message_type, text_of_the_message)
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end # describe 'logger returned by logger_with_stats_for(:something)' do
77
+
78
+ end
79
+ end
80
+
81
+
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe Babysitter do
4
+
5
+ describe '.monitor' do
6
+ it 'returns an instance of Monitor' do
7
+ Babysitter.monitor.should be_an_instance_of(Babysitter::Monitor)
8
+ end
9
+ end
10
+
11
+ describe '.configuration' do
12
+ it 'returns an instance of Configuration' do
13
+ Babysitter.configuration.should be_an_instance_of(Babysitter::Configuration)
14
+ end
15
+
16
+ it 'returns the same instance every time' do
17
+ c = Babysitter.configuration
18
+ Babysitter.configuration.should eql(c)
19
+ end
20
+ end
21
+
22
+ describe '.configure' do
23
+ describe 'object yielded to block' do
24
+ it 'is the unique configuration object' do
25
+ Babysitter.configure do |c|
26
+ c.should eql(Babysitter.configuration)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '.logger' do
33
+ let(:configured_logger) { double('configured logger').as_null_object }
34
+
35
+ it 'returns the logger from the configuration' do
36
+ Babysitter::Configuration.any_instance.stub(:logger).and_return(configured_logger)
37
+ Babysitter.logger.should eql(configured_logger)
38
+ end
39
+ end
40
+
41
+ describe '.exception_notifiers' do
42
+ let(:exception_notifiers) { double('notifiers').as_null_object}
43
+
44
+ it 'returns the notifiers from the configuration' do
45
+ Babysitter::Configuration.any_instance.stub(:exception_notifiers).and_return(exception_notifiers)
46
+ Babysitter.exception_notifiers.should eql(exception_notifiers)
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,21 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ require 'awesome_print'
8
+ require 'babysitter'
9
+ require 'timecop'
10
+
11
+ RSpec.configure do |config|
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run :focus
15
+
16
+ # Run specs in random order to surface order dependencies. If you find an
17
+ # order dependency and want to debug it, you can fix the order by providing
18
+ # the seed, which is printed after each run.
19
+ # --seed 1234
20
+ config.order = 'random'
21
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: babysitter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nicolas Overloop
9
+ - Paul Grayson
10
+ - Andy Roberts
11
+ - Mike Wagg
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+ date: 2013-02-13 00:00:00.000000000 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: fozzie
19
+ requirement: !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ! '>='
23
+ - !ruby/object:Gem::Version
24
+ version: '0'
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: timecop
35
+ requirement: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ - !ruby/object:Gem::Dependency
50
+ name: aws-sdk
51
+ requirement: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ - !ruby/object:Gem::Dependency
66
+ name: awesome_print
67
+ requirement: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rspec
83
+ requirement: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Babysits long-running processes and reports progress or failures
98
+ email:
99
+ - noverloop@gmail.com
100
+ - paul.grayson@lonelyplanet.com
101
+ - coder@onesandthrees.com
102
+ - michael@guerillatactics.co.uk
103
+ executables: []
104
+ extensions: []
105
+ extra_rdoc_files: []
106
+ files:
107
+ - .gitignore
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - babysitter.gemspec
113
+ - lib/babysitter.rb
114
+ - lib/babysitter/configuration.rb
115
+ - lib/babysitter/counter.rb
116
+ - lib/babysitter/exception_notifiers.rb
117
+ - lib/babysitter/exception_notifiers/simple_notification_service.rb
118
+ - lib/babysitter/logger_with_stats.rb
119
+ - lib/babysitter/logging.rb
120
+ - lib/babysitter/monitor.rb
121
+ - lib/babysitter/null_logger.rb
122
+ - lib/babysitter/tracker.rb
123
+ - lib/babysitter/version.rb
124
+ - spec/lib/babysitter/configuration_spec.rb
125
+ - spec/lib/babysitter/exception_notifiers/simple_notification_service_spec.rb
126
+ - spec/lib/babysitter/monitor_spec.rb
127
+ - spec/lib/babysitter/tracker_spec.rb
128
+ - spec/lib/babysitter_spec.rb
129
+ - spec/spec_helper.rb
130
+ homepage: ''
131
+ licenses: []
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ! '>='
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 1.8.24
151
+ signing_key:
152
+ specification_version: 3
153
+ summary: Babysits long-running processes and reports progress or failures
154
+ test_files:
155
+ - spec/lib/babysitter/configuration_spec.rb
156
+ - spec/lib/babysitter/exception_notifiers/simple_notification_service_spec.rb
157
+ - spec/lib/babysitter/monitor_spec.rb
158
+ - spec/lib/babysitter/tracker_spec.rb
159
+ - spec/lib/babysitter_spec.rb
160
+ - spec/spec_helper.rb