statsd-instrument 1.7.2 → 2.0.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.
@@ -1,9 +1,6 @@
1
1
  require 'socket'
2
- require 'benchmark'
3
2
  require 'logger'
4
3
 
5
- require 'statsd/instrument/version'
6
-
7
4
  module StatsD
8
5
  module Instrument
9
6
 
@@ -11,6 +8,20 @@ module StatsD
11
8
  metric_name.respond_to?(:call) ? metric_name.call(callee, args).gsub('::', '.') : metric_name.gsub('::', '.')
12
9
  end
13
10
 
11
+ if Process.respond_to?(:clock_gettime)
12
+ def self.duration
13
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
14
+ yield
15
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
16
+ end
17
+ else
18
+ def self.duration
19
+ start = Time.now
20
+ yield
21
+ Time.now - start
22
+ end
23
+ end
24
+
14
25
  def statsd_measure(method, name, *metric_options)
15
26
  add_to_method(method, name, :measure) do |old_method, new_method, metric_name, *args|
16
27
  define_method(new_method) do |*args, &block|
@@ -110,41 +121,24 @@ module StatsD
110
121
  end
111
122
 
112
123
  class << self
113
- attr_accessor :host, :port, :mode, :logger, :enabled, :default_sample_rate, :prefix, :implementation
114
-
115
- def server=(conn)
116
- self.host, port = conn.split(':')
117
- self.port = port.to_i
118
- invalidate_socket
119
- end
124
+ attr_accessor :logger, :default_sample_rate, :prefix
125
+ attr_writer :backend
120
126
 
121
- def host=(host)
122
- @host = host
123
- invalidate_socket
124
- end
125
-
126
- def port=(port)
127
- @port = port
128
- invalidate_socket
129
- end
130
-
131
- def invalidate_socket
132
- @socket = nil
127
+ def backend
128
+ @backend ||= StatsD::Instrument::Environment.default_backend
133
129
  end
134
130
 
135
131
  # glork:320|ms
136
- def measure(key, value = nil, *metric_options)
132
+ def measure(key, value = nil, *metric_options, &block)
137
133
  if value.is_a?(Hash) && metric_options.empty?
138
134
  metric_options = [value]
139
135
  value = nil
140
136
  end
141
137
 
142
138
  result = nil
143
- ms = value || 1000 * Benchmark.realtime do
144
- result = yield
145
- end
146
-
147
- collect(:ms, key, ms, hash_argument(metric_options))
139
+ value = 1000 * StatsD::Instrument.duration { result = block.call } if block_given?
140
+ metric = collect_metric(hash_argument(metric_options).merge(type: :ms, name: key, value: value))
141
+ result = metric unless block_given?
148
142
  result
149
143
  end
150
144
 
@@ -155,29 +149,27 @@ module StatsD
155
149
  value = 1
156
150
  end
157
151
 
158
- collect(:c, key, value, hash_argument(metric_options))
152
+ collect_metric(hash_argument(metric_options).merge(type: :c, name: key, value: value))
159
153
  end
160
154
 
161
155
  # gaugor:333|g
162
156
  # guagor:1234|kv|@1339864935 (statsite)
163
157
  def gauge(key, value, *metric_options)
164
- collect(:g, key, value, hash_argument(metric_options))
158
+ collect_metric(hash_argument(metric_options).merge(type: :g, name: key, value: value))
165
159
  end
166
160
 
167
161
  # histogram:123.45|h
168
162
  def histogram(key, value, *metric_options)
169
- raise NotImplementedError, "StatsD.histogram only supported on :datadog implementation." unless self.implementation == :datadog
170
- collect(:h, key, value, hash_argument(metric_options))
163
+ collect_metric(hash_argument(metric_options).merge(type: :h, name: key, value: value))
171
164
  end
172
165
 
173
166
  def key_value(key, value, *metric_options)
174
- raise NotImplementedError, "StatsD.key_value only supported on :statsite implementation." unless self.implementation == :statsite
175
- collect(:kv, key, value, hash_argument(metric_options))
167
+ collect_metric(hash_argument(metric_options).merge(type: :kv, name: key, value: value))
176
168
  end
177
169
 
178
170
  # uniques:765|s
179
171
  def set(key, value, *metric_options)
180
- collect(:s, key, value, hash_argument(metric_options))
172
+ collect_metric(hash_argument(metric_options).merge(type: :s, name: key, value: value))
181
173
  end
182
174
 
183
175
  private
@@ -190,64 +182,20 @@ module StatsD
190
182
  hash = {}
191
183
  args.each_with_index do |value, index|
