harness 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/README.md +152 -2
  2. data/Rakefile +8 -0
  3. data/harness.gemspec +7 -0
  4. data/lib/harness/adapters/librato_adapter.rb +68 -0
  5. data/lib/harness/adapters/memory_adapter.rb +19 -0
  6. data/lib/harness/adapters/null_adapter.rb +11 -0
  7. data/lib/harness/consumer.rb +47 -0
  8. data/lib/harness/counter.rb +27 -0
  9. data/lib/harness/gauge.rb +21 -0
  10. data/lib/harness/instrumentation.rb +6 -0
  11. data/lib/harness/integration/action_controller.rb +9 -0
  12. data/lib/harness/integration/action_mailer.rb +10 -0
  13. data/lib/harness/integration/action_view.rb +10 -0
  14. data/lib/harness/integration/active_support.rb +9 -0
  15. data/lib/harness/measurement.rb +17 -0
  16. data/lib/harness/railtie.rb +36 -0
  17. data/lib/harness/tasks.rake +6 -0
  18. data/lib/harness/version.rb +1 -1
  19. data/lib/harness.rb +104 -1
  20. data/test/integration/counters_with_redis_test.rb +69 -0
  21. data/test/integration/instrumentation_test.rb +28 -0
  22. data/test/integration/integrations/action_controller_test.rb +51 -0
  23. data/test/integration/integrations/action_mailer_test.rb +22 -0
  24. data/test/integration/integrations/action_view_test.rb +22 -0
  25. data/test/integration/integrations/active_support_test.rb +40 -0
  26. data/test/integration/logging_test.rb +17 -0
  27. data/test/test_helper.rb +54 -0
  28. data/test/unit/adapters/librato_adapter_test.rb +176 -0
  29. data/test/unit/adapters/memory_adapter_test.rb +22 -0
  30. data/test/unit/counter_test.rb +55 -0
  31. data/test/unit/gauge_test.rb +53 -0
  32. data/test/unit/harness_test.rb +27 -0
  33. data/test/unit/measurement_test.rb +36 -0
  34. metadata +101 -10
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Harness
2
2
 
3
- TODO: Write a gem description
3
+ Harness connects measurements coming from `ActiveSupport::Notifications`
4
+ to external metric tracking services. Counters are stored locally with
5
+ redis before being sent to the service.
6
+
7
+ Currently Supported Services:
8
+
9
+ * Librato
4
10
 
5
11
  ## Installation
6
12
 
@@ -18,7 +24,151 @@ Or install it yourself as:
18
24
 
19
25
  ## Usage
20
26
 
