statsd-instrument 2.1.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4fc69c05df312c43b484dde1e38e7d1ea4258e38
4
- data.tar.gz: 74480d14bbbad40d9e089144735bcd982258119f
3
+ metadata.gz: 1888f7964454431b2167c0c253ea7c53de3c1afb
4
+ data.tar.gz: 74c6d0f9f44eb989bcfdc44cfea912fb07c1447a
5
5
  SHA512:
6
- metadata.gz: 248787123782dfebb2e8a24be83aa793ae2b7490fb9859373999446441425305c5a69c306f2c7fb58b19ee73bf2f90272f53b8e28615c64f3983c40a9f34b875
7
- data.tar.gz: 690c614ac56b4937d6d7afec393a037491e41bf4c128efccdec92c4c2fbc62cc5f58f8b911417de171408c63f066b7b44047d33050323ad75ffb61ae0354c65c
6
+ metadata.gz: b2faeaa7145f89905c966f08b044bb90b544f53f9bc5de0b1aa29bcf7731e3807f70ad114692a31481e19693d761f74253702791c24212f173c3254dc466f9dc
7
+ data.tar.gz: 8f7d60c2a4144afb033adbb18435373d7bba35702782e835642923a09dff7ec8028fbf7de655c7031aa6102a8df698685af0b71b6bca4195ca17e86cb20d01fa
data/CHANGELOG.md CHANGED
@@ -5,6 +5,10 @@ please at an entry to the "unreleased changes" section below.
5
5
 
6
6
  ### Unreleased changes
7
7
 
8
+ ### Version 2.2.0
9
+
10
+ - Add support for two new datadog specific metric types: events and service checks.
11
+
8
12
  ### Version 2.1.3
9
13
 
10
14
  - The `assert_statsd_calls` test helper will now raise an exception whenever a block isn't passed.
data/CONTRIBUTING.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Contributing
2
2
 
3
- This project is MIT licensed and welcomes outside contributions.
3
+ This project is MIT licensed.
4
+
5
+ > **Note**: this project is currently not actively maintained, but is heavily used in production.
6
+ > As a result, pull requests and issues may not be responded to. Also, due to the limited time we have
7
+ > avaibale to work on this library, we cannot accept PRs that do not maintain backwards compatibility,
8
+ > or PRs that would affect the performance of the hot code paths.
4
9
 
5
10
  ## Reporting issues
6
11
 
@@ -17,7 +22,6 @@ When reporting issues, please incldue the following information:
17
22
  1. Fork the repository, and create a branch.
18
23
  2. Implement the feature or bugfix, and add tests that cover the changed functionality.
19
24
  3. Create a pull request. Make sure that you get Travis CI passes.
20
- 4. Ping **@jstorimer** and/or **@wvanbergen** for a code review.
21
25
 
22
26
  Some notes:
23
27
 
@@ -26,6 +30,9 @@ Some notes:
26
30
  - Add an entry to the "unreleased changes" section of [CHANGELOG.md](./CHANGELOG.md).
27
31
  - **Do not** update `StatsD::Instrument::VERSION`. This will be done during the release prodecure.
28
32
 
33
+ > **Important:** if you change anything in the hot code path (sending a StatsD metric), please
34
+ > include benchmarks to show the performance impact of your changes.
35
+
29
36
  ## Release procedure
30
37
 
31
38
  1. Update the version number in `lib/statsd/instrument/version.rb`.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # StatsD client for Ruby apps
2
2
 