192
184
  hash[order[index]] = value
193
- end
194
-
195
- return hash
196
- end
197
-
198
- def socket
199
- if @socket.nil?
200
- @socket = UDPSocket.new
201
- @socket.connect(host, port)
202
- end
203
- @socket
204
- end
205
-
206
- def collect(type, k, v, options = {})
207
- return unless enabled
208
- sample_rate = options[:sample_rate] || StatsD.default_sample_rate
209
- return if sample_rate < 1 && rand > sample_rate
210
-
211
- packet = generate_packet(type, k, v, sample_rate, options[:tags])
212
- write_packet(packet)
213
- end
214
-
215
- def write_packet(command)
216
- if mode.to_s == 'production'
217
- socket.send(command, 0)
218
- else
219
- logger.info "[StatsD] #{command}"
220
185
  end
221
- rescue SocketError, IOError, SystemCallError => e
222
- logger.error e
223
- end
224
186
 
225
- def clean_tags(tags)
226
- tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
227
- tags.map do |tag|
228
- components = tag.split(':', 2)
229
- components.map { |c| c.gsub(/[^\w\.-]+/, '_') }.join(':')
230
- end
187
+ return hash
231
188
  end
232
189
 
233
- def generate_packet(type, k, v, sample_rate = default_sample_rate, tags = nil)
234
- command = self.prefix ? self.prefix + '.' : ''
235
- command << "#{k}:#{v}|#{type}"
236
- command << "|@#{sample_rate}" if sample_rate < 1 || (self.implementation == :statsite && sample_rate > 1)
237
- if tags
238
- raise ArgumentError, "Tags are only supported on :datadog implementation" unless self.implementation == :datadog
239
- command << "|##{clean_tags(tags).join(',')}"
240
- end
241
-
242
- command << "\n" if self.implementation == :statsite
243
- command
190
+ def collect_metric(options)
191
+ backend.collect_metric(metric = StatsD::Instrument::Metric.new(options))
192
+ metric
244
193
  end
245
194
  end
246
195
  end
247
196
 
248
- StatsD.enabled = true
249
- StatsD.default_sample_rate = 1.0
250
- StatsD.implementation = ENV.fetch('STATSD_IMPLEMENTATION', 'statsd').to_sym
251
- StatsD.server = ENV['STATSD_ADDR'] if ENV.has_key?('STATSD_ADDR')
252
- StatsD.mode = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
253
- StatsD.logger = Logger.new($stderr)
197
+ require 'statsd/instrument/metric'
198
+ require 'statsd/instrument/backend'
199
+ require 'statsd/instrument/assertions'
200
+ require 'statsd/instrument/environment'
201
+ require 'statsd/instrument/version'
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.homepage = "https://github.com/Shopify/statsd-instrument"
12
12
  spec.summary = %q{A StatsD client for Ruby apps}
13
13
  spec.description = %q{A StatsD client for Ruby appspec. Provides metaprogramming methods to inject StatsD instrumentation into your code.}
14
- spec.license = "MIT"
14
+ spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
@@ -19,5 +19,6 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_development_dependency 'rake'
22
+ spec.add_development_dependency 'minitest'
22
23
  spec.add_development_dependency 'mocha'
23
24
  end