21
- TODO: Write usage instructions here
27
+ In the metrics world there are two types of things: Gauges and Counters.
28
+ Gauges are time senstive and represent something at a specific point in
29
+ time. Counters keep track of things and should be increasing. Counters
30
+ can be reset back to zero. You can combine counters and/or gauges to
31
+ correlate data about your application.
32
+
33
+ Harness makes this process easily. Harness' primary goal it make it dead
34
+ simple to start measuring different parts of your application.
35
+ `ActiveSupport::Notifications` makes this very easy because it provides
36
+ measurements and implements the observer pattern.
37
+
38
+ ## Tracking Things
39
+
40
+ I guess you read the `ActiveSupport::Notifications`
41
+ [documentation](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html)
42
+ before going any further or this will seems like php to you. Harness
43
+ hooks into your notifications and looks for `:gauge` or `:counter`
44
+ options. If either is present, it will be sent to the external service.
45
+ For example, you can track how long it's taking to do a specific thing:
46
+
47
+ ```ruby
48
+ class MyClass
49
+ def important_method(stuff)
50
+ ActiveSupport::Notifications.instrument "important_method.my_class", :gauge => true do
51
+ do_important_stuff
52
+ end
53
+ end
54
+ end
55
+ ```
56
+
57
+ You can do the same with a counter. Counter values are automatically
58
+ stored in redis and incremented. This means you can simply pass
59
+ `:counter => true` in instrumentations if you'd like to count it. You
60
+ may also pass `:counter => 5` if you'd like to provide your own value.
61
+ This value is stored in redis so the next time `:counter => true` will
62
+ work correctly. You can reset all the counters back to zero by calling:
63
+ `Harness.reset_counters!`.
64
+
65
+ ```ruby
66
+ class MyClass
67
+ def important_method(stuff)
68
+ ActiveSupport::Notifications.instrument "important_method.my_class", :counter => true do
69
+ do_important_stuff
70
+ end
71
+ end
72
+ end
73
+ ```
74
+
75
+ The instuments name will be sent as the name (`important_method.my_class`)
76
+ for that gauge or counter.
77
+
78
+ Harness will do all the extra work in sending these metrics to whatever
79
+ service you're using.
80
+
81
+ ## Customzing
82
+
83
+ You can pash a hash to `:counter` or `:gauge` to initialize the
84
+ measurement your own way.
85
+
86
+ ```ruby
87
+ class MyClass
88
+ def important_method(stuff)
89
+ ActiveSupport::Notifications.instrument "important_method.my_class", :gauge => { :id => 'custom-id', :name => "My Measurement" } do
90
+ do_important_stuff
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## One Off Gauges and Counters
97
+
98
+ You can instantiate `Harness::Counter` and `Harness::Guage` wherever you
99
+ want. Events from `ActiveSupport` are just converted to these classes
100
+ under the covers anyways. You can use these class if you want to take
101
+ peridocial measurements or tracking something that happens outside the
102
+ application.
103
+
104
+ ```ruby
105
+ gauge = Harness::Gauge.new
106
+ gauge.id = "foo.bar"
107
+ gauge.name = "Foo's Bar"
108
+ gauge.time # defaults to Time.now
109
+ gauge.value = readings_from_my_server
110
+ gauge.units = 'bytes'
111
+ gauge.log
112
+
113
+ counter = Harness::Counter.new
114
+ counter.id = "foo.bar"
115
+ counter.name = "# of Foo bars"
116
+ counter.time # defaults to Time.now
117
+ counter.value = read_total_users_in_database
118
+ counter.log
119
+
120
+ # Both class take an option hash
121
+
122
+ gauge = Harness::Guage.new :time => Time.now, :id => 'foo.bar'
123
+ counter = Harness::Counter.new :time => Time.now, :id => 'foo.bar'
124
+ ```
125
+
126
+ ## Configuration
127
+
128
+ ```ruby
129
+ Harness.config.adapter = :librato
130
+
131
+ Harness.config.librato.email = 'example@example.com'
132
+ Harness.config.librato.token = 'your-api-key'
133
+
134
+ Harness.redis = Redis.new
135
+ ```
136
+
137
+ ## Rails Integration
138
+
139
+ Harness will automatically log metrics coming from `ActionPack`,
140
+ `ActiveRecord`, `ActiveSupport` and `ActionMailer`.
141
+
142
+ You can configure Harness from `application.rb`
143
+
144
+ ```ruby
145
+ config.harness.adapter = :librato
146
+ config.librato.email = 'example@example.com'
147
+ config.librato.token = 'your-api-key'
148
+ ```
149
+
150
+ Redis will be automatically configured if you `REDISTOGO_URL` or
151
+ `REDIS_URL` environment variables at set. They are wrapped in a
152
+ namespace so there will be no conflicts. If they are not present, the
153
+ default values are used. You can customize this in an initializer:
154
+
155
+ ```ruby
156
+ # config/initializers/harness.rb
157
+ require 'erb'
158
+
159
+ file = Rails.root.join 'config', 'resque.yml'
160
+ config = YAML.load(ERB.new(File.read(Rails.root.join('config', 'resque.yml'))).result)
161
+
162
+ Harness.redis = Redis.new(:url => config[Rails.env])
163
+ ```
164
+
165
+ `rake harness:reset_counters` is also added.
166
+
167
+ ### Rails Environments
168
+
169
+ Measurements are completely ignored in the test env. They are processed
170
+ in development mode, but not sent to the external service. Everything is
171
+ logged in production.
22
172
 
23
173
  ## Contributing
24
174
 
data/Rakefile CHANGED
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'test'
7
+ test.pattern = 'test/**/*_test.rb'
8
+ end
9
+
10
+ task :default => :test
data/harness.gemspec CHANGED
@@ -14,4 +14,11 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "harness"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Harness::VERSION
17
+
18
+ gem.add_dependency "activesupport", "~> 3"
19
+ gem.add_dependency "redis"
20
+ gem.add_dependency "redis-namespace"
21
+
22
+ gem.add_development_dependency "simplecov"
23
+ gem.add_development_dependency "webmock"
17
24
  end
@@ -0,0 +1,68 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'net/http'
4
+
5
+ module Harness
6
+ class LibratoAdapter
7
+ class Config
8
+ attr_accessor :email, :token
9
+ end
10
+
11
+ def self.config
12
+ @config ||= Config.new
13
+ end
14
+
15
+ def self.log_gauge(gauge)
16
+ raise Harness::LoggingError if gauge.id.length > 63
17
+
18
+ post({:gauges => [{
19
+ :name => sanitize(gauge.id),
20
+ :display_name => gauge.name,
21
+ :value => gauge.value,
22
+ :measure_time => gauge.time.to_i,
23
+ :source => gauge.source,
24
+ :attributes => { :display_units_short => gauge.units }
25
+ }]})
26
+ end
27
+
28
+ def self.log_counter(counter)
29
+ raise Harness::LoggingError if counter.id.length > 63
30
+
31
+ post({:counters => [{
32
+ :name => sanitize(counter.id),
33
+ :display_name => counter.name,
34
+ :value => counter.value,
35
+ :measure_time => counter.time.to_i,
36
+ :source => counter.source,
37
+ :attributes => { :display_units_short => counter.units }
38
+ }]})
39
+ end
40
+
41
+ private
42
+ def self.post(params)
43
+ unless config.email && config.token
44
+ raise "Adapter not configured. Ensure email and token are set."
45
+ end
46
+
47
+ uri = URI.parse('https://metrics-api.librato.com/v1/metrics')
48
+
49
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
50
+ request = Net::HTTP::Post.new uri.request_uri
51
+
52
+ request.basic_auth config.email, config.token
53
+ request['Content-Type'] = 'application/json'
54
+ request.body = params.to_json
55
+
56
+ response = http.request request
57
+
58
+ raise Harness::LoggingError, response.body unless response.code.to_i == 200
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ def self.sanitize(name)
65
+ name.gsub(/\./, '-')
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,19 @@
1
+ module Harness
2
+ class MemoryAdapter
3
+ def self.gauges
4
+ @gauges ||= []
5
+ end
6
+
7
+ def self.counters
8
+ @counters ||= []
9
+ end
10
+
11
+ def self.log_gauge(gauge)
12
+ gauges << gauge
13
+ end
14
+
15
+ def self.log_counter(counter)
16
+ counters << counter
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module Harness
2
+ class NullAdapter
3
+ def self.log_gauge(gauge)
4
+
5
+ end
6
+
7
+ def self.log_counter(counter)
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,47 @@
1
+ module Harness
2
+ class Consumer
3
+ def consume
4
+ Thread.new do
5
+ while measurement = queue.pop
6
+ begin
7
+ logger.debug "[Harness] Processing Measurement: #{measurement.inspect}"
8
+
9
+ case measurement.class.to_s.demodulize.underscore.to_sym
10
+ when :gauge
11
+ adapter.log_gauge measurement
12
+ when :counter
13
+ adapter.log_counter measurement
14
+ end
15
+ rescue LoggingError => ex
16
+ logger.debug "[Harness] Logging measurement failed! Server Said: #{ex}"
17
+ logger.debug ex.backtrace.join("\n")
18
+ logger.warn "[Harness] Could not post measurement! Enable debug logging to see full errors"
19
+ ensure
20
+ mutex.synchronize { @finished = queue.empty? }
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def finished?
27
+ @finished
28
+ end
29
+
30
+ private
31
+ def queue
32
+ Harness.queue
33
+ end
34
+
35
+ def adapter
36
+ Harness.config.adapter
37
+ end
38
+
39
+ def mutex
40
+ Harness.mutex
41
+ end
42
+
43
+ def logger
44
+ Harness.logger
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ module Harness
2
+ class Counter < Measurement
3
+ def self.from_event(event)
4
+ if event.payload[:counter].is_a? Hash
5
+ counter = new event.payload[:counter]
6
+ else
7
+ counter = new
8
+ end
9
+
10
+ counter.id ||= event.name
11
+
12
+ Harness.redis.sadd 'counters', counter.id
13
+
14
+ if event.payload[:counter].is_a? Fixnum
15
+ counter.value = event.payload[:counter]
16
+ end
17
+
18
+ if counter.value
19
+ Harness.redis.set counter.id, counter.value
20
+ else
21
+ counter.value = Harness.redis.incr(counter.id).to_i
22
+ end
23
+
24
+ counter
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ module Harness
2
+ class Gauge < Measurement
3
+ def initialize(attributes = {})
4
+ super
5
+ self.units ||= :ms
6
+ end
7
+
8
+ def self.from_event(event)
9
+ if event.payload[:gauge].is_a? Hash
10
+ gauge = new event.payload[:gauge]
11
+ else
12
+ gauge = new
13
+ end
14
+
15
+ gauge.id ||= event.name
16
+ gauge.value = event.duration
17
+
18
+ gauge
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,6 @@
1
+ ActiveSupport::Notifications.subscribe %r{.+} do |*args|
2
+ event = ActiveSupport::Notifications::Event.new(*args)
3
+
4
+ Harness::Gauge.from_event(event).log if event.payload[:gauge]
5
+ Harness::Counter.from_event(event).log if event.payload[:counter]
6
+ end
@@ -0,0 +1,9 @@
1
+ events = %w(write_fragment read_fragment expire_fragment write_page expire_page process_action send_file)
2
+
3
+ regex = %r{#{events.join("|")}.action_controller}
4
+
5
+ ActiveSupport::Notifications.subscribe regex do |*args|
6
+ event = ActiveSupport::Notifications::Event.new(*args)
7
+ gauge = Harness::Gauge.from_event event
8
+ gauge.log
9
+ end
@@ -0,0 +1,10 @@
1
+ events = %w(receive deliver)
2
+
3
+ regex = %r{#{events.join("|")}.action_mailer}
4
+
5
+ ActiveSupport::Notifications.subscribe regex do |*args|
6
+ event = ActiveSupport::Notifications::Event.new(*args)
7
+ gauge = Harness::Gauge.from_event event
8
+ gauge.log
9
+ end
10
+
@@ -0,0 +1,10 @@
1
+ events = %w(render_template render_partial)
2
+
3
+ regex = %r{#{events.join("|")}.action_view}
4
+
5
+ ActiveSupport::Notifications.subscribe regex do |*args|
6
+ event = ActiveSupport::Notifications::Event.new(*args)
7
+ gauge = Harness::Gauge.from_event event
8
+ gauge.log
9
+ end
10
+
@@ -0,0 +1,9 @@
1
+ events = %w(cache_read cache_generate cache_fetch_hit cache_write cache_delete)
2
+
3
+ regex = %r{#{events.join("|")}.active_support}
4
+
5
+ ActiveSupport::Notifications.subscribe regex do |*args|
6
+ event = ActiveSupport::Notifications::Event.new(*args)
7
+ gauge = Harness::Gauge.from_event event
8
+ gauge.log
9
+ end
@@ -0,0 +1,17 @@
1
+ module Harness
2
+ class Measurement
3
+ attr_accessor :id, :name, :source, :time, :value, :units
4
+
5
+ def initialize(attributes = {})
6
+ attributes.each_pair do |name, value|
7
+ send "#{name}=", value
8
+ end
9
+
10
+ self.time ||= Time.now
11
+ end
12
+
13
+ def log
14
+ Harness.log self
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ module Harness
2
+ class Railtie < ::Rails::Railtie
3
+ config.harness = Harness.config
4
+
5
+ rake_tasks do
6
+ load "harness/tasks.rake"
7
+ end
8
+
9
+ initializer "harness.thread" do
10
+ Thread.abort_on_exception = Rails.env.development? || Rails.env.test?
11
+ end
12
+
13
+ initializer "harness.adapter" do |app|
14
+ case Rails.env
15
+ when 'development'
16
+ app.config.harness.adapter = :null
17
+ when 'test'
18
+ app.config.harness.adapter = :null
19
+ else
20
+ app.config.harness.adapter = :librato
21
+ end
22
+ end
23
+
24
+ initializer "harness.logger" do |app|
25
+ Harness.logger = Rails.logger
26
+ end
27
+
28
+ initializer "harness.redis" do
29
+ if existing_url = ENV['REDISTOGO_URL'] || ENV['REDIS_URL']
30
+ Harness.redis ||= Redis::Namespace.new('harness', :redis => Redis.connect(:url => existing_url))
31
+ else
32
+ Harness.redis ||= Redis::Namespace.new('harness', :redis => Redis.connect(:host => 'localhost', :port => '6379'))
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ namespace :harness do
2
+ desc "Reset all counters back to zero"
3
+ task :reset_counters => :environment do
4
+ Harness.reset_counters!
5
+ end
6
+ end
@@ -1,3 +1,3 @@
1
1
  module Harness
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/harness.rb CHANGED
@@ -1,5 +1,108 @@
1
1
  require "harness/version"
2
2
 
3
+ require 'thread'
4
+
5
+ require 'securerandom'
6
+
7
+ require 'redis'
8
+ require 'redis/namespace'
9
+
10
+ require 'active_support/notifications'
11
+ require 'active_support/core_ext/string'
12
+
3
13
  module Harness
4
- # Your code goes here...
14
+ class LoggingError < RuntimeError ; end
15
+
16
+ class Config
17
+ attr_reader :adapter
18
+ attr_accessor :test_mode
19
+
20
+ def adapter=(val)
21
+ if val.is_a? Symbol
22
+ @adapter = "Harness::#{val.to_s.classify}Adapter".constantize
23
+ else
24
+ @adapter = val
25
+ end
26
+ end
27
+
28
+ def method_missing(name, *args, &block)
29
+ begin
30
+ "Harness::#{name.to_s.classify}Adapter".constantize.config
31
+ rescue NameError
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.config
38
+ @config ||= Config.new
39
+ end
40
+
41
+ def self.queue
42
+ @queue ||= Queue.new
43
+ end
44
+
45
+ def self.consumer
46
+ Thread.current["#{object_id}_harness_consumer_"] ||= Consumer.new
47
+ end
48
+
49
+ def self.log(measurement)
50
+ queue << measurement
51
+ wait if config.test_mode
52
+ end
53
+
54
+ def self.mutex
55
+ @mutex ||= Mutex.new
56
+ end
57
+
58
+ def self.wait
59
+ sleep 0.01 until consumer.finished? && queue.empty?
60
+ end
61
+
62
+ def self.logger
63
+ @logger
64
+ end
65
+
66
+ def self.logger=(logger)
67
+ @logger = logger
68
+ end
69
+
70
+ def self.redis=(redis)
71
+ @redis = redis
72
+ end
73
+
74
+ def self.redis
75
+ @redis
76
+ end
77
+
78
+ def self.reset_counters!
79
+ redis.smembers('counters').each do |counter|
80
+ redis.set counter, -1
81
+ end
82
+ end
5
83
  end
84
+
85
+ require 'harness/measurement'
86
+ require 'harness/counter'
87
+ require 'harness/gauge'
88
+
89
+ require 'harness/instrumentation'
90
+
91
+ require 'harness/consumer'
92
+
93
+ require 'harness/adapters/librato_adapter'
94
+ require 'harness/adapters/memory_adapter'
95
+ require 'harness/adapters/null_adapter'
96
+
97
+ require 'harness/integration/action_controller'
98
+ require 'harness/integration/action_view'
99
+ require 'harness/integration/action_mailer'
100
+ require 'harness/integration/active_support'
101
+
102
+ require 'harness/railtie' if defined?(Rails)
103
+
104
+ Harness.consumer.consume
105
+
106
+ require 'logger'
107
+
108
+ Harness.logger = Logger.new $stdout
@@ -0,0 +1,69 @@
1
+ require 'test_helper'
2
+
3
+ class CountersWithRedis < IntegrationTest
4
+ def test_stores_name_in_redis
5
+ instrument "event-counter", :counter => true
6
+
7
+ assert_includes redis.smembers('counters'), 'event-counter'
8
+ assert_equal 1, redis.get('event-counter').to_i
9
+
10
+ assert_counter_logged "event-counter"
11
+ end
12
+
13
+ def test_increments_counter_each_instrument
14
+ instrument "event-counter", :counter => true
15
+ assert_counter_logged "event-counter"
16
+ counters.clear
17
+
18
+ assert_empty counters
19
+ instrument "event-counter", :counter => true
20
+ assert_counter_logged "event-counter"
21
+
22
+ assert_equal 2, counters.first.value
23
+ end
24
+
25
+ def test_sets_given_value_in_redis_with_shortform
26
+ instrument "event-counter", :counter => 10
27
+
28
+ assert_equal 10, redis.get("event-counter").to_i
29
+
30
+ counters.clear
31
+
32
+ instrument "event-counter", :counter => true
33
+ assert_counter_logged 'event-counter'
34
+
35
+ assert_equal 11, counters.first.value
36
+ end
37
+
38
+ def test_sets_given_value_in_redis_with_longform
39
+ instrument "event-counter", :counter => { :value => 10 }
40
+
41
+ assert_equal 10, redis.get("event-counter").to_i
42
+
43
+ counters.clear
44
+
45
+ instrument "event-counter", :counter => true
46
+ assert_counter_logged 'event-counter'
47
+
48
+ assert_equal 11, counters.first.value
49
+ end
50
+
51
+ def test_resets_counters
52
+ instrument "event-counter", :counter => true
53
+ instrument "event-counter2", :counter => true
54
+
55
+ assert_equal 1, redis.get("event-counter").to_i
56
+ assert_equal 1, redis.get("event-counter2").to_i
57
+
58
+ Harness.reset_counters!
59
+
60
+ assert_equal -1, redis.get("event-counter").to_i
61
+ assert_equal -1, redis.get("event-counter2").to_i
62
+
63
+ counters.clear
64
+ instrument "event-counter", :counter => true
65
+ assert_counter_logged 'event-counter'
66
+
67
+ assert_equal 0, counters.first.value
68
+ end
69
+ end