3
- [![Built on Travis](https://secure.travis-ci.org/Shopify/statsd-instrument.png?branch=master)](https://secure.travis-ci.org/Shopify/statsd-instrument)
3
+ [![Built on Travis](https://secure.travis-ci.org/Shopify/statsd-instrument.svg?branch=master)](https://secure.travis-ci.org/Shopify/statsd-instrument)
4
4
 
5
5
  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.
6
6
 
@@ -106,6 +106,32 @@ StatsD.set('GoogleBase.customers', "12345", sample_rate: 1.0)
106
106
 
107
107
  Because you are counting unique values, the results of using a sampling value less than 1.0 can lead to unexpected, hard to interpret results.
108
108
 
109
+ #### StatsD.event
110
+
111
+ An event is a (title, text) tuple that can be used to correlate metrics with something that occured within the system.
112
+ This is a good fit for instance to correlate response time variation with a deploy of the new code.
113
+
114
+ ```ruby
115
+ StatsD.event('shipit.deploy', 'started', sample_rate: 1.0)
116
+ ```
117
+
118
+ *Note: This is only supported by the [datadog implementatation](https://docs.datadoghq.com/guides/dogstatsd/#events).*
119
+
120
+ Events support additional metadata such as `date_happened`, `hostname`, `aggregation_key`, `priority`, `source_type_name`, `alert_type`.
121
+
122
+ #### StatsD.service_check
123
+
124
+ An event is a (check_name, status) tuple that can be used to monitor the status of services your application depends on.
125
+
126
+ ```ruby
127
+ StatsD.service_check('shipit.redis_connection', 'ok')
128
+ ```
129
+
130
+ *Note: This is only supported by the [datadog implementatation](https://docs.datadoghq.com/guides/dogstatsd/#service-checks).*
131
+
132
+ Service checks support additional metadata such as `timestamp`, `hostname`, `message`.
133
+
134
+
109
135
  ### Metaprogramming Methods
110
136
 
111
137
  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.
@@ -191,7 +217,7 @@ GoogleBase.statsd_count :insert, lambda{|object, args| object.class.to_s.downcas
191
217
 
192
218
  ### Tags
193
219
 
194
- The Datadog implementation support tags, which you can use to slice and dice metrics in their UI. You can specify a list of tags as an option, either standalone tag (e.g. `"mytag"`), or key value based, separated by a colon: `"env:production"`.
220
+ The Datadog implementation supports tags, which you can use to slice and dice metrics in their UI. You can specify a list of tags as an option, either standalone tag (e.g. `"mytag"`), or key value based, separated by a colon: `"env:production"`.
195
221
 
196
222
  ``` ruby
197
223
  StatsD.increment('my.counter', tags: ['env:production', 'unicorn'])
@@ -3,12 +3,86 @@ require 'monitor'
3
3
  module StatsD::Instrument::Backends
4
4
  class UDPBackend < StatsD::Instrument::Backend
5
5
 
6
+ class DogStatsDProtocol
7
+ EVENT_OPTIONS = {
8
+ date_happened: 'd',
9
+ hostname: 'h',
10
+ aggregation_key: 'k',
11
+ priority: 'p',
12
+ source_type_name: 's',
13
+ alert_type: 't',
14
+ }
15
+
16
+ SERVICE_CHECK_OPTIONS = {
17
+ timestamp: 'd',
18
+ hostname: 'h',
19
+ message: 'm',
20
+ }
21
+
22
+ def supported?(metric)
23
+ [:c, :ms, :g, :h, :s, :_e, :_sc].include?(metric.type)
24
+ end
25
+
26
+ def generate_packet(metric)
27
+ packet = ""
28
+
29
+ if metric.type == :_e
30
+ escaped_title = metric.name.tr('\n', '\\n')
31
+ escaped_text = metric.value.tr('\n', '\\n')
32
+
33
+ packet << "_e{#{escaped_title.size},#{escaped_text.size}}:#{escaped_title}|#{escaped_text}"
34
+ packet << generate_metadata(metric, EVENT_OPTIONS)
35
+ elsif metric.type == :_sc
36
+ packet << "_sc|#{metric.name}|#{metric.value}"
37
+ packet << generate_metadata(metric, SERVICE_CHECK_OPTIONS)
38
+ else
39
+ packet << "#{metric.name}:#{metric.value}|#{metric.type}"
40
+ end
41
+
42
+ packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
43
+ packet << "|##{metric.tags.join(',')}" if metric.tags
44
+ packet
45
+ end
46
+
47
+ private
48
+
49
+ def generate_metadata(metric, options)
50
+ (metric.metadata.keys & options.keys).map do |key|
51
+ "|#{options[key]}:#{metric.metadata[key]}"
52
+ end.join
53
+ end
54
+ end
55
+
56
+ class StatsiteStatsDProtocol
57
+ def supported?(metric)
58
+ [:c, :ms, :g, :s, :kv].include?(metric.type)
59
+ end
60
+
61
+ def generate_packet(metric)
62
+ packet = "#{metric.name}:#{metric.value}|#{metric.type}"
63
+ packet << "|@#{metric.sample_rate}" unless metric.sample_rate == 1
64
+ packet << "\n"
65
+ packet
66
+ end
67
+ end
68
+
69
+ class StatsDProtocol
70
+ def supported?(metric)
71
+ [:c, :ms, :g, :s].include?(metric.type)
72
+ end
73
+
74
+ def generate_packet(metric)
75
+ packet = "#{metric.name}:#{metric.value}|#{metric.type}"
76
+ packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
77
+ packet
78
+ end
79
+ end
80
+
6
81
  DEFAULT_IMPLEMENTATION = :statsd
7
82
 
8
83
  include MonitorMixin
9
84
 
10
- attr_reader :host, :port
11
- attr_accessor :implementation
85
+ attr_reader :host, :port, :implementation
12
86
 
13
87
  def initialize(server = nil, implementation = nil)
14
88
  super()
@@ -16,8 +90,20 @@ module StatsD::Instrument::Backends
16
90
  self.implementation = (implementation || DEFAULT_IMPLEMENTATION).to_sym
17
91
  end
18
92
 
93
+ def implementation=(value)
94
+ @packet_factory = case value
95
+ when :datadog
96
+ DogStatsDProtocol.new
97
+ when :statsite
98
+ StatsiteStatsDProtocol.new
99
+ else
100
+ StatsDProtocol.new
101
+ end
102
+ @implementation = value
103
+ end
104
+
19
105
  def collect_metric(metric)
20
- unless implementation_supports_metric_type?(metric.type)
106
+ unless @packet_factory.supported?(metric)
21
107
  StatsD.logger.warn("[StatsD] Metric type #{metric.type.inspect} not supported on #{implementation} implementation.")
22
108
  return false
23
109
  end
@@ -26,20 +112,12 @@ module StatsD::Instrument::Backends
26
112
  return false
27
113
  end
28
114
 
29
- write_packet(generate_packet(metric))
30
- end
31
-
32
- def implementation_supports_metric_type?(type)
33
- case type
34
- when :h; implementation == :datadog
35
- when :kv; implementation == :statsite
36
- else true
37
- end
115
+ write_packet(@packet_factory.generate_packet(metric))
38
116
  end
39
117
 
40
118
  def server=(connection_string)
41
- self.host, port = connection_string.split(':', 2)
42
- self.port = port.to_i
119
+ @host, @port = connection_string.split(':', 2)
120
+ @port = @port.to_i
43
121
  invalidate_socket
44
122
  end
45
123
 
@@ -61,25 +139,6 @@ module StatsD::Instrument::Backends
61
139
  @socket
62
140
  end
63
141
 
64
- def generate_packet(metric)
65
- command = "#{metric.name}:#{metric.value}|#{metric.type}"
66
- command << "|@#{metric.sample_rate}" if metric.sample_rate < 1 || (implementation == :statsite && metric.sample_rate > 1)
67
- if metric.tags
68
- if tags_supported?
69
- command << "|##{metric.tags.join(',')}"
70
- else
71
- StatsD.logger.warn("[StatsD] Tags are only supported on Datadog implementation.")
72
- end
73
- end
74
-
75
- command << "\n" if implementation == :statsite
76
- command
77
- end
78
-
79
- def tags_supported?
80
- implementation == :datadog
81
- end
82
-
83
142
  def write_packet(command)
84
143
  synchronize do
85
144
  socket.send(command, 0) > 0
@@ -28,7 +28,7 @@
28
28
  #
29
29
  class StatsD::Instrument::Metric
30
30
 
31
- attr_accessor :type, :name, :value, :sample_rate, :tags
31
+ attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata
32
32
 
33
33
  # Initializes a new metric instance.
34
34
  # Normally, you don't want to call this method directly, but use one of the metric collection
@@ -46,11 +46,13 @@ class StatsD::Instrument::Metric
46
46
  def initialize(options = {})
47
47
  @type = options[:type] or raise ArgumentError, "Metric :type is required."
48
48
  @name = options[:name] or raise ArgumentError, "Metric :name is required."
49
+ @name = normalize_name(@name)
49
50
  @name = StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name unless options[:no_prefix]
50
51
 
51
52
  @value = options[:value] || default_value
52
53
  @sample_rate = options[:sample_rate] || StatsD.default_sample_rate
53
54
  @tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
55
+ @metadata = options.reject { |k, _| [:type, :name, :value, :sample_rate, :tags].include?(k) }
54
56
  end
55
57
 
56
58
  # The default value for this metric, which will be used if it is not set.
@@ -93,6 +95,14 @@ class StatsD::Instrument::Metric
93
95
  s: 'set',
94
96
  }
95
97
 
98
+ # Strip metric names of special characters used by StatsD line protocol, replace with underscore
99
+ #
100
+ # @param name [String]
101
+ # @return [String]
102
+ def normalize_name(name)
103
+ name.tr(':|@'.freeze, '_')
104
+ end
105
+
96
106
  # Utility function to convert tags to the canonical form.
97
107
  #
98
108
  # - Tags specified as key value pairs will be converted into an array
@@ -1,5 +1,5 @@
1
1
  module StatsD
2
2
  module Instrument
3
- VERSION = "2.1.4"
3
+ VERSION = "2.2.0"
4
4
  end
5
5
  end
@@ -369,6 +369,36 @@ module StatsD
369
369
  collect_metric(hash_argument(metric_options).merge(type: :s, name: key, value: value))
370
370
  end
371
371
 
372
+ # Emits an event metric.
373
+ # @param title [String] Title of the event.
374
+ # @param text [String] Body of the event.
375
+ # @param metric_options [Hash] (default: {}) Metric options
376
+ # @return (see #collect_metric)
377
+ # @note Supported by the datadog implementation only.
378
+ def event(title, text, *metric_options)
379
+ if text.is_a?(Hash) && metric_options.empty?
380
+ metric_options = [text]
381
+ text = text.fetch(:text, nil)
382
+ end
383
+
384
+ collect_metric(hash_argument(metric_options).merge(type: :_e, name: title, value: text))
385
+ end
386
+
387
+ # Emits a service check metric.
388
+ # @param title [String] Title of the event.
389
+ # @param text [String] Body of the event.
390
+ # @param metric_options [Hash] (default: {}) Metric options
391
+ # @return (see #collect_metric)
392
+ # @note Supported by the datadog implementation only.
393
+ def service_check(name, status, *metric_options)
394
+ if status.is_a?(Hash) && metric_options.empty?
395
+ metric_options = [status]
396
+ status = status.fetch(:status, nil)
397
+ end
398
+
399
+ collect_metric(hash_argument(metric_options).merge(type: :_sc, name: name, value: status))
400
+ end
401
+
372
402
  private
373
403
 
374
404
  # Converts old-style ordered arguments in an argument hash for backwards compatibility.
data/test/metric_test.rb CHANGED
@@ -24,6 +24,15 @@ class MetricTest < Minitest::Test
24
24
  assert_equal 'counter', m.name
25
25
  end
26
26
 
27
+ def test_bad_metric_name
28
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'my:metric', no_prefix: true)
29
+ assert_equal 'my_metric', m.name
30
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'my|metric', no_prefix: true)
31
+ assert_equal 'my_metric', m.name
32
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'my@metric', no_prefix: true)
33
+ assert_equal 'my_metric', m.name
34
+ end
35
+
27
36
  def test_handle_bad_tags
28
37
  assert_equal ['ignored'], StatsD::Instrument::Metric.normalize_tags(['igno|red'])
29
38
  assert_equal ['lol::class:omg::lol'], StatsD::Instrument::Metric.normalize_tags({ :"lol::class" => "omg::lol" })
@@ -76,6 +76,50 @@ class UDPBackendTest < Minitest::Test
76
76
  StatsD.histogram('fooh', 42.4)
77
77
  end
78
78
 
79
+ def test_event_on_datadog
80
+ @backend.implementation = :datadog
81
+ @backend.expects(:write_packet).with('_e{4,3}:fooh|baz|h:localhost:3000|@0.01|#foo')
82
+ StatsD.event('fooh', 'baz', hostname: 'localhost:3000', sample_rate: 0.01, tags: ["foo"])
83
+ end
84
+
85
+ def test_event_on_datadog_escapes_newlines
86
+ @backend.implementation = :datadog
87
+ @backend.expects(:write_packet).with('_e{8,5}:fooh\\n\\n|baz\\n')
88
+ StatsD.event('fooh\n\n', 'baz\n')
89
+ end
90
+
91
+ def test_event_on_datadog_ignores_invalid_metadata
92
+ @backend.implementation = :datadog
93
+ @backend.expects(:write_packet).with('_e{4,3}:fooh|baz')
94
+ StatsD.event('fooh', 'baz', i_am_not_supported: 'not-supported')
95
+ end
96
+
97
+ def test_event_warns_when_not_using_datadog
98
+ @backend.implementation = :other
99
+ @backend.expects(:write_packet).never
100
+ @logger.expects(:warn)
101
+ StatsD.event('fooh', 'bar')
102
+ end
103
+
104
+ def test_service_check_on_datadog
105
+ @backend.implementation = :datadog
106
+ @backend.expects(:write_packet).with('_sc|fooh|baz|h:localhost:3000|@0.01|#foo')
107
+ StatsD.service_check('fooh', 'baz', hostname: 'localhost:3000', sample_rate: 0.01, tags: ["foo"])
108
+ end
109
+
110
+ def test_service_check_on_datadog_ignores_invalid_metadata
111
+ @backend.implementation = :datadog
112
+ @backend.expects(:write_packet).with('_sc|fooh|baz')
113
+ StatsD.service_check('fooh', 'baz', i_am_not_supported: 'not-supported')
114
+ end
115
+
116
+ def test_service_check_warns_when_not_using_datadog
117
+ @backend.implementation = :other
118
+ @backend.expects(:write_packet).never
119
+ @logger.expects(:warn)
120
+ StatsD.service_check('fooh', 'bar')
121
+ end
122
+
79
123
  def test_histogram_warns_if_not_using_datadog
80
124
  @backend.implementation = :other
81
125
  @backend.expects(:write_packet).never
@@ -108,13 +152,6 @@ class UDPBackendTest < Minitest::Test
108
152
  StatsD.increment('fooc', 3, tags: ['topic:foo', 'bar'])
109
153
  end
110
154
 
111
- def test_warn_when_using_tags_and_not_on_datadog
112
- @backend.implementation = :other
113
- @backend.expects(:write_packet).with("fooc:1|c")
114
- @logger.expects(:warn)
115
- StatsD.increment('fooc', tags: ['ignored'])
116
- end
117
-
118
155
  def test_socket_error_should_not_raise_but_log
119
156
  @socket.stubs(:connect).raises(SocketError)
120
157
  @logger.expects(:error)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsd-instrument
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.4
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Storimer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2017-07-26 00:00:00.000000000 Z
13
+ date: 2018-05-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rake
@@ -162,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
162
  version: '0'
163
163
  requirements: []
164
164
  rubyforge_project:
165
- rubygems_version: 2.5.2
165
+ rubygems_version: 2.6.14
166
166
  signing_key:
167
167
  specification_version: 4
168
168
  summary: A StatsD client for Ruby apps