harness 0.0.1 → 0.1.0

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