kicks 3.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +24 -0
  3. data/.gitignore +12 -0
  4. data/ChangeLog.md +142 -0
  5. data/Dockerfile +24 -0
  6. data/Dockerfile.slim +20 -0
  7. data/Gemfile +8 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +209 -0
  11. data/Rakefile +12 -0
  12. data/bin/sneakers +6 -0
  13. data/docker-compose.yml +24 -0
  14. data/examples/benchmark_worker.rb +22 -0
  15. data/examples/max_retry_handler.rb +68 -0
  16. data/examples/metrics_worker.rb +34 -0
  17. data/examples/middleware_worker.rb +36 -0
  18. data/examples/newrelic_metrics_worker.rb +40 -0
  19. data/examples/profiling_worker.rb +69 -0
  20. data/examples/sneakers.conf.rb.example +11 -0
  21. data/examples/title_scraper.rb +36 -0
  22. data/examples/workflow_worker.rb +23 -0
  23. data/kicks.gemspec +44 -0
  24. data/lib/sneakers/cli.rb +122 -0
  25. data/lib/sneakers/concerns/logging.rb +34 -0
  26. data/lib/sneakers/concerns/metrics.rb +34 -0
  27. data/lib/sneakers/configuration.rb +125 -0
  28. data/lib/sneakers/content_encoding.rb +47 -0
  29. data/lib/sneakers/content_type.rb +47 -0
  30. data/lib/sneakers/error_reporter.rb +33 -0
  31. data/lib/sneakers/errors.rb +2 -0
  32. data/lib/sneakers/handlers/maxretry.rb +219 -0
  33. data/lib/sneakers/handlers/oneshot.rb +26 -0
  34. data/lib/sneakers/metrics/logging_metrics.rb +16 -0
  35. data/lib/sneakers/metrics/newrelic_metrics.rb +32 -0
  36. data/lib/sneakers/metrics/null_metrics.rb +13 -0
  37. data/lib/sneakers/metrics/statsd_metrics.rb +21 -0
  38. data/lib/sneakers/middleware/config.rb +23 -0
  39. data/lib/sneakers/publisher.rb +49 -0
  40. data/lib/sneakers/queue.rb +87 -0
  41. data/lib/sneakers/runner.rb +91 -0
  42. data/lib/sneakers/spawner.rb +30 -0
  43. data/lib/sneakers/support/production_formatter.rb +11 -0
  44. data/lib/sneakers/support/utils.rb +18 -0
  45. data/lib/sneakers/tasks.rb +66 -0
  46. data/lib/sneakers/version.rb +3 -0
  47. data/lib/sneakers/worker.rb +162 -0
  48. data/lib/sneakers/workergroup.rb +60 -0
  49. data/lib/sneakers.rb +125 -0
  50. data/log/.gitkeep +0 -0
  51. data/scripts/local_integration +2 -0
  52. data/scripts/local_worker +3 -0
  53. data/spec/fixtures/integration_worker.rb +18 -0
  54. data/spec/fixtures/require_worker.rb +23 -0
  55. data/spec/gzip_helper.rb +15 -0
  56. data/spec/sneakers/cli_spec.rb +75 -0
  57. data/spec/sneakers/concerns/logging_spec.rb +39 -0
  58. data/spec/sneakers/concerns/metrics_spec.rb +38 -0
  59. data/spec/sneakers/configuration_spec.rb +97 -0
  60. data/spec/sneakers/content_encoding_spec.rb +81 -0
  61. data/spec/sneakers/content_type_spec.rb +81 -0
  62. data/spec/sneakers/integration_spec.rb +158 -0
  63. data/spec/sneakers/publisher_spec.rb +179 -0
  64. data/spec/sneakers/queue_spec.rb +169 -0
  65. data/spec/sneakers/runner_spec.rb +70 -0
  66. data/spec/sneakers/sneakers_spec.rb +77 -0
  67. data/spec/sneakers/support/utils_spec.rb +44 -0
  68. data/spec/sneakers/tasks/sneakers_run_spec.rb +115 -0
  69. data/spec/sneakers/worker_handlers_spec.rb +469 -0
  70. data/spec/sneakers/worker_spec.rb +712 -0
  71. data/spec/sneakers/workergroup_spec.rb +83 -0
  72. data/spec/spec_helper.rb +21 -0
  73. metadata +352 -0
