metrics-instrument 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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in metrics-instrument.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Ragnarson Sp. z o.o.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Metrics client for Ruby apps
2
+
3
+ Adaptation of [statsd-instrument](https://github.com/Shopify/statsd-instrument/blob/master/README.md) for the (yet unreleased) metrics server.
4
+
5
+ ## Configuration
6
+ ```ruby
7
+ Metrics.server = 'statsd.myservice.com:8125'
8
+ Metrics.logger = Rails.logger
9
+ Metrics.mode = :production
10
+ Metrics.default_sample_rate = 0.1 # Sample 10% of events. By default all events are reported.
11
+ ```
12
+
13
+ ## Usage
14
+ ### Metrics.measure
15
+ ```ruby
16
+ # You can pass a key and a ms value
17
+ Metrics.measure('GoogleBase.insert', 2.55)
18
+
19
+ # or more commonly pass a block that calls your code
20
+ Metrics.measure('GoogleBase.insert') do
21
+ GoogleBase.insert(product)
22
+ end
23
+ ```
24
+
25
+ or with metaprogramming:
26
+
27
+ ```ruby
28
+ GoogleBase.extend Metrics::Instrument
29
+ GoogleBase.metrics_measure :insert, 'GoogleBase.insert'
30
+ ```
31
+
32
+ ### Metaprogramming methods
33
+
34
+ * `metrics_count` adds '1' to the metric
35
+ * `metrics_count_if` adds '1' to the metric if method returned something
36
+ that evaulates to true. Takes optional block to evaluate the returned
37
+ value.
38
+ * `metrics_count_success` adds '1' to `metric.success` for successful
39
+ calls and `metrics.failure` for failed calls.
40
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs.push "lib"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
@@ -0,0 +1 @@
1
+ require "metrics/instrument"
@@ -0,0 +1,5 @@
1
+ module Metrics
2
+ module Instrument
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,138 @@
1
+ require 'socket'
2
+ require 'benchmark'
3
+ require 'logger'
4
+ require 'timeout'
5
+
6
+ class << Benchmark
7
+ def ms
8
+ 1000 * realtime { yield }
9
+ end
10
+ end
11
+
12
+ module Metrics
13
+ class << self
14
+ attr_accessor :host, :port, :mode, :logger, :enabled, :default_sample_rate
15
+ end
16
+
17
+ self.enabled = true
18
+ self.default_sample_rate = 1
19
+ self.logger = Logger.new($stdout)
20
+
21
+ Timeout = defined?(::SystemTimer) ? ::SystemTimer : ::Timeout
22
+
23
+ def self.server=(conn)
24
+ self.host, port = conn.split(':')
25
+ self.port = port.to_i
26
+ end
27
+
28
+ module Instrument
29
+ def metrics_measure(method, name)
30
+ add_to_method(method, name, :measure) do |old_method, new_method, metric_name, *args|
31
+ define_method(new_method) do |*args|
32
+ Metrics.measure(metric_name) { send(old_method, *args) }
33
+ end
34
+ end
35
+ end
36
+
37
+ def metrics_count_success(method, name)
38
+ add_to_method(method, name, :count_success) do |old_method, new_method, metric_name|
39
+ define_method(new_method) do |*args|
40
+ begin
41
+ truthiness = result = send(old_method, *args)
42
+ rescue
43
+ truthiness = false
44
+ raise
45
+ else
46
+ truthiness = (yield(result) rescue false) if block_given?
47
+ result
48
+ ensure
49
+ Metrics.increment("#{metric_name}." + (truthiness == false ? 'failure' : 'success'))
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def metrics_count_if(method, name)
56
+ add_to_method(method, name, :count_if) do |old_method, new_method, metric_name|
57
+ define_method(new_method) do |*args|
58
+ begin
59
+ truthiness = result = send(old_method, *args)
60
+ rescue
61
+ truthiness = false
62
+ raise
63
+ else
64
+ truthiness = (yield(result) rescue false) if block_given?
65
+ result
66
+ ensure
67
+ Metrics.increment(metric_name) if truthiness
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ def metrics_count(method, name)
74
+ add_to_method(method, name, :count) do |old_method, new_method, metric_name|
75
+ define_method(new_method) do |*args|
76
+ Metrics.increment(metric_name)
77
+ send(old_method, *args)
78
+ end
79
+ end
80
+ end
81
+
82
+ private
83
+ def add_to_method(method, name, action, &block)
84
+ metric_name = name
85
+
86
+ method_name_without_metrics = :"#{method}_for_#{action}_on_#{self.name}_without_#{name}"
87
+ # raw_ssl_request_for_measure_on_FedEx_without_ActiveMerchant.Shipping.#{self.class.name}.ssl_request
88
+
89
+ method_name_with_metrics = :"#{method}_for_#{action}_on_#{self.name}_with_#{name}"
90
+ # raw_ssl_request_for_measure_on_FedEx_with_ActiveMerchant.Shipping.#{self.class.name}.ssl_request
91
+
92
+ raise ArgumentError, "already instrumented #{method} for #{self.name}" if method_defined? method_name_without_metrics
93
+ raise ArgumentError, "could not find method #{method} for #{self.name}" unless method_defined?(method) || private_method_defined?(method)
94
+
95
+ alias_method method_name_without_metrics, method
96
+ yield method_name_without_metrics, method_name_with_metrics, metric_name
97
+ alias_method method, method_name_with_metrics
98
+ end
99
+ end
100
+
101
+ def self.measure(key, milli = nil)
102
+ result = nil
103
+ ms = milli || Benchmark.ms do
104
+ result = yield
105
+ end
106
+
107
+ write(key, ms)
108
+ result
109
+ end
110
+
111
+ def self.increment(key, delta = 1, sample_rate = default_sample_rate)
112
+ write(key, delta, sample_rate)
113
+ end
114
+
115
+ private
116
+ def self.socket
117
+ @socket ||= UDPSocket.new
118
+ end
119
+
120
+ def self.write(k, v, sample_rate = default_sample_rate)
121
+ return unless enabled
122
+ return if sample_rate < 1 && rand > sample_rate
123
+
124
+ msg = "#{k} #{v}"
125
+
126
+ if mode == :production
127
+ socket_wrapper { socket.send(msg, 0, host, port) }
128
+ else
129
+ logger.info "[Metrics] #{msg}"
130
+ end
131
+ end
132
+
133
+ def self.socket_wrapper(options = {})
134
+ Timeout.timeout(options.fetch(:timeout, 0.1)) { yield }
135
+ rescue Timeout::Error, SocketError, IOError, SystemCallError => e
136
+ logger.error e
137
+ end
138
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "metrics-instrument/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "metrics-instrument"
7
+ s.version = Metrics::Instrument::VERSION
8
+ s.authors = ["Grzesiek Kolodziejczyk"]
9
+ s.email = ["grk@ragnarson.com"]
10
+ s.homepage = "https://github.com/Ragnarson/metrics-instrument"
11
+ s.summary = %q{A Metrics client for Ruby apps}
12
+ s.description = %q{A Metrics client for Ruby apps. Provides metaprogramming methods to inject Metrics instrumentation into your code.}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency 'mocha'
20
+ s.add_development_dependency 'rake'
21
+ #s.add_development_dependency 'minitest-reporters'
22
+ end
@@ -0,0 +1,197 @@
1
+ require 'metrics-instrument'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+ require 'logger'
5
+
6
+ Metrics.logger = Logger.new('/dev/null')
7
+
8
+ module ActiveMerchant; end
9
+ class ActiveMerchant::Base
10
+ def ssl_post(arg)
11
+ if arg
12
+ 'OK'
13
+ else
14
+ raise 'Not OK'
15
+ end
16
+ end
17
+ end
18
+
19
+ class ActiveMerchant::Gateway < ActiveMerchant::Base
20
+ def purchase(arg)
21
+ ssl_post(arg)
22
+ true
23
+ rescue
24
+ false
25
+ end
26
+
27
+ def self.sync
28
+ true
29
+ end
30
+
31
+ def self.singleton_class
32
+ class << self; self; end
33
+ end
34
+ end
35
+
36
+ class ActiveMerchant::UniqueGateway < ActiveMerchant::Base
37
+ def ssl_post(arg)
38
+ {:success => arg}
39
+ end
40
+
41
+ def purchase(arg)
42
+ ssl_post(arg)
43
+ end
44
+ end
45
+
46
+ ActiveMerchant::Base.extend Metrics::Instrument
47
+
48
+ class MetricsTest < Test::Unit::TestCase
49
+ def setup
50
+ Metrics.stubs(:increment)
51
+ end
52
+
53
+ def test_metrics_count_if
54
+ ActiveMerchant::Gateway.metrics_count_if :ssl_post, 'ActiveMerchant.Gateway.if'
55
+
56
+ Metrics.expects(:increment).with(includes('if')).once
57
+ ActiveMerchant::Gateway.new.purchase(true)
58
+ ActiveMerchant::Gateway.new.purchase(false)
59
+ end
60
+
61
+ def test_metrics_count_if_with_block
62
+ ActiveMerchant::UniqueGateway.metrics_count_if :ssl_post, 'ActiveMerchant.Gateway.block' do |result|
63
+ result[:success]
64
+ end
65
+
66
+ Metrics.expects(:increment).with(includes('block')).once
67
+ ActiveMerchant::UniqueGateway.new.purchase(true)
68
+ ActiveMerchant::UniqueGateway.new.purchase(false)
69
+ end
70
+
71
+ def test_metrics_count_success
72
+ ActiveMerchant::Gateway.metrics_count_success :ssl_post, 'ActiveMerchant.Gateway'
73
+
74
+ Metrics.expects(:increment).with(includes('success'))
75
+ ActiveMerchant::Gateway.new.purchase(true)
76
+
77
+ Metrics.expects(:increment).with(includes('failure'))
78
+ ActiveMerchant::Gateway.new.purchase(false)
79
+ end
80
+
81
+ def test_metrics_count_success_with_block
82
+ ActiveMerchant::UniqueGateway.metrics_count_success :ssl_post, 'ActiveMerchant.Gateway' do |result|
83
+ result[:success]
84
+ end
85
+
86
+ Metrics.expects(:increment).with(includes('success'))
87
+ ActiveMerchant::UniqueGateway.new.purchase(true)
88
+
89
+ Metrics.expects(:increment).with(includes('failure'))
90
+ ActiveMerchant::UniqueGateway.new.purchase(false)
91
+ end
92
+
93
+ def test_metrics_count
94
+ ActiveMerchant::Gateway.metrics_count :ssl_post, 'ActiveMerchant.Gateway.ssl_post'
95
+
96
+ Metrics.expects(:increment).with(includes('ssl_post'))
97
+ ActiveMerchant::Gateway.new.purchase(true)
98
+ end
99
+
100
+ def test_metrics_measure
101
+ ActiveMerchant::UniqueGateway.metrics_measure :ssl_post, 'ActiveMerchant.Gateway.ssl_post'
102
+
103
+ Metrics.expects(:write).with('ActiveMerchant.Gateway.ssl_post', is_a(Float)).returns({:success => true})
104
+ ActiveMerchant::UniqueGateway.new.purchase(true)
105
+ end
106
+
107
+ def test_instrumenting_class_method
108
+ ActiveMerchant::Gateway.singleton_class.extend Metrics::Instrument
109
+ ActiveMerchant::Gateway.singleton_class.metrics_count :sync, 'ActiveMerchant.Gateway.sync'
110
+
111
+ Metrics.expects(:increment).with(includes('sync'))
112
+ ActiveMerchant::Gateway.sync
113
+ end
114
+
115
+ def test_count_with_sampling
116
+ Metrics.unstub(:increment)
117
+ Metrics.stubs(:rand).returns(0.6)
118
+ Metrics.logger.expects(:info).never
119
+
120
+ Metrics.increment('sampling.foo.bar', 1, 0.1)
121
+ end
122
+
123
+ def test_count_with_successful_sample
124
+ Metrics.unstub(:increment)
125
+ Metrics.stubs(:rand).returns(0.01)
126
+ Metrics.logger.expects(:info).once.with do |string|
127
+ string.include?('1')
128
+ end
129
+
130
+ Metrics.increment('sampling.foo.bar', 1, 0.1)
131
+ end
132
+
133
+ def test_production_mode_should_use_udp_socket
134
+ Metrics.unstub(:increment)
135
+
136
+ Metrics.mode = :production
137
+ Metrics.server = 'localhost:123'
138
+ UDPSocket.any_instance.expects(:send)
139
+
140
+ Metrics.increment('fooz')
141
+ Metrics.mode = :test
142
+ end
143
+
144
+ def test_should_not_write_when_disabled
145
+ Metrics.enabled = false
146
+ Metrics.expects(:logger).never
147
+ Metrics.increment('fooz')
148
+ Metrics.enabled = true
149
+ end
150
+
151
+ def test_metrics_measure_with_explicit_value
152
+ Metrics.expects(:write).with('values.foobar', 42)
153
+
154
+ Metrics.measure('values.foobar', 42)
155
+ end
156
+
157
+ def test_socket_error_should_not_raise
158
+ Metrics.mode = :production
159
+ UDPSocket.any_instance.expects(:send).raises(SocketError)
160
+ Metrics.measure('values.foobar', 42)
161
+ Metrics.mode = :test
162
+ end
163
+
164
+ def test_timeout_error_should_not_raise
165
+ Metrics.mode = :production
166
+ UDPSocket.any_instance.expects(:send).raises(Timeout::Error)
167
+ Metrics.measure('values.foobar', 42)
168
+ Metrics.mode = :test
169
+ end
170
+
171
+ def test_system_call_error_should_not_raise
172
+ Metrics.mode = :production
173
+ UDPSocket.any_instance.expects(:send).raises(Errno::ETIMEDOUT)
174
+ Metrics.measure('values.foobar', 42)
175
+ Metrics.mode = :test
176
+ end
177
+
178
+ def test_io_error_should_not_raise
179
+ Metrics.mode = :production
180
+ UDPSocket.any_instance.expects(:send).raises(IOError)
181
+ Metrics.measure('values.foobar', 42)
182
+ Metrics.mode = :test
183
+ end
184
+
185
+ def test_long_request_should_timeout
186
+ Metrics.mode = :production
187
+ UDPSocket.any_instance.expects(:send).yields do
188
+ begin
189
+ Timeout.timeout(0.5) { sleep 1 }
190
+ rescue Timeout::Error
191
+ raise "Allowed long running request"
192
+ end
193
+ end
194
+ Metrics.measure('values.foobar', 42)
195
+ Metrics.mode = :test
196
+ end
197
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metrics-instrument
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Grzesiek Kolodziejczyk
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mocha
16
+ requirement: &70134701481200 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70134701481200
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &70134701480780 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70134701480780
36
+ description: A Metrics client for Ruby apps. Provides metaprogramming methods to inject
37
+ Metrics instrumentation into your code.
38
+ email:
39
+ - grk@ragnarson.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - LICENSE
47
+ - README.md
48
+ - Rakefile
49
+ - lib/metrics-instrument.rb
50
+ - lib/metrics-instrument/version.rb
51
+ - lib/metrics/instrument.rb
52
+ - metrics-instrument.gemspec
53
+ - test/metrics-instrument_test.rb
54
+ homepage: https://github.com/Ragnarson/metrics-instrument
55
+ licenses: []
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ segments:
67
+ - 0
68
+ hash: 3448421278235268376
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ segments:
76
+ - 0
77
+ hash: 3448421278235268376
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 1.8.16
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: A Metrics client for Ruby apps
84
+ test_files:
85
+ - test/metrics-instrument_test.rb