statsd-instrument 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 statsd-instrument.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # StatsD client for Ruby apps
2
+
3
+ This is a ruby client for statsd (http://github.com/etsy/statsd). It provides a lightweight way to track and measure metrics in your application.
4
+
5
+ We call out to statsd by sending data over a UDP socket. UDP sockets are fast, but unreliable, there is no guarantee that your data will ever arrive at it's location. In other words, fire and forget. This is perfect for this use case because it means your code doesn't get bogged down trying to log statistics. We send data to statsd several times per request and haven't noticed a performance hit.
6
+
7
+ The fact that all of your stats data may not make it into statsd is no issue. Graphite (the graph database that statsd is built on) will only show you trends in your data. Internally it only keeps enough data to satisfy the levels of granularity we specify. As well as satisfying it's requirement as a fixed size database. We can throw as much data at it as we want it and it will do it's best to show us the trends over time and get rid of the fluff.
8
+
9
+ For Shopify, our retention periods are:
10
+
11
+ 1. 10 seconds of granularity for the last 6 hours
12
+ 2. 60 seconds of granularity for the last week
13
+ 3. 10 minutes of granularity for the last 5 years
14
+
15
+ This is the same as what Etsy uses (mentioned in the README for [http://github.com/etsy/statd](http://github.com/etsy/statd])).
16
+
17
+ ## Configuration
18
+
19
+ ``` ruby
20
+ StatsD.server = 'statsd.myservice.com:8125'
21
+ StatsD.logger = Rails.logger
22
+ StatsD.mode = :production
23
+ ```
24
+
25
+ If you set the mode to anything besides production then the library will print its calls to the logger, rather than sending them over the wire.
26
+
27
+ ## StatsD keys
28
+
29
+ StatsD keys look like 'admin.logins.api.success'. Each dot in the key represents a 'folder' in the graphite interface. You can include any data you want in the keys.
30
+
31
+ ## Usage
32
+
33
+ ### StatsD.measure
34
+
35
+ Lets you benchmark how long the execution of a specific method takes.
36
+
37
+ ``` ruby
38
+ # You can pass a key and a ms value
39
+ StatsD.measure('GoogleBase.insert', 2.55)
40
+
41
+ # or more commonly pass a block that calls your code
42
+ StatsD.measure('GoogleBase.insert') do
43
+ GoogleBase.insert(product)
44
+ end
45
+ ```
46
+
47
+ Rather than using this method directly it's more common to use the metaprogramming methods made available.
48
+
49
+ ``` ruby
50
+ GoogleBase.extend StatsD::Instrument
51
+ GoogleBase.statsd_measure :insert, 'GoogleBase.insert'
52
+ ```
53
+
54
+ ### StatsD.increment
55
+
56
+ Lets you increment a key in statsd to keep a count of something. If the specified key doesn't exist it will create it for you.
57
+
58
+ ``` ruby
59
+ # increments default to +1
60
+ StatsD.increment('GoogleBase.insert')
61
+ # you can also specify how much to increment the key by
62
+ StatsD.increment('GoogleBase.insert', 10)
63
+ # you can also specify a sample rate, so only 1/10 of events
64
+ # actually get to statsd. Useful for very high volume data
65
+ StatsD.increment('GoogleBase.insert', 1, 0.1)
66
+ ```
67
+
68
+ Again it's more common to use the metaprogramming methods.
69
+
70
+ ## Metaprogramming Methods
71
+
72
+ As mentioned, it's most common to use the provided metaprogramming methods. This lets you define all of your instrumentation in one file and not litter your code with instrumentation details. You should enable a class for instrumentation by extending it with the `StatsD::Instrument` class.
73
+
74
+ ``` ruby
75
+ GoogleBase.extend StatsD::Instrument
76
+ ```
77
+
78
+ Then use the methods provided below to instrument methods in your class.
79
+
80
+ ### statsd\_count
81
+
82
+ This will increment the given key even if the method doesn't finish (ie. raises).
83
+
84
+ ``` ruby
85
+ GoogleBase.statsd_count :insert, 'GoogleBase.insert'
86
+ ```
87
+
88
+ Note how I used the 'GoogleBase.insert' key above when measuring this method, and I reused here when counting the method calls. StatsD automatically separates these two kinds of stats into namespaces so there won't be a key collision here.
89
+
90
+ ### statsd\_count\_if
91
+
92
+ This will only increment the given key if the method executes successfully.
93
+
94
+ ``` ruby
95
+ GoogleBase.statsd_count_if :insert, 'GoogleBase.insert'
96
+ ```
97
+
98
+ So now, if GoogleBase#insert raises an exception or returns false (ie. result == false), we won't increment the key. If you want to define what success means for a given method you can pass a block that takes the result of the method.
99
+
100
+ ``` ruby
101
+ GoogleBase.statsd_count_if :insert, 'GoogleBase.insert' do |response|
102
+ result.code == 200
103
+ end
104
+ ```
105
+
106
+ In the above example we will only increment the key in statsd if the result of the block returns true. So the method is returning a Net::HTTP response and we're checking the status code.
107
+
108
+ ### statsd\_count\_success
109
+
110
+ Similar to statsd_count_if, except this will increment one key in the case of success and another key in the case of failure.
111
+
112
+ ``` ruby
113
+ GoogleBase.statsd_count_success :insert, 'GoogleBase.insert'
114
+ ```
115
+
116
+ So if this method fails execution (raises or returns false) we'll increment the failure key ('GoogleBase.insert.failure'), otherwise we'll increment the success key ('GoogleBase.insert.success'). Notice that we're modifying the given key before sending it to statsd.
117
+
118
+ Again you can pass a block to define what success means.
119
+
120
+ ``` ruby
121
+ GoogleBase.statsd_count_if :insert, 'GoogleBase.insert' do |response|
122
+ result.code == 200
123
+ end
124
+ ```
125
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new('test') do |t|
5
+ t.ruby_opts << '-rubygems'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/*.rb']
8
+ end
@@ -0,0 +1 @@
1
+ require 'statsd/instrument'
@@ -0,0 +1,146 @@
1
+ require 'socket'
2
+
3
+ module StatsD
4
+ class << self
5
+ attr_accessor :host, :port, :mode, :logger, :enabled
6
+ end
7
+ self.enabled = true
8
+
9
+ trap("TTOU") { self.enabled = false }
10
+ trap("TTIN") { self.enabled = true }
11
+
12
+ # StatsD.server = 'localhost:1234'
13
+ def self.server=(conn)
14
+ self.host, port = conn.split(':')
15
+ self.port = port.to_i
16
+ end
17
+
18
+ module Instrument
19
+ def statsd_measure(method, name)
20
+ add_to_method(method, name, :measure) do |old_method, new_method, metric_name, *args|
21
+ define_method(new_method) do |*args|
22
+ StatsD.measure(send(metric_name)) { send(old_method, *args) }
23
+ end
24
+ end
25
+ end
26
+
27
+ def statsd_count_success(method, name)
28
+ add_to_method(method, name, :count_success) do |old_method, new_method, metric_name|
29
+ define_method(new_method) do |*args|
30
+ begin
31
+ truthiness = result = send(old_method, *args)
32
+ rescue
33
+ truthiness = false
34
+ raise
35
+ else
36
+ truthiness = (yield(result) rescue false) if block_given?
37
+ result
38
+ ensure
39
+ StatsD.increment("#{send(metric_name)}." + (truthiness == false ? 'failure' : 'success'))
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def statsd_count_if(method, name)
46
+ add_to_method(method, name, :count_if) do |old_method, new_method, metric_name|
47
+ define_method(new_method) do |*args|
48
+ begin
49
+ truthiness = result = send(old_method, *args)
50
+ rescue
51
+ truthiness = false
52
+ raise
53
+ else
54
+ truthiness = (yield(result) rescue false) if block_given?
55
+ result
56
+ ensure
57
+ StatsD.increment(send(metric_name)) if truthiness
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def statsd_count(method, name)
64
+ add_to_method(method, name, :count) do |old_method, new_method, metric_name|
65
+ define_method(new_method) do |*args|
66
+ StatsD.increment(send(metric_name))
67
+ send(old_method, *args)
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+ def statsd_memoize(metric_name, name)
74
+ define_method(metric_name) do
75
+ name = eval("\"#{name}\"", binding)
76
+
77
+ self.class.send(:define_method, metric_name) do
78
+ name
79
+ end
80
+ send(metric_name)
81
+ end
82
+ end
83
+
84
+ def add_to_method(method, name, action, &block)
85
+ metric_name = :"#{method}_#{name}_metric_name"
86
+ statsd_memoize(metric_name, name)
87
+
88
+ method_name_without_statsd = :"#{method}_for_#{action}_on_#{self.name}_without_#{name}"
89
+ # raw_ssl_request_for_measure_on_FedEx_without_ActiveMerchant.Shipping.#{self.class.name}.ssl_request
90
+
91
+ method_name_with_statsd = :"#{method}_for_#{action}_on_#{self.name}_with_#{name}"
92
+ # raw_ssl_request_for_measure_on_FedEx_with_ActiveMerchant.Shipping.#{self.class.name}.ssl_request
93
+
94
+ raise ArgumentError, "already instrumented #{method} for #{self.name}" if method_defined? method_name_without_statsd
95
+ raise ArgumentError, "could not find method #{method} for #{self.name}" unless method_defined?(method) || private_method_defined?(method)
96
+
97
+ alias_method method_name_without_statsd, method
98
+ yield method_name_without_statsd, method_name_with_statsd, metric_name
99
+ alias_method method, method_name_with_statsd
100
+ end
101
+ end
102
+
103
+ # glork:320|ms
104
+ def self.measure(key, milli = nil)
105
+ result = nil
106
+ ms = Benchmark.ms do
107
+ result = yield
108
+ end if milli.nil?
109
+
110
+ write(key, ms, :ms)
111
+ result
112
+ end
113
+
114
+ # gorets:1|c
115
+ def self.increment(key, delta = 1, sample_rate = 1)
116
+ write(key, delta, :incr, sample_rate)
117
+ end
118
+
119
+ private
120
+
121
+ def self.socket
122
+ @socket ||= UDPSocket.new
123
+ end
124
+
125
+ def self.write(k,v,op, sample_rate = 1)
126
+ return unless enabled
127
+ return if sample_rate < 1 && rand > sample_rate
128
+
129
+ command = "#{k}:#{v}"
130
+ case op
131
+ when :incr
132
+ command << '|c'
133
+ when :ms
134
+ command << '|ms'
135
+ end
136
+
137
+ command << "|@#{sample_rate}" if sample_rate < 1
138
+
139
+ if mode == :production
140
+ socket.send(command, 0, host, port)
141
+ else
142
+ logger.info "[StatsD] #{command}"
143
+ end
144
+ end
145
+ end
146
+
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "statsd-instrument"
3
+ s.version = '1.0.0'
4
+ s.authors = ["Jesse Storimer"]
5
+ s.email = ["jesse@shopify.com"]
6
+ s.homepage = "http://github.com/shopify/statsd-instrument"
7
+
8
+ s.summary = %q{A StatsD client for Ruby apps}
9
+ s.description = %q{A StatsD client for Ruby apps. Provides metaprogramming methods to inject StatsD instrumentation into your code.}
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
13
+
14
+ s.add_development_dependency 'mocha'
15
+ end
@@ -0,0 +1,148 @@
1
+ require 'statsd-instrument'
2
+ require 'test/unit'
3
+ require 'mocha'
4
+ require 'logger'
5
+
6
+ StatsD.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
+ end
27
+
28
+ class ActiveMerchant::UniqueGateway < ActiveMerchant::Base
29
+ def ssl_post(arg)
30
+ {:success => arg}
31
+ end
32
+
33
+ def purchase(arg)
34
+ ssl_post(arg)
35
+ end
36
+ end
37
+
38
+ ActiveMerchant::Base.extend StatsD::Instrument
39
+
40
+ class StatsDTest < Test::Unit::TestCase
41
+ def setup
42
+ StatsD.stubs(:increment)
43
+ end
44
+
45
+ def test_statsd_count_if
46
+ ActiveMerchant::Gateway.statsd_count_if :ssl_post, 'ActiveMerchant.Billing.#{self.class.name}.if'
47
+
48
+ StatsD.expects(:increment).with(includes('if')).once
49
+ ActiveMerchant::Gateway.new.purchase(true)
50
+ ActiveMerchant::Gateway.new.purchase(false)
51
+ end
52
+
53
+ def test_statsd_count_if_with_block
54
+ ActiveMerchant::UniqueGateway.statsd_count_if :ssl_post, 'ActiveMerchant.Billing.#{self.class.name}.block' do |result|
55
+ result[:success]
56
+ end
57
+
58
+ StatsD.expects(:increment).with(includes('block')).once
59
+ ActiveMerchant::UniqueGateway.new.purchase(true)
60
+ ActiveMerchant::UniqueGateway.new.purchase(false)
61
+ end
62
+
63
+ def test_statsd_count_success
64
+ ActiveMerchant::Gateway.statsd_count_success :ssl_post, 'ActiveMerchant.Billing.#{self.class.name}'
65
+
66
+ StatsD.expects(:increment).with(includes('success'))
67
+ ActiveMerchant::Gateway.new.purchase(true)
68
+
69
+ StatsD.expects(:increment).with(includes('failure'))
70
+ ActiveMerchant::Gateway.new.purchase(false)
71
+ end
72
+
73
+ def test_statsd_count_success_with_block
74
+ ActiveMerchant::UniqueGateway.statsd_count_success :ssl_post, 'ActiveMerchant.Billing.#{self.class.name}' do |result|
75
+ result[:success]
76
+ end
77
+
78
+ StatsD.expects(:increment).with(includes('success'))
79
+ ActiveMerchant::UniqueGateway.new.purchase(true)
80
+
81
+ StatsD.expects(:increment).with(includes('failure'))
82
+ ActiveMerchant::UniqueGateway.new.purchase(false)
83
+ end
84
+
85
+ def test_statsd_count
86
+ ActiveMerchant::Gateway.statsd_count :ssl_post, 'ActiveMerchant.Billing.#{self.class.name}.ssl_post'
87
+
88
+ StatsD.expects(:increment).with(includes('ssl_post'))
89
+ ActiveMerchant::Gateway.new.purchase(true)
90
+ end
91
+
92
+ def test_statsd_measure
93
+ ActiveMerchant::UniqueGateway.statsd_measure :ssl_post, 'ActiveMerchant.Billing.#{self.class.name}.ssl_post'
94
+
95
+ StatsD.expects(:measure).with(includes('ssl_post')).returns({:success => true})
96
+ ActiveMerchant::UniqueGateway.new.purchase(true)
97
+ end
98
+
99
+ def test_count_with_sampling
100
+ StatsD.unstub(:increment)
101
+ StatsD.stubs(:rand).returns(0.6)
102
+ StatsD.logger.expects(:info).never
103
+
104
+ StatsD.increment('sampling.foo.bar', 1, 0.1)
105
+ end
106
+
107
+ def test_count_with_successful_sample
108
+ StatsD.unstub(:increment)
109
+ StatsD.stubs(:rand).returns(0.01)
110
+ StatsD.logger.expects(:info).once.with do |string|
111
+ string.include?('@0.1')
112
+ end
113
+
114
+ StatsD.increment('sampling.foo.bar', 1, 0.1)
115
+ end
116
+
117
+ def test_production_mode_should_use_udp_socket
118
+ StatsD.unstub(:increment)
119
+
120
+ StatsD.mode = :production
121
+ StatsD.server = 'localhost:123'
122
+ UDPSocket.any_instance.expects(:send)
123
+
124
+ StatsD.increment('fooz')
125
+ StatsD.mode = :test
126
+ end
127
+
128
+ def test_should_not_write_when_disabled
129
+ StatsD.enabled = false
130
+ StatsD.expects(:logger).never
131
+ StatsD.increment('fooz')
132
+ StatsD.enabled = true
133
+ end
134
+
135
+ def test_receiving_TTOU_should_disable
136
+ Process.kill("TTOU", $$)
137
+ sleep 0.5
138
+ assert !StatsD.enabled
139
+ end
140
+
141
+ def test_receiving_TTIN_should_disable
142
+ StatsD.enabled = false
143
+
144
+ Process.kill("TTIN", $$)
145
+ sleep 0.5
146
+ assert StatsD.enabled
147
+ end
148
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statsd-instrument
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Jesse Storimer
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-28 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: mocha
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description: A StatsD client for Ruby apps. Provides metaprogramming methods to inject StatsD instrumentation into your code.
36
+ email:
37
+ - jesse@shopify.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - .gitignore
46
+ - Gemfile
47
+ - README.md
48
+ - Rakefile
49
+ - lib/statsd-instrument.rb
50
+ - lib/statsd/instrument.rb
51
+ - statsd-instrument.gemspec
52
+ - test/statsd-instrument_test.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/shopify/statsd-instrument
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.9.2
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: A StatsD client for Ruby apps
87
+ test_files:
88
+ - test/statsd-instrument_test.rb