@@ -0,0 +1,66 @@
1
+ require 'sneakers'
2
+ require 'sneakers/runner'
3
+
4
+ task :environment
5
+
6
+ namespace :sneakers do
7
+ desc "Start work (set $WORKERS=Klass1,Klass2)"
8
+ task :run do
9
+ Sneakers.server = true
10
+ Rake::Task['environment'].invoke
11
+
12
+ if defined?(::Rails)
13
+ if Rails.autoloaders.zeitwerk_enabled?
14
+ ::Zeitwerk::Loader.eager_load_all
15
+ else
16
+ ::Rails.application.eager_load!
17
+ end
18
+ end
19
+
20
+ workers, missing_workers = get_worker_classes
21
+
22
+ unless missing_workers.nil? || missing_workers.empty?
23
+ puts "Missing workers: #{missing_workers.join(', ')}" if missing_workers
24
+ puts "Did you `require` properly?"
25
+ exit(1)
26
+ end
27
+
28
+ if workers.empty?
29
+ puts <<EOF
30
+ Error: No workers found.
31
+ Please set the classes of the workers you want to run like so:
32
+
33
+ $ export WORKERS=MyWorker,FooWorker
34
+ $ rake sneakers:run
35
+
36
+ You can also configure them with
37
+ $ Sneakers.rake_worker_classes
38
+
39
+ If you use something that responds to :call it will execute that
40
+
41
+ Eventually, if nothing before applied, every class is used where you directly included the Sneakers::Worker
42
+ EOF
43
+ exit(1)
44
+ end
45
+ opts = (!ENV['WORKER_COUNT'].nil? ? {:workers => ENV['WORKER_COUNT'].to_i} : {})
46
+ r = Sneakers::Runner.new(workers, opts)
47
+
48
+ r.run
49
+ end
50
+
51
+ private
52
+
53
+ def get_worker_classes
54
+ if ENV["WORKERS"]
55
+ Sneakers::Utils.parse_workers(ENV['WORKERS'])
56
+ elsif Sneakers.rake_worker_classes
57
+ if Sneakers.rake_worker_classes.respond_to?(:call)
58
+ [Sneakers.rake_worker_classes.call]
59
+ else
60
+ [Sneakers.rake_worker_classes]
61
+ end
62
+ else
63
+ [Sneakers::Worker::Classes]
64
+ end || [[]]
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ module Sneakers
2
+ VERSION = "3.0.0.pre"
3
+ end
@@ -0,0 +1,162 @@
1
+ require 'sneakers/queue'
2
+ require 'sneakers/support/utils'
3
+
4
+ module Sneakers
5
+ module Worker
6
+ attr_reader :queue, :id, :opts
7
+
8
+ # For now, a worker is hardly dependant on these concerns
9
+ # (because it uses methods from them directly.)
10
+ include Concerns::Logging
11
+ include Concerns::Metrics
12
+ include Sneakers::ErrorReporter
13
+
14
+ def initialize(queue = nil, pool = nil, opts = {})
15
+ opts = opts.merge(self.class.queue_opts || {})
16
+ queue_name = self.class.queue_name
17
+ opts = Sneakers::CONFIG.merge(opts)
18
+
19
+ @should_ack = opts[:ack]
20
+ @pool = pool || Concurrent::FixedThreadPool.new(opts[:threads] || Sneakers::Configuration::DEFAULTS[:threads])
21
+ @call_with_params = respond_to?(:work_with_params)
22
+ @content_type = opts[:content_type]
23
+ @content_encoding = opts[:content_encoding]
24
+
25
+ @queue = queue || Sneakers::Queue.new(
26
+ queue_name,
27
+ opts
28
+ )
29
+
30
+ @opts = opts
31
+ @id = Utils.make_worker_id(queue_name)
32
+ end
33
+
34
+ def ack!; :ack end
35
+ def reject!; :reject; end
36
+ def requeue!; :requeue; end
37
+
38
+ def publish(msg, opts)
39
+ to_queue = opts.delete(:to_queue)
40
+ opts[:routing_key] ||= to_queue
41
+ return unless opts[:routing_key]
42
+ serialized_msg = Sneakers::ContentType.serialize(msg, opts[:content_type])
43
+ encoded_msg = Sneakers::ContentEncoding.encode(serialized_msg, opts[:content_encoding])
44
+ @queue.exchange.publish(encoded_msg, **opts)
45
+ end
46
+
47
+ def do_work(delivery_info, metadata, msg, handler)
48
+ worker_trace "Working off: #{msg.inspect}"
49
+
50
+ @pool.post do
51
+ process_work(delivery_info, metadata, msg, handler)
52
+ end
53
+ end
54
+
55
+ def process_work(delivery_info, metadata, msg, handler)
56
+ res = nil
57
+ error = nil
58
+
59
+ begin
60
+ metrics.increment("work.#{self.class.name}.started")
61
+ metrics.timing("work.#{self.class.name}.time") do
62
+ decoded_msg = ContentEncoding.decode(msg, @content_encoding || metadata && metadata[:content_encoding])
63
+ deserialized_msg = ContentType.deserialize(decoded_msg, @content_type || metadata && metadata[:content_type])
64
+
65
+ app = -> (deserialized_msg, delivery_info, metadata, handler) do
66
+ if @call_with_params
67
+ work_with_params(deserialized_msg, delivery_info, metadata)
68
+ else
69
+ work(deserialized_msg)
70
+ end
71
+ end
72
+
73
+ middlewares = Sneakers.middleware.to_a
74
+ block_to_call = middlewares.reverse.reduce(app) do |mem, h|
75
+ h[:class].new(mem, *h[:args])
76
+ end
77
+ res = block_to_call.call(deserialized_msg, delivery_info, metadata, handler)
78
+ end
79
+ rescue SignalException, SystemExit
80
+ # ServerEngine handles these exceptions, so they are not expected to be raised within the worker.
81
+ # Nevertheless, they are listed here to ensure that they are not caught by the rescue block below.
82
+ raise
83
+ rescue Exception => ex
84
+ res = :error
85
+ error = ex
86
+ worker_error(ex, log_msg: log_msg(msg), class: self.class.name,
87
+ message: msg, delivery_info: delivery_info, metadata: metadata)
88
+ ensure
89
+ if @should_ack
90
+ case res
91
+ # note to future-self. never acknowledge multiple (multiple=true) messages under threads.
92
+ when :ack then handler.acknowledge(delivery_info, metadata, msg)
93
+ when :error then handler.error(delivery_info, metadata, msg, error)
94
+ when :reject then handler.reject(delivery_info, metadata, msg)
95
+ when :requeue then handler.reject(delivery_info, metadata, msg, true)
96
+ else
97
+ handler.noop(delivery_info, metadata, msg)
98
+ end
99
+ metrics.increment("work.#{self.class.name}.handled.#{res || 'noop'}")
100
+ end
101
+
102
+ metrics.increment("work.#{self.class.name}.ended")
103
+ end
104
+ end
105
+
106
+ def stop
107
+ worker_trace "Stopping worker: unsubscribing."
108
+ @queue.unsubscribe
109
+ worker_trace "Stopping worker: shutting down thread pool."
110
+ @pool.shutdown
111
+ @pool.wait_for_termination
112
+ worker_trace "Stopping worker: I'm gone."
113
+ end
114
+
115
+ def run
116
+ worker_trace "New worker: subscribing."
117
+ @queue.subscribe(self)
118
+ worker_trace "New worker: I'm alive."
119
+ end
120
+
121
+ # Construct a log message with some standard prefix for this worker
122
+ def log_msg(msg)
123
+ "[#{@id}][#{Thread.current}][#{@queue.name}][#{@queue.opts}] #{msg}"
124
+ end
125
+
126
+ def worker_trace(msg)
127
+ logger.debug(log_msg(msg))
128
+ end
129
+
130
+ Classes = []
131
+
132
+ def self.included(base)
133
+ base.extend ClassMethods
134
+ Classes << base if base.is_a? Class
135
+ end
136
+
137
+ module ClassMethods
138
+ attr_reader :queue_opts
139
+ attr_reader :queue_name
140
+
141
+ def from_queue(q, opts={})
142
+ @queue_name = q.to_s
143
+ @queue_opts = opts
144
+ end
145
+
146
+ def enqueue(msg, opts={})
147
+ opts[:routing_key] ||= @queue_opts[:routing_key]
148
+ opts[:content_type] ||= @queue_opts[:content_type]
149
+ opts[:content_encoding] ||= @queue_opts[:content_encoding]
150
+ opts[:to_queue] ||= @queue_name
151
+
152
+ publisher.publish(msg, opts)
153
+ end
154
+
155
+ private
156
+
157
+ def publisher
158
+ @publisher ||= Sneakers::Publisher.new(queue_opts)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,60 @@
1
+ module Sneakers
2
+ module WorkerGroup
3
+ @workers = []
4
+
5
+ def initialize
6
+ @stop_flag = ServerEngine::BlockingFlag.new
7
+ end
8
+
9
+ def before_fork
10
+ fbefore = Sneakers::CONFIG[:hooks][:before_fork]
11
+ fbefore.call if fbefore
12
+ end
13
+
14
+ def after_fork # note! this is not Serverengine#after_start, this is ours!
15
+ fafter = Sneakers::CONFIG[:hooks][:after_fork]
16
+ fafter.call if fafter
17
+ end
18
+
19
+ def run
20
+ after_fork
21
+
22
+ # Allocate single thread pool if share_threads is set. This improves load balancing
23
+ # when used with many workers.
24
+ pool = config[:share_threads] ? Concurrent::FixedThreadPool.new(config[:threads]) : nil
25
+
26
+ worker_classes = config[:worker_classes]
27
+
28
+ if worker_classes.respond_to? :call
29
+ worker_classes = worker_classes.call
30
+ end
31
+
32
+ # If we don't provide a connection to a worker,
33
+ # the queue used in the worker will create a new one
34
+
35
+ @workers = worker_classes.map do |worker_class|
36
+ worker_class.new(nil, pool, { connection: config[:connection] })
37
+ end
38
+
39
+ # if more than one worker this should be per worker
40
+ # accumulate clients and consumers as well
41
+ @workers.each do |worker|
42
+ worker.run
43
+ end
44
+ # end per worker
45
+ #
46
+ until @stop_flag.wait_for_set(Sneakers::CONFIG[:amqp_heartbeat])
47
+ Sneakers.logger.debug("Heartbeat: running threads [#{Thread.list.count}]")
48
+ # report aggregated stats?
49
+ end
50
+ end
51
+
52
+ def stop
53
+ Sneakers.logger.info("Shutting down workers")
54
+ @workers.each do |worker|
55
+ worker.stop
56
+ end
57
+ @stop_flag.set!
58
+ end
59
+ end
60
+ end
data/lib/sneakers.rb ADDED
@@ -0,0 +1,125 @@
1
+ require 'sneakers/version'
2
+ require 'concurrent/executors'
3
+ require 'bunny'
4
+ require 'logger'
5
+ require 'serverengine'
6
+
7
+ module Sneakers
8
+ module Handlers
9
+ end
10
+ module Concerns
11
+ end
12
+ end
13
+
14
+ require 'sneakers/configuration'
15
+ require 'sneakers/errors'
16
+ require 'sneakers/support/production_formatter'
17
+ require 'sneakers/concerns/logging'
18
+ require 'sneakers/concerns/metrics'
19
+ require 'sneakers/handlers/oneshot'
20
+ require 'sneakers/content_type'
21
+ require 'sneakers/content_encoding'
22
+ require 'sneakers/middleware/config'
23
+ require 'sneakers/worker'
24
+ require 'sneakers/publisher'
25
+
26
+ module Sneakers
27
+ extend self
28
+
29
+ CONFIG = Configuration.new
30
+
31
+ def configure(opts={})
32
+ # worker > userland > defaults
33
+ CONFIG.merge!(opts)
34
+ setup_general_logger!
35
+ setup_worker_concerns!
36
+ setup_general_publisher!
37
+ @configured = true
38
+ end
39
+
40
+ def clear!
41
+ CONFIG.clear
42
+ @logger = nil
43
+ @publisher = nil
44
+ @configured = false
45
+ end
46
+
47
+ def daemonize!(loglevel=Logger::INFO)
48
+ CONFIG[:log] = 'sneakers.log'
49
+ CONFIG[:daemonize] = true
50
+ setup_general_logger!
51
+ logger.level = loglevel
52
+ end
53
+
54
+ def rake_worker_classes=(worker_classes)
55
+ @rake_worker_classes = worker_classes
56
+ end
57
+
58
+ def rake_worker_classes
59
+ @rake_worker_classes
60
+ end
61
+
62
+ def logger=(logger)
63
+ @logger = logger
64
+ end
65
+
66
+ def logger
67
+ @logger
68
+ end
69
+
70
+ def publish(msg, routing)
71
+ @publisher.publish(msg, routing)
72
+ end
73
+
74
+ def configured?
75
+ @configured
76
+ end
77
+
78
+ def server=(server)
79
+ @server = server
80
+ end
81
+
82
+ def server?
83
+ @server
84
+ end
85
+
86
+ def configure_server
87
+ yield self if server?
88
+ end
89
+
90
+ # Register a proc to handle any error which occurs within the Sneakers process.
91
+ #
92
+ # Sneakers.error_reporters << proc { |exception, worker, context_hash| MyErrorService.notify(exception, context_hash) }
93
+ #
94
+ # The default error handler logs errors to Sneakers.logger.
95
+ # Ripped off from https://github.com/mperham/sidekiq/blob/6ad6a3aa330deebd76c6cf0d353f66abd3bef93b/lib/sidekiq.rb#L165-L174
96
+ def error_reporters
97
+ CONFIG[:error_reporters]
98
+ end
99
+
100
+ def middleware
101
+ @middleware ||= Sneakers::Middleware::Config
102
+ end
103
+
104
+ private
105
+
106
+ def setup_general_logger!
107
+ if [:info, :debug, :error, :warn].all?{ |meth| CONFIG[:log].respond_to?(meth) }
108
+ @logger = CONFIG[:log]
109
+ else
110
+ @logger = ServerEngine::DaemonLogger.new(CONFIG[:log])
111
+ @logger.formatter = Sneakers::Support::ProductionFormatter
112
+ end
113
+ end
114
+
115
+ def setup_worker_concerns!
116
+ Worker.configure_logger(Sneakers::logger)
117
+ Worker.configure_metrics(CONFIG[:metrics])
118
+ CONFIG[:handler] ||= Sneakers::Handlers::Oneshot
119
+ end
120
+
121
+ def setup_general_publisher!
122
+ @publisher = Sneakers::Publisher.new
123
+ end
124
+ end
125
+
data/log/.gitkeep ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ docker-compose run -e INTEGRATION=1 -e INTEGRATION_LOG=1 sneakers rake test
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ docker-compose run sneakers sneakers work TitleScraper --require examples/title_scraper.rb
3
+
@@ -0,0 +1,18 @@
1
+ require 'sneakers'
2
+ require 'thread'
3
+ require 'redis'
4
+
5
+
6
+ redis_addr = compose_or_localhost("redis")
7
+ puts "REDIS is at #{redis_addr}"
8
+ $redis = Redis.new(:host => redis_addr)
9
+
10
+ class IntegrationWorker
11
+ include Sneakers::Worker
12
+
13
+ def work(msg)
14
+ $redis.incr(self.class.queue_name)
15
+ ack!
16
+ end
17
+ end
18
+
@@ -0,0 +1,23 @@
1
+ require 'sneakers'
2
+ require 'open-uri'
3
+
4
+
5
+ class TitleScraper
6
+ include Sneakers::Worker
7
+
8
+ from_queue 'downloads'
9
+
10
+ def work(msg)
11
+ title = extract_title(open(msg))
12
+ logger.info "FOUND <#{title}>"
13
+ ack!
14
+ end
15
+
16
+ private
17
+
18
+ def extract_title(html)
19
+ html =~ /<title>(.*?)<\/title>/
20
+ $1
21
+ end
22
+ end
23
+
@@ -0,0 +1,15 @@
1
+ require 'stringio'
2
+ require 'zlib'
3
+
4
+ # Simple gzip encoder/decoder for testing
5
+ def gzip_compress(s)
6
+ io = StringIO.new('w')
7
+ w = Zlib::GzipWriter.new(io)
8
+ w.write(s)
9
+ w.close
10
+ io.string
11
+ end
12
+
13
+ def gzip_decompress(s)
14
+ Zlib::GzipReader.new(StringIO.new(s, 'rb')).read
15
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'sneakers/cli'
4
+ require 'sneakers/runner'
5
+
6
+ describe Sneakers::CLI do
7
+ describe "#work" do
8
+ before do
9
+ any_instance_of(Sneakers::Runner) do |runner|
10
+ stub(runner).run{ true }
11
+ end
12
+ end
13
+
14
+ after do
15
+ # require cleanup
16
+ Object.send(:remove_const, :TitleScraper) if Object.constants.include?(:TitleScraper)
17
+ end
18
+
19
+ describe 'with dirty class loading' do
20
+ it "should perform a run" do
21
+ any_instance_of(Sneakers::Runner) do |runner|
22
+ mock(runner).run{ true }
23
+ end
24
+ out = capture_io{ Sneakers::CLI.start [
25
+ 'work',
26
+ "TitleScraper",
27
+ "--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
28
+ ]}.join ''
29
+
30
+ _(out).must_match(/Workers.*:.*TitleScraper.*/)
31
+
32
+ end
33
+
34
+ it "should be able to run as front-running process" do
35
+ out = capture_io{ Sneakers::CLI.start [
36
+ 'work',
37
+ "TitleScraper",
38
+ "--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
39
+ ]}.join ''
40
+
41
+ _(out).must_match(/Log.*Console/)
42
+ end
43
+
44
+ it "should be able to run as daemonized process" do
45
+ out = capture_io{ Sneakers::CLI.start [
46
+ 'work',
47
+ "TitleScraper",
48
+ "--daemonize",
49
+ "--require=#{File.expand_path('../fixtures/require_worker.rb', File.dirname(__FILE__))}"
50
+ ]}.join ''
51
+
52
+ _(out).must_match(/sneakers.log/)
53
+ end
54
+ end
55
+
56
+ it "should fail when no workers found" do
57
+ out = capture_io{ Sneakers::CLI.start ['work', 'TitleScraper'] }.join ''
58
+ _(out).must_match(/Missing workers: TitleScraper/)
59
+ end
60
+
61
+ it "should run all workers when run without specifying any" do
62
+ out = capture_io{ Sneakers::CLI.start [
63
+ "work",
64
+ "--require=#{File.expand_path("../fixtures/require_worker.rb", File.dirname(__FILE__))}"
65
+ ]}.join ''
66
+
67
+ _(out).must_match(/Workers.*:.*TitleScraper.*/)
68
+ end
69
+
70
+ after do
71
+ # require cleanup
72
+ Object.send(:remove_const, :TitleScraper) if Object.constants.include?(:TitleScraper)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'logger'
4
+
5
+
6
+ class Foobar
7
+ include Sneakers::Concerns::Logging
8
+ end
9
+
10
+ describe Sneakers::Concerns::Logging do
11
+ describe ".configure" do
12
+ before do
13
+ Foobar.logger = nil
14
+ end
15
+
16
+ it "should configure a default logger when included" do
17
+ _(Foobar.logger).must_be_nil
18
+ Foobar.configure_logger
19
+ _(Foobar.logger).wont_be_nil
20
+ _(Foobar.logger.formatter).must_equal Sneakers::Support::ProductionFormatter
21
+ end
22
+
23
+ it "should supply accessible instance logger" do
24
+ _(Foobar.logger).must_be_nil
25
+ Foobar.configure_logger
26
+ f = Foobar.new
27
+ _(f.logger).must_equal Foobar.logger
28
+ _(f.logger).wont_be_nil
29
+ end
30
+
31
+ it "should configure a given logger when specified" do
32
+ _(Foobar.logger).must_be_nil
33
+ log = Logger.new(STDOUT)
34
+ Foobar.configure_logger(log)
35
+ _(Foobar.logger).must_equal log
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'sneakers'
3
+ require 'logger'
4
+
5
+
6
+ class Foometrics
7
+ include Sneakers::Concerns::Metrics
8
+ end
9
+
10
+ describe Sneakers::Concerns::Metrics do
11
+ describe ".configure" do
12
+ before do
13
+ Foometrics.metrics = nil
14
+ end
15
+
16
+ it "should configure a default logger when included" do
17
+ _(Foometrics.metrics).must_be_nil
18
+ Foometrics.configure_metrics
19
+ _(Foometrics.metrics).wont_be_nil
20
+ end
21
+
22
+ it "should supply accessible instance logger" do
23
+ _(Foometrics.metrics).must_be_nil
24
+ Foometrics.configure_metrics
25
+ f = Foometrics.new
26
+ _(f.metrics).must_equal Foometrics.metrics
27
+ _(f.metrics).wont_be_nil
28
+ end
29
+
30
+ it "should configure a given metrics when specified" do
31
+ _(Foometrics.metrics).must_be_nil
32
+ o = Object.new
33
+ Foometrics.configure_metrics(o)
34
+ _(Foometrics.metrics).must_equal o
35
+ end
36
+ end
37
+ end
38
+