statsd-instrument 2.3.2 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/benchmark.yml +32 -0
  4. data/.github/workflows/ci.yml +47 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
  7. data/.rubocop.yml +50 -0
  8. data/.yardopts +5 -0
  9. data/CHANGELOG.md +288 -2
  10. data/CONTRIBUTING.md +28 -6
  11. data/Gemfile +5 -0
  12. data/README.md +54 -46
  13. data/Rakefile +4 -2
  14. data/benchmark/README.md +29 -0
  15. data/benchmark/datagram-client +41 -0
  16. data/benchmark/send-metrics-to-dev-null-log +47 -0
  17. data/benchmark/send-metrics-to-local-udp-receiver +57 -0
  18. data/lib/statsd/instrument/assertions.rb +179 -30
  19. data/lib/statsd/instrument/backend.rb +3 -2
  20. data/lib/statsd/instrument/backends/capture_backend.rb +4 -1
  21. data/lib/statsd/instrument/backends/logger_backend.rb +3 -3
  22. data/lib/statsd/instrument/backends/null_backend.rb +2 -0
  23. data/lib/statsd/instrument/backends/udp_backend.rb +39 -45
  24. data/lib/statsd/instrument/capture_sink.rb +27 -0
  25. data/lib/statsd/instrument/client.rb +313 -0
  26. data/lib/statsd/instrument/datagram.rb +75 -0
  27. data/lib/statsd/instrument/datagram_builder.rb +101 -0
  28. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +71 -0
  29. data/lib/statsd/instrument/environment.rb +108 -29
  30. data/lib/statsd/instrument/helpers.rb +16 -8
  31. data/lib/statsd/instrument/log_sink.rb +24 -0
  32. data/lib/statsd/instrument/matchers.rb +14 -11
  33. data/lib/statsd/instrument/metric.rb +72 -45
  34. data/lib/statsd/instrument/metric_expectation.rb +32 -18
  35. data/lib/statsd/instrument/null_sink.rb +13 -0
  36. data/lib/statsd/instrument/railtie.rb +2 -1
  37. data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +39 -0
  38. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +42 -0
  39. data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +37 -0
  40. data/lib/statsd/instrument/rubocop/metric_return_value.rb +32 -0
  41. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +36 -0
  42. data/lib/statsd/instrument/rubocop/positional_arguments.rb +99 -0
  43. data/lib/statsd/instrument/rubocop/splat_arguments.rb +31 -0
  44. data/lib/statsd/instrument/rubocop.rb +64 -0
  45. data/lib/statsd/instrument/statsd_datagram_builder.rb +14 -0
  46. data/lib/statsd/instrument/strict.rb +235 -0
  47. data/lib/statsd/instrument/udp_sink.rb +62 -0
  48. data/lib/statsd/instrument/version.rb +3 -1
  49. data/lib/statsd/instrument.rb +340 -163
  50. data/lib/statsd-instrument.rb +2 -0
  51. data/statsd-instrument.gemspec +13 -10
  52. data/test/assertions_test.rb +167 -156
  53. data/test/benchmark/clock_gettime.rb +27 -0
  54. data/test/benchmark/default_tags.rb +47 -0
  55. data/test/benchmark/metrics.rb +9 -8
  56. data/test/benchmark/tags.rb +5 -3
  57. data/test/capture_backend_test.rb +4 -2
  58. data/test/capture_sink_test.rb +44 -0
  59. data/test/client_test.rb +164 -0
  60. data/test/compatibility/dogstatsd_datagram_compatibility_test.rb +162 -0
  61. data/test/datagram_builder_test.rb +120 -0
  62. data/test/deprecations_test.rb +132 -0
  63. data/test/dogstatsd_datagram_builder_test.rb +32 -0
  64. data/test/environment_test.rb +75 -8
  65. data/test/helpers/rubocop_helper.rb +47 -0
  66. data/test/helpers_test.rb +2 -1
  67. data/test/integration_test.rb +31 -7
  68. data/test/log_sink_test.rb +37 -0
  69. data/test/logger_backend_test.rb +10 -8
  70. data/test/matchers_test.rb +42 -28
  71. data/test/metric_test.rb +18 -22
  72. data/test/null_sink_test.rb +13 -0
  73. data/test/rubocop/measure_as_dist_argument_test.rb +44 -0
  74. data/test/rubocop/metaprogramming_positional_arguments_test.rb +58 -0
  75. data/test/rubocop/metric_prefix_argument_test.rb +38 -0
  76. data/test/rubocop/metric_return_value_test.rb +78 -0
  77. data/test/rubocop/metric_value_keyword_argument_test.rb +39 -0
  78. data/test/rubocop/positional_arguments_test.rb +110 -0
  79. data/test/rubocop/splat_arguments_test.rb +27 -0
  80. data/test/statsd_datagram_builder_test.rb +22 -0
  81. data/test/statsd_instrumentation_test.rb +109 -100
  82. data/test/statsd_test.rb +113 -79
  83. data/test/test_helper.rb +12 -1
  84. data/test/udp_backend_test.rb +38 -36
  85. data/test/udp_sink_test.rb +85 -0
  86. metadata +85 -5
  87. data/.travis.yml +0 -12