@@ -0,0 +1,126 @@
1
+ require 'test_helper'
2
+
3
+ class AssertionsTest < Minitest::Test
4
+
5
+ def setup
6
+ test_class = Class.new(Minitest::Test)
7
+ test_class.send(:include, StatsD::Instrument::Assertions)
8
+ @test_case = test_class.new('fake')
9
+ end
10
+
11
+ def test_capture_metrics_inside_block_only
12
+ StatsD.increment('counter')
13
+ metrics = @test_case.capture_statsd_calls do
14
+ StatsD.increment('counter')
15
+ StatsD.gauge('gauge', 12)
16
+ end
17
+ StatsD.gauge('gauge', 15)
18
+
19
+ assert_equal 2, metrics.length
20
+ assert_equal 'counter', metrics[0].name
21
+ assert_equal 'gauge', metrics[1].name
22
+ assert_equal 12, metrics[1].value
23
+ end
24
+
25
+ def test_assert_no_statsd_calls
26
+ assert_no_assertion_triggered do
27
+ @test_case.assert_no_statsd_calls('counter') do
28
+ # noop
29
+ end
30
+ end
31
+
32
+ assert_no_assertion_triggered do
33
+ @test_case.assert_no_statsd_calls('counter') do
34
+ StatsD.increment('other')
35
+ end
36
+ end
37
+
38
+ assert_assertion_triggered do
39
+ @test_case.assert_no_statsd_calls('counter') do
40
+ StatsD.increment('counter')
41
+ end
42
+ end
43
+
44
+ assert_assertion_triggered do
45
+ @test_case.assert_no_statsd_calls do
46
+ StatsD.increment('other')
47
+ end
48
+ end
49
+ end
50
+
51
+ def test_assert_statsd_call
52
+ assert_no_assertion_triggered do
53
+ @test_case.assert_statsd_increment('counter') do
54
+ StatsD.increment('counter')
55
+ end
56
+ end
57
+
58
+ assert_no_assertion_triggered do
59
+ @test_case.assert_statsd_increment('counter') do
60
+ StatsD.increment('counter')
61
+ StatsD.increment('other')
62
+ end
63
+ end
64
+
65
+ assert_assertion_triggered do
66
+ @test_case.assert_statsd_increment('counter') do
67
+ StatsD.increment('other')
68
+ end
69
+ end
70
+
71
+ assert_assertion_triggered do
72
+ @test_case.assert_statsd_increment('counter') do
73
+ StatsD.gauge('counter', 42)
74
+ end
75
+ end
76
+
77
+ assert_assertion_triggered do
78
+ @test_case.assert_statsd_increment('counter') do
79
+ StatsD.increment('counter')
80
+ StatsD.increment('counter')
81
+ end
82
+ end
83
+
84
+ assert_no_assertion_triggered do
85
+ @test_case.assert_statsd_increment('counter', times: 2) do
86
+ StatsD.increment('counter')
87
+ StatsD.increment('counter')
88
+ end
89
+ end
90
+
91
+ assert_no_assertion_triggered do
92
+ @test_case.assert_statsd_increment('counter', sample_rate: 0.5, tags: ['a', 'b']) do
93
+ StatsD.increment('counter', sample_rate: 0.5, tags: ['a', 'b'])
94
+ end
95
+ end
96
+
97
+ assert_assertion_triggered do
98
+ @test_case.assert_statsd_increment('counter', sample_rate: 0.5, tags: ['a', 'b']) do
99
+ StatsD.increment('counter', sample_rate: 0.2, tags: ['c'])
100
+ end
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def assert_no_assertion_triggered(&block)
107
+ block.call
108
+ rescue MiniTest::Assertion => assertion
109
+ flunk "No assertion trigger expected, but one was triggered with message #{assertion.message}."
110
+ else
111
+ pass
112
+ end
113
+
114
+ def assert_assertion_triggered(message = nil, &block)
115
+ block.call
116
+ rescue MiniTest::Assertion => assertion
117
+ if message
118
+ assert_equal message, assertion.message, "Assertion triggered, but message was not what was expected."
119
+ else
120
+ pass
121
+ end
122
+ assertion
123
+ else
124
+ flunk "No assertion was triggered, but one was expected."
125
+ end
126
+ end
@@ -0,0 +1,24 @@
1
+ require 'test_helper'
2
+
3
+ class CaptureBackendTest < Minitest::Test
4
+ def setup
5
+ @backend = StatsD::Instrument::Backends::CaptureBackend.new
6
+ @metric1 = StatsD::Instrument::Metric::new(type: :c, name: 'mock.counter')
7
+ @metric2 = StatsD::Instrument::Metric::new(type: :ms, name: 'mock.measure', value: 123)
8
+ end
9
+
10
+ def test_collecting_metric
11
+ assert @backend.collected_metrics.empty?
12
+ @backend.collect_metric(@metric1)
13
+ @backend.collect_metric(@metric2)
14
+ assert_equal [@metric1, @metric2], @backend.collected_metrics
15
+ end
16
+
17
+ def test_reset
18
+ @backend.collect_metric(@metric1)
19
+ @backend.reset
20
+ assert @backend.collected_metrics.empty?
21
+ @backend.collect_metric(@metric2)
22
+ assert_equal [@metric2], @backend.collected_metrics
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ class EnvironmentTest < Minitest::Test
4
+
5
+ def setup
6
+ ENV['STATSD_ADDR'] = nil
7
+ ENV['IMPLEMENTATION'] = nil
8
+ end
9
+
10
+ def test_uses_logger_in_development_environment
11
+ StatsD::Instrument::Environment.stubs(:environment).returns('development')
12
+ assert_instance_of StatsD::Instrument::Backends::LoggerBackend, StatsD::Instrument::Environment.default_backend
13
+ end
14
+
15
+ def test_uses_null_backend_in_test_environment
16
+ StatsD::Instrument::Environment.stubs(:environment).returns('test')
17
+ assert_instance_of StatsD::Instrument::Backends::NullBackend, StatsD::Instrument::Environment.default_backend
18
+ end
19
+
20
+ def test_uses_udp_backend_in_production_environment
21
+ StatsD::Instrument::Environment.stubs(:environment).returns('production')
22
+ assert_instance_of StatsD::Instrument::Backends::UDPBackend, StatsD::Instrument::Environment.default_backend
23
+ end
24
+
25
+ def test_uses_environment_variables_in_production_environment
26
+ StatsD::Instrument::Environment.stubs(:environment).returns('production')
27
+ ENV['STATSD_ADDR'] = '127.0.0.1:1234'
28
+ ENV['STATSD_IMPLEMENTATION'] = 'datadog'
29
+
30
+ backend = StatsD::Instrument::Environment.default_backend
31
+ assert_equal '127.0.0.1', backend.host
32
+ assert_equal 1234, backend.port
33
+ assert_equal :datadog, backend.implementation
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+
3
+ class IntegrationTest < Minitest::Test
4
+
5
+ def setup
6
+ @old_backend, StatsD.backend = StatsD.backend, StatsD::Instrument::Backends::UDPBackend.new("localhost:31798")
7
+ end
8
+
9
+ def teardown
10
+ StatsD.backend = @old_backend
11
+ end
12
+
13
+ def test_live_local_udp_socket
14
+ server = UDPSocket.new
15
+ server.bind('localhost', 31798)
16
+
17
+ StatsD.increment('counter')
18
+ assert_equal "counter:1|c", server.recvfrom(100).first
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+
3
+ class LoggerBackendTest < Minitest::Test
4
+ def setup
5
+ logger = Logger.new(@io = StringIO.new)
6
+ logger.formatter = lambda { |_,_,_, msg| "#{msg}\n" }
7
+ @backend = StatsD::Instrument::Backends::LoggerBackend.new(logger)
8
+ @metric1 = StatsD::Instrument::Metric::new(type: :c, name: 'mock.counter', tags: { a: 'b', c: 'd'})
9
+ @metric2 = StatsD::Instrument::Metric::new(type: :ms, name: 'mock.measure', value: 123, sample_rate: 0.3)
10
+ end
11
+
12
+ def test_logs_metrics
13
+ @backend.collect_metric(@metric1)
14
+ assert_equal @io.string, "[StatsD] increment mock.counter:1 #a:b #c:d\n"
15
+ @io.string = ""
16
+
17
+ @backend.collect_metric(@metric2)
18
+ assert_equal @io.string, "[StatsD] measure mock.measure:123 @0.3\n"
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ class MetricTest < Minitest::Test
4
+
5
+ def test_required_arguments
6
+ assert_raises(ArgumentError) { StatsD::Instrument::Metric.new(type: :c) }
7
+ assert_raises(ArgumentError) { StatsD::Instrument::Metric.new(name: 'test') }
8
+ assert_raises(ArgumentError) { StatsD::Instrument::Metric.new(type: :ms, name: 'test') }
9
+ end
10
+
11
+ def test_default_values
12
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'counter')
13
+ assert_equal 1, m.value
14
+ assert_equal StatsD.default_sample_rate, m.sample_rate
15
+ assert m.tags.nil?
16
+ end
17
+
18
+ def test_name_prefix
19
+ StatsD.stubs(:prefix).returns('prefix')
20
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'counter')
21
+ assert_equal 'prefix.counter', m.name
22
+
23
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'counter', no_prefix: true)
24
+ assert_equal 'counter', m.name
25
+ end
26
+
27
+ def test_rewrite_shitty_tags
28
+ assert_equal ['igno_red'], StatsD::Instrument::Metric.normalize_tags(['igno,red'])
29
+ assert_equal ['igno_red'], StatsD::Instrument::Metric.normalize_tags(['igno red'])
30
+ assert_equal ['test:test_test'], StatsD::Instrument::Metric.normalize_tags(['test:test:test'])
31
+ assert_equal ['topic:foo_foo', 'bar_'], StatsD::Instrument::Metric.normalize_tags(['topic:foo : foo', 'bar '])
32
+ end
33
+
34
+ def test_rewrite_tags_provided_as_hash
35
+ assert_equal ['tag:value'], StatsD::Instrument::Metric.normalize_tags(:tag => 'value')
36
+ assert_equal ['tag:value', 'tag2:value2'], StatsD::Instrument::Metric.normalize_tags(:tag => 'value', :tag2 => 'value2')
37
+ end
38
+ end