babysitter 0.0.1

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.
@@ -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