@@ -0,0 +1,37 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative '../rubocop' unless defined?(RuboCop::Cop::StatsD)
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module StatsD
8
+ # This Rubocop will check for specifying the `prefix` keyword argument on `StatsD` metric
9
+ # methods and `statsd_*` metaprogramming methods. To run this cop on your codebase:
10
+ #
11
+ # rubocop --require `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb \
12
+ # --only StatsD/MetricPrefixArgument
13
+ #
14
+ # This cop will not autocorrect offenses.
15
+ class MetricPrefixArgument < Cop
16
+ include RuboCop::Cop::StatsD
17
+
18
+ MSG = <<~MSG
19
+ Do not use StatsD.metric(..., prefix: "foo"). The prefix argument is deprecated.
20
+
21
+ You can simply include the prefix in the metric name instead.
22
+ If you want to override the global prefix, you can set `no_prefix: true`.
23
+ MSG
24
+
25
+ def on_send(node)
26
+ if metric_method?(node)
27
+ add_offense(node) if has_keyword_argument?(node, :prefix)
28
+ end
29
+
30
+ if metaprogramming_method?(node)
31
+ add_offense(node) if has_keyword_argument?(node, :prefix)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative '../rubocop' unless defined?(RuboCop::Cop::StatsD)
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module StatsD
8
+ # This Rubocop will check for using the return value of StatsD metric calls, which is deprecated.
9
+ # To check your codebase, use the following Rubocop invocation:
10
+ #
11
+ # rubocop --require `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb \
12
+ # --only StatsD/MetricReturnValue
13
+ #
14
+ # This cop cannot autocorrect offenses. In production code, StatsD should be used in a fire-and-forget
15
+ # fashion. This means that you shouldn't rely on the return value. If you really need to access the
16
+ # emitted metrics, you can look into `capture_statsd_calls`
17
+ class MetricReturnValue < Cop
18
+ include RuboCop::Cop::StatsD
19
+
20
+ MSG = 'Do not use the return value of StatsD metric methods'
21
+
22
+ INVALID_PARENTS = %i{lvasgn array pair send return yield}
23
+
24
+ def on_send(node)
25
+ if metric_method?(node) && node.arguments.last&.type != :block_pass
26
+ add_offense(node.parent) if INVALID_PARENTS.include?(node.parent&.type)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative '../rubocop' unless defined?(RuboCop::Cop::StatsD)
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module StatsD
8
+ # This Rubocop will check for providing the value for a metric using a keyword argument, which is
9
+ # deprecated. Use the following Rubocop invocation to check your project's codebase:
10
+ #
11
+ # rubocop --require \
12
+ # `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb \
13
+ # --only StatsD/MetricValueKeywordArgument
14
+ #
15
+ # This cop will not autocorrect offenses. Most of the time, these are easy to fix by providing the
16
+ # value as the second argument, rather than a keyword argument.
17
+ #
18
+ # `StatsD.increment('foo', value: 3)` => `StatsD.increment('foo', 3)`
19
+ class MetricValueKeywordArgument < Cop
20
+ include RuboCop::Cop::StatsD
21
+
22
+ MSG = <<~MSG
23
+ Do not use the StatsD.metric('name', value: <value>, ...). The `value` keyword argument is deprecated.
24
+
25
+ Use a positional argument instead: StatsD.metric('name', <value>, ...).
26
+ MSG
27
+
28
+ def on_send(node)
29
+ if metric_method?(node) && has_keyword_argument?(node, :value)
30
+ add_offense(node)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,99 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative '../rubocop' unless defined?(RuboCop::Cop::StatsD)
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module StatsD
8
+ # This Rubocop will check for using the StatsD metric methods (e.g. `StatsD.instrument`)
9
+ # for positional argument usage, which is deprecated.
10
+ #
11
+ # Use the following Rubocop invocation to check your project's codebase:
12
+ #
13
+ # rubocop --require `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb \
14
+ # --only StatsD/PositionalArguments
15
+ #
16
+ # This cop can autocorrect some offenses it finds, but not all of them.
17
+ class PositionalArguments < Cop
18
+ include RuboCop::Cop::StatsD
19
+
20
+ MSG = 'Use keyword arguments for StatsD calls'
21
+
22
+ POSITIONAL_ARGUMENT_TYPES = Set[:int, :float, :nil]
23
+ UNKNOWN_ARGUMENT_TYPES = Set[:send, :const, :lvar, :splat]
24
+ REFUSED_ARGUMENT_TYPES = POSITIONAL_ARGUMENT_TYPES | UNKNOWN_ARGUMENT_TYPES
25
+
26
+ KEYWORD_ARGUMENT_TYPES = Set[:hash]
27
+ BLOCK_ARGUMENT_TYPES = Set[:block_pass]
28
+ ACCEPTED_ARGUMENT_TYPES = KEYWORD_ARGUMENT_TYPES | BLOCK_ARGUMENT_TYPES
29
+
30
+ def on_send(node)
31
+ if metric_method?(node) && node.arguments.length >= 3
32
+ case node.arguments[2].type
33
+ when *REFUSED_ARGUMENT_TYPES
34
+ add_offense(node)
35
+ when *ACCEPTED_ARGUMENT_TYPES
36
+ nil
37
+ else
38
+ $stderr.puts "[StatsD/PositionalArguments] Unhandled argument type: #{node.arguments[2].type.inspect}"
39
+ end
40
+ end
41
+ end
42
+
43
+ def autocorrect(node)
44
+ -> (corrector) do
45
+ positial_arguments = if node.arguments.last.type == :block_pass
46
+ node.arguments[2...node.arguments.length - 1]
47
+ else
48
+ node.arguments[2...node.arguments.length]
49
+ end
50
+
51
+ case positial_arguments[0].type
52
+ when *UNKNOWN_ARGUMENT_TYPES
53
+ # We don't know whether the method returns a hash, in which case it would be interpreted
54
+ # as keyword arguments. In this case, the fix would be to add a keywordf splat:
55
+ #
56
+ # `StatsD.instrument('foo', 1, method_call)`
57
+ # => `StatsD.instrument('foo', 1, **method_call)`
58
+ #
59
+ # However, it's also possible this method returns a sample rate, in which case the fix
60
+ # above will not do the right thing.
61
+ #
62
+ # `StatsD.instrument('foo', 1, SAMPLE_RATE_CONSTANT)`
63
+ # => `StatsD.instrument('foo', 1, sample_rate: SAMPLE_RATE_CONSTANT)`
64
+ #
65
+ # Because of this, we will not auto-correct and let the user fix the issue manually.
66
+ return
67
+
68
+ when *POSITIONAL_ARGUMENT_TYPES
69
+ value_argument = node.arguments[1]
70
+ from = value_argument.source_range.end_pos
71
+ to = positial_arguments.last.source_range.end_pos
72
+ range = Parser::Source::Range.new(node.source_range.source_buffer, from, to)
73
+ corrector.remove(range)
74
+
75
+ keyword_arguments = []
76
+ sample_rate = positial_arguments[0]
77
+ if sample_rate && sample_rate.type != :nil
78
+ keyword_arguments << "sample_rate: #{sample_rate.source}"
79
+ end
80
+
81
+ tags = positial_arguments[1]
82
+ if tags && tags.type != :nil
83
+ keyword_arguments << if tags.type == :hash && tags.source[0] != '{'
84
+ "tags: { #{tags.source} }"
85
+ else
86
+ "tags: #{tags.source}"
87
+ end
88
+ end
89
+
90
+ unless keyword_arguments.empty?
91
+ corrector.insert_after(value_argument.source_range, ", #{keyword_arguments.join(', ')}")
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,31 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative '../rubocop' unless defined?(RuboCop::Cop::StatsD)
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module StatsD
8
+ # This Rubocop will check for using splat arguments (*args) in StatsD metric calls. To run
9
+ # this rule on your codebase, invoke Rubocop this way:
10
+ #
11
+ # rubocop --require \
12
+ # `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb \
13
+ # --only StatsD/SplatArguments
14
+ #
15
+ # This cop will not autocorrect offenses.
16
+ class SplatArguments < Cop
17
+ include RuboCop::Cop::StatsD
18
+
19
+ MSG = 'Do not use splat arguments in StatsD metric calls'
20
+
21
+ def on_send(node)
22
+ if metric_method?(node)
23
+ if node.arguments.any? { |arg| arg.type == :splat }
24
+ add_offense(node)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module StatsD
6
+ METRIC_METHODS = %i{
7
+ increment
8
+ gauge
9
+ measure
10
+ set
11
+ histogram
12
+ distribution
13
+ key_value
14
+ }
15
+
16
+ METAPROGRAMMING_METHODS = %i{
17
+ statsd_measure
18
+ statsd_distribution
19
+ statsd_count_success
20
+ statsd_count_if
21
+ statsd_count
22
+ }
23
+
24
+ private
25
+
26
+ def metaprogramming_method?(node)
27
+ METAPROGRAMMING_METHODS.include?(node.method_name)
28
+ end
29
+
30
+ def metric_method?(node)
31
+ node.receiver&.type == :const &&
32
+ node.receiver&.const_name == "StatsD" &&
33
+ METRIC_METHODS.include?(node.method_name)
34
+ end
35
+
36
+ def has_keyword_argument?(node, sym)
37
+ if (kwargs = keyword_arguments(node))
38
+ kwargs.child_nodes.detect do |pair|
39
+ pair.child_nodes[0]&.type == :sym && pair.child_nodes[0].value == sym
40
+ end
41
+ end
42
+ end
43
+
44
+ def keyword_arguments(node)
45
+ return nil if node.arguments.empty?
46
+ last_argument = if node.arguments.last&.type == :block_pass
47
+ node.arguments[node.arguments.length - 2]
48
+ else
49
+ node.arguments[node.arguments.length - 1]
50
+ end
51
+
52
+ last_argument&.type == :hash ? last_argument : nil
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ require_relative 'rubocop/metaprogramming_positional_arguments'
59
+ require_relative 'rubocop/metric_return_value'
60
+ require_relative 'rubocop/metric_value_keyword_argument'
61
+ require_relative 'rubocop/positional_arguments'
62
+ require_relative 'rubocop/splat_arguments'
63
+ require_relative 'rubocop/measure_as_dist_argument'
64
+ require_relative 'rubocop/metric_prefix_argument'
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @note This class is part of the new Client implementation that is intended
4
+ # to become the new default in the next major release of this library.
5
+ class StatsD::Instrument::StatsDDatagramBuilder < StatsD::Instrument::DatagramBuilder
6
+ unsupported_datagram_types :h, :d, :kv
7
+
8
+ protected
9
+
10
+ def normalize_tags(tags)
11
+ raise NotImplementedError, "#{self.class.name} does not support tags" if tags
12
+ super
13
+ end
14
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'statsd-instrument' unless Object.const_defined?(:StatsD)
4
+
5
+ module StatsD
6
+ module Instrument
7
+ UNSPECIFIED = Object.new.freeze
8
+ private_constant :UNSPECIFIED
9
+
10
+ # The Strict monkeypatch can be loaded to see if you're using the StatsD library in
11
+ # a deprecated way.
12
+ #
13
+ # - The metric methods are not retuning a Metric instance.
14
+ # - Only accept keyword arguments for tags and sample_rate, rather than position arguments.
15
+ # - Only accept a position argument for value, rather than a keyword argument.
16
+ # - The provided arguments have the right type.
17
+ #
18
+ # You can enable thois monkeypatch by changing your Gemfile as follows:
19
+ #
20
+ # gem 'statsd-instrument', require: 'statsd/instrument/strict'
21
+ #
22
+ # By doing this as part of your QA/CI, you can find where you are still using deprecated patterns,
23
+ # and fix them before the deprecated behavior is removed in the next major version.
24
+ #
25
+ # This monkeypatch is not meant to be used in production.
26
+ module Strict
27
+ def increment(key, value = 1, sample_rate: nil, tags: nil, no_prefix: false)
28
+ raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
29
+ raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Integer)
30
+ check_tags_and_sample_rate(sample_rate, tags)
31
+
32
+ super
33
+ end
34
+
35
+ def gauge(key, value, sample_rate: nil, tags: nil, no_prefix: false)
36
+ raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
37
+ raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
38
+ check_tags_and_sample_rate(sample_rate, tags)
39
+
40
+ super
41
+ end
42
+
43
+ def histogram(key, value, sample_rate: nil, tags: nil, no_prefix: false)
44
+ raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
45
+ raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
46
+ check_tags_and_sample_rate(sample_rate, tags)
47
+
48
+ super
49
+ end
50
+
51
+ def set(key, value, sample_rate: nil, tags: nil, no_prefix: false)
52
+ raise ArgumentError, "StatsD.set does not accept a block" if block_given?
53
+ check_tags_and_sample_rate(sample_rate, tags)
54
+
55
+ super
56
+ end
57
+
58
+ def service_check(name, status, tags: nil, no_prefix: false,
59
+ hostname: nil, timestamp: nil, message: nil)
60
+
61
+ super
62
+ end
63
+
64
+ def event(title, text, tags: nil, no_prefix: false,
65
+ hostname: nil, timestamp: nil, aggregation_key: nil, priority: nil, source_type_name: nil, alert_type: nil)
66
+
67
+ super
68
+ end
69
+
70
+ def measure(key, value = UNSPECIFIED, sample_rate: nil, tags: nil, no_prefix: false, &block)
71
+ check_block_or_numeric_value(value, &block)
72
+ check_tags_and_sample_rate(sample_rate, tags)
73
+
74
+ super
75
+ end
76
+
77
+ def distribution(key, value = UNSPECIFIED, sample_rate: nil, tags: nil, no_prefix: false, &block)
78
+ check_block_or_numeric_value(value, &block)
79
+ check_tags_and_sample_rate(sample_rate, tags)
80
+
81
+ super
82
+ end
83
+
84
+ private
85
+
86
+ def check_block_or_numeric_value(value)
87
+ if block_given?
88
+ raise ArgumentError, "The value argument should not be set when providing a block" unless value == UNSPECIFIED
89
+ else
90
+ raise ArgumentError, "The value argument should be a number, got #{value.inspect}" unless value.is_a?(Numeric)
91
+ end
92
+ end
93
+
94
+ def check_tags_and_sample_rate(sample_rate, tags)
95
+ unless sample_rate.nil? || sample_rate.is_a?(Numeric)
96
+ raise ArgumentError, "The sample_rate argument should be a number, got #{sample_rate}"
97
+ end
98
+ unless tags.nil? || tags.is_a?(Hash) || tags.is_a?(Array)
99
+ raise ArgumentError, "The tags argument should be a hash or an array, got #{tags.inspect}"
100
+ end
101
+ end
102
+
103
+ def collect_metric(type, name, value, sample_rate:, tags: nil, prefix:, metadata: nil)
104
+ super
105
+ nil # We explicitly discard the return value, so people cannot depend on it.
106
+ end
107
+ end
108
+
109
+ module StrictMetaprogramming
110
+ def statsd_measure(method, name, sample_rate: nil, tags: nil, no_prefix: false)
111
+ check_method_and_metric_name(method, name)
112
+
113
+ # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
114
+ # Stats.measure call to not use the `as_dist` and `prefix` arguments.
115
+ add_to_method(method, name, :measure) do
116
+ define_method(method) do |*args, &block|
117
+ key = StatsD::Instrument.generate_metric_name(name, self, *args)
118
+ StatsD.measure(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix) do
119
+ super(*args, &block)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def statsd_distribution(method, name, sample_rate: nil, tags: nil, no_prefix: false)
126
+ check_method_and_metric_name(method, name)
127
+
128
+ # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
129
+ # Stats.distribution call to not use the `prefix` argument.
130
+
131
+ add_to_method(method, name, :distribution) do
132
+ define_method(method) do |*args, &block|
133
+ key = StatsD::Instrument.generate_metric_name(name, self, *args)
134
+ StatsD.distribution(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix) do
135
+ super(*args, &block)
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def statsd_count_success(method, name, sample_rate: nil, tags: nil, no_prefix: false)
142
+ check_method_and_metric_name(method, name)
143
+
144
+ # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
145
+ # Stats.increment call to not use the `prefix` argument.
146
+
147
+ add_to_method(method, name, :count_success) do
148
+ define_method(method) do |*args, &block|
149
+ begin
150
+ truthiness = result = super(*args, &block)
151
+ rescue
152
+ truthiness = false
153
+ raise
154
+ else
155
+ if block_given?
156
+ begin
157
+ truthiness = yield(result)
158
+ rescue
159
+ truthiness = false
160
+ end
161
+ end
162
+ result
163
+ ensure
164
+ suffix = truthiness == false ? 'failure' : 'success'
165
+ key = "#{StatsD::Instrument.generate_metric_name(name, self, *args)}.#{suffix}"
166
+ StatsD.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def statsd_count_if(method, name, sample_rate: nil, tags: nil, no_prefix: false)
173
+ check_method_and_metric_name(method, name)
174
+
175
+ # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
176
+ # Stats.increment call to not use the `prefix` argument.
177
+
178
+ add_to_method(method, name, :count_if) do
179
+ define_method(method) do |*args, &block|
180
+ begin
181
+ truthiness = result = super(*args, &block)
182
+ rescue
183
+ truthiness = false
184
+ raise
185
+ else
186
+ if block_given?
187
+ begin
188
+ truthiness = yield(result)
189
+ rescue
190
+ truthiness = false
191
+ end
192
+ end
193
+ result
194
+ ensure
195
+ if truthiness
196
+ key = StatsD::Instrument.generate_metric_name(name, self, *args)
197
+ StatsD.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+
204
+ def statsd_count(method, name, sample_rate: nil, tags: nil, no_prefix: false)
205
+ check_method_and_metric_name(method, name)
206
+
207
+ # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
208
+ # Stats.increment call to not use the `prefix` argument.
209
+
210
+ add_to_method(method, name, :count) do
211
+ define_method(method) do |*args, &block|
212
+ key = StatsD::Instrument.generate_metric_name(name, self, *args)
213
+ StatsD.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
214
+ super(*args, &block)
215
+ end
216
+ end
217
+ end
218
+
219
+ private
220
+
221
+ def check_method_and_metric_name(method, metric_name)
222
+ unless method.is_a?(Symbol)
223
+ raise ArgumentError, "The method name should be provided as symbol, got #{method.inspect}"
224
+ end
225
+
226
+ unless metric_name.is_a?(String) || metric_name.is_a?(Proc)
227
+ raise ArgumentError, "The metric name should be a proc or string, got #{metric_name.inspect}"
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ StatsD.singleton_class.prepend(StatsD::Instrument::Strict)
235
+ StatsD::Instrument.prepend(StatsD::Instrument::StrictMetaprogramming)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @note This class is part of the new Client implementation that is intended
4
+ # to become the new default in the next major release of this library.
5
+ class StatsD::Instrument::UDPSink
6
+ def self.for_addr(addr)
7
+ host, port_as_string = addr.split(':', 2)
8
+ new(host, Integer(port_as_string))
9
+ end
10
+
11
+ attr_reader :host, :port
12
+
13
+ def initialize(host, port)
14
+ @host = host
15
+ @port = port
16
+ @mutex = Mutex.new
17
+ @socket = nil
18
+ end
19
+
20
+ def sample?(sample_rate)
21
+ sample_rate == 1 || rand < sample_rate
22
+ end
23
+
24
+ def <<(datagram)
25
+ with_socket { |socket| socket.send(datagram, 0) > 0 }
26
+ self
27
+
28
+ rescue ThreadError
29
+ # In cases where a TERM or KILL signal has been sent, and we send stats as
30
+ # part of a signal handler, locks cannot be acquired, so we do our best
31
+ # to try and send the datagram without a lock.
32
+ socket.send(datagram, 0) > 0
33
+
34
+ rescue SocketError, IOError, SystemCallError
35
+ # TODO: log?
36
+ invalidate_socket
37
+ end
38
+
39
+ def addr
40
+ "#{host}:#{port}"
41
+ end
42
+
43
+ private
44
+
45
+ def with_socket
46
+ @mutex.synchronize { yield(socket) }
47
+ end
48
+
49
+ def socket
50
+ if @socket.nil?
51
+ @socket = UDPSocket.new
52
+ @socket.connect(@host, @port)
53
+ end
54
+ @socket
55
+ end
56
+
57
+ def invalidate_socket
58
+ @mutex.synchronize do
59
+ @socket = nil
60
+ end
61
+ end
62
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StatsD
2
4
  module Instrument
3
- VERSION = "2.3.2"
5
+ VERSION = "2.6.0"
4
6
  end
5
7
  end