statsd-instrument 2.3.2 → 2.6.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.
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