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
@@ -1,25 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
 
3
5
  # The environment module is used to detect, and initialize the environment in
4
6
  # which this library is active. It will use different default values based on the environment.
5
- module StatsD::Instrument::Environment
6
- extend self
7
+ class StatsD::Instrument::Environment
8
+ class << self
9
+ def from_env
10
+ @from_env ||= StatsD::Instrument::Environment.new(ENV)
11
+ end
7
12
 
8
- # Instantiates a default backend for the current environment.
9
- #
10
- # @return [StatsD::Instrument::Backend]
11
- # @see #environment
12
- def default_backend
13
- case environment
14
- when 'production', 'staging'
15
- StatsD::Instrument::Backends::UDPBackend.new(ENV['STATSD_ADDR'], ENV['STATSD_IMPLEMENTATION'])
16
- when 'test'
17
- StatsD::Instrument::Backends::NullBackend.new
18
- else
19
- StatsD::Instrument::Backends::LoggerBackend.new(StatsD.logger)
13
+ # Detects the current environment, either by asking Rails, or by inspecting environment variables.
14
+ #
15
+ # - Within a Rails application, <tt>Rails.env</tt> is used.
16
+ # - It will check the following environment variables in order: <tt>RAILS_ENV</tt>, <tt>RACK_ENV</tt>, <tt>ENV</tt>.
17
+ # - If none of these are set, it will return <tt>development</tt>
18
+ #
19
+ # @return [String] The detected environment.
20
+ def environment
21
+ from_env.environment
22
+ end
23
+
24
+ # Instantiates a default backend for the current environment.
25
+ #
26
+ # @return [StatsD::Instrument::Backend]
27
+ # @see #environment
28
+ def default_backend
29
+ case environment
30
+ when 'production', 'staging'
31
+ StatsD::Instrument::Backends::UDPBackend.new(from_env.statsd_addr, from_env.statsd_implementation)
32
+ when 'test'
33
+ StatsD::Instrument::Backends::NullBackend.new
34
+ else
35
+ StatsD::Instrument::Backends::LoggerBackend.new(StatsD.logger)
36
+ end
37
+ end
38
+
39
+ # Sets default values for sample rate and logger.
40
+ #
41
+ # - Default sample rate is set to the value in the STATSD_SAMPLE_RATE environment variable,
42
+ # or 1.0 otherwise. See {StatsD#default_sample_rate}
43
+ # - {StatsD#logger} is set to a logger that send output to stderr.
44
+ #
45
+ # If you are including this library inside a Rails environment, additional initialization will
46
+ # be done as part of the {StatsD::Instrument::Railtie}.
47
+ #
48
+ # @return [void]
49
+ def setup
50
+ StatsD.prefix = from_env.statsd_prefix
51
+ StatsD.default_tags = from_env.statsd_default_tags
52
+ StatsD.default_sample_rate = from_env.statsd_sample_rate
53
+ StatsD.logger = Logger.new($stderr)
20
54
  end
21
55
  end
22
56
 
57
+ attr_reader :env
58
+
59
+ def initialize(env)
60
+ @env = env
61
+ end
62
+
23
63
  # Detects the current environment, either by asking Rails, or by inspecting environment variables.
24
64
  #
25
65
  # - Within a Rails application, <tt>Rails.env</tt> is used.
@@ -28,26 +68,65 @@ module StatsD::Instrument::Environment
28
68
  #
29
69
  # @return [String] The detected environment.
30
70
  def environment
31
- if defined?(Rails) && Rails.respond_to?(:env)
71
+ if env['STATSD_ENV']
72
+ env['STATSD_ENV']
73
+ elsif defined?(Rails) && Rails.respond_to?(:env)
32
74
  Rails.env.to_s
33
75
  else
34
- ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV'] || 'development'
76
+ env['RAILS_ENV'] || env['RACK_ENV'] || env['ENV'] || 'development'
35
77
  end
36
78
  end
37
79
 
38
- # Sets default values for sample rate and logger.
39
- #
40
- # - Default sample rate is set to the value in the STATSD_SAMPLE_RATE environment variable,
41
- # or 1.0 otherwise. See {StatsD#default_sample_rate}
42
- # - {StatsD#logger} is set to a logger that send output to stderr.
43
- #
44
- # If you are including this library inside a Rails environment, additional initialization will
45
- # be done as part of the {StatsD::Instrument::Railtie}.
46
- #
47
- # @return [void]
48
- def setup
49
- StatsD.default_sample_rate = ENV.fetch('STATSD_SAMPLE_RATE', 1.0).to_f
50
- StatsD.logger = Logger.new($stderr)
80
+ def statsd_implementation
81
+ env.fetch('STATSD_IMPLEMENTATION', 'statsd')
82
+ end
83
+
84
+ def statsd_sample_rate
85
+ env.fetch('STATSD_SAMPLE_RATE', 1.0).to_f
86
+ end
87
+
88
+ def statsd_prefix
89
+ env.fetch('STATSD_PREFIX', nil)
90
+ end
91
+
92
+ def statsd_addr
93
+ env.fetch('STATSD_ADDR', 'localhost:8125')
94
+ end
95
+
96
+ def statsd_default_tags
97
+ env.key?('STATSD_DEFAULT_TAGS') ? env.fetch('STATSD_DEFAULT_TAGS').split(',') : nil
98
+ end
99
+
100
+ def default_client
101
+ @default_client ||= StatsD::Instrument::Client.new(
102
+ sink: default_sink_for_environment,
103
+ datagram_builder_class: default_datagram_builder_class_for_implementation,
104
+ default_sample_rate: statsd_sample_rate,
105
+ prefix: statsd_prefix,
106
+ default_tags: statsd_default_tags,
107
+ )
108
+ end
109
+
110
+ def default_datagram_builder_class_for_implementation
111
+ case statsd_implementation
112
+ when 'statsd'
113
+ StatsD::Instrument::StatsDDatagramBuilder
114
+ when 'datadog', 'dogstatsd'
115
+ StatsD::Instrument::DogStatsDDatagramBuilder
116
+ else
117
+ raise NotImplementedError, "No implementation for #{statsd_implementation}"
118
+ end
119
+ end
120
+
121
+ def default_sink_for_environment
122
+ case environment
123
+ when 'production', 'staging'
124
+ StatsD::Instrument::UDPSink.for_addr(statsd_addr)
125
+ when 'test'
126
+ StatsD::Instrument::NullSink.new
127
+ else
128
+ StatsD::Instrument::LogSink.new(StatsD.logger)
129
+ end
51
130
  end
52
131
  end
53
132
 
@@ -1,14 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StatsD::Instrument::Helpers
2
- def capture_statsd_calls(&block)
3
- mock_backend = StatsD::Instrument::Backends::CaptureBackend.new
4
- old_backend, StatsD.backend = StatsD.backend, mock_backend
5
- block.call
6
- mock_backend.collected_metrics
7
- ensure
8
- if old_backend.kind_of?(StatsD::Instrument::Backends::CaptureBackend)
9
- old_backend.collected_metrics.concat(mock_backend.collected_metrics)
4
+ def with_capture_backend(backend, &block)
5
+ if StatsD.backend.is_a?(StatsD::Instrument::Backends::CaptureBackend)
6
+ backend.parent = StatsD.backend
10
7
  end
11
8
 
9
+ old_backend = StatsD.backend
10
+ StatsD.backend = backend
11
+
12
+ block.call
13
+ ensure
12
14
  StatsD.backend = old_backend
13
15
  end
16
+
17
+ def capture_statsd_calls(&block)
18
+ capture_backend = StatsD::Instrument::Backends::CaptureBackend.new
19
+ with_capture_backend(capture_backend, &block)
20
+ capture_backend.collected_metrics
21
+ end
14
22
  end
@@ -0,0 +1,24 @@
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::LogSink
6
+ attr_reader :logger, :severity
7
+
8
+ def initialize(logger, severity: Logger::DEBUG)
9
+ @logger = logger
10
+ @severity = severity
11
+ end
12
+
13
+ def sample?(_sample_rate)
14
+ true
15
+ end
16
+
17
+ def <<(datagram)
18
+ # Some implementations require a newline at the end of datagrams.
19
+ # When logging, we make sure those newlines are removed using chomp.
20
+
21
+ logger.add(severity, "[StatsD] #{datagram.chomp}")
22
+ self
23
+ end
24
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rspec/expectations'
2
4
  require 'rspec/core/version'
3
5
 
@@ -9,7 +11,7 @@ module StatsD::Instrument::Matchers
9
11
  histogram: :h,
10
12
  distribution: :d,
11
13
  set: :s,
12
- key_value: :kv
14
+ key_value: :kv,
13
15
  }
14
16
 
15
17
  class Matcher
@@ -23,13 +25,10 @@ module StatsD::Instrument::Matchers
23
25
  end
24
26
 
25
27
  def matches?(block)
26
- begin
27
- expect_statsd_call(@metric_type, @metric_name, @options, &block)
28
- rescue RSpec::Expectations::ExpectationNotMetError => e
29
- @message = e.message
30
-
31
- false
32
- end
28
+ expect_statsd_call(@metric_type, @metric_name, @options, &block)
29
+ rescue RSpec::Expectations::ExpectationNotMetError => e
30
+ @message = e.message
31
+ false
33
32
  end
34
33
 
35
34
  def failure_message
@@ -50,8 +49,12 @@ module StatsD::Instrument::Matchers
50
49
  metrics = capture_statsd_calls(&block)
51
50
  metrics = metrics.select { |m| m.type == metric_type && m.name == metric_name }
52
51
 
53
- raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made." if metrics.empty?
54
- raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric #{metric_name} was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}" if options[:times] && options[:times] != metrics.length
52
+ if metrics.empty?
53
+ raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made."
54
+ elsif options[:times] && options[:times] != metrics.length
55
+ raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric #{metric_name} " \
56
+ "was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}"
57
+ end
55
58
 
56
59
  [:sample_rate, :value, :tags].each do |expectation|
57
60
  next unless options[expectation]
@@ -63,7 +66,7 @@ module StatsD::Instrument::Matchers
63
66
 
64
67
  found = options[:times] ? num_matches == options[:times] : num_matches > 0
65
68
 
66
- if !found
69
+ unless found
67
70
  message = metric_information(metric_name, options, metrics, expectation)
68
71
  raise RSpec::Expectations::ExpectationNotMetError, message
69
72
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # The Metric class represents a metric sample to be send by a backend.
2
4
  #
3
5
  # @!attribute type
4
6
  # @return [Symbol] The metric type. Must be one of {StatsD::Instrument::Metric::TYPES}
5
7
  # @!attribute name
6
8
  # @return [String] The name of the metric. {StatsD#prefix} will automatically be applied
7
- # to the metric in the constructor, unless the <tt>:no_prefix</tt> option is set or is
9
+ # to the metric in the constructor, unless the <tt>:no_prefix</tt> option is set or is
8
10
  # overridden by the <tt>:prefix</tt> option. Note that <tt>:no_prefix</tt> has greater
9
11
  # precedence than <tt>:prefix</tt>.
10
12
  # @!attribute value
@@ -29,39 +31,24 @@
29
31
  # @see StatsD::Instrument::Backend A StatsD::Instrument::Backend is used to collect metrics.
30
32
  #
31
33
  class StatsD::Instrument::Metric
32
-
33
- attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata
34
-
35
- # Initializes a new metric instance.
36
- # Normally, you don't want to call this method directly, but use one of the metric collection
37
- # methods on the {StatsD} module.
38
- #
39
- # @option options [Symbol] :type The type of the metric.
40
- # @option options [String] :name The name of the metric without prefix.
41
- # @option options [String] :prefix Override the default StatsD prefix.
42
- # @option options [Boolean] :no_prefix Set to <tt>true</tt> if you don't want to apply a prefix.
43
- # @option options [Numeric, String, nil] :value The value to collect for the metric. If set to
44
- # <tt>nil>/tt>, {#default_value} will be used.
45
- # @option options [Numeric, nil] :sample_rate The sample rate to use. If not set, it will use
46
- # {StatsD#default_sample_rate}.
47
- # @option options [Array<String>, Hash<String, String>, nil] :tags The tags to apply to this metric.
48
- # See {.normalize_tags} for more information.
49
- def initialize(options = {})
50
- @type = options[:type] or raise ArgumentError, "Metric :type is required."
51
- @name = options[:name] or raise ArgumentError, "Metric :name is required."
52
- @name = normalize_name(@name)
53
- unless options[:no_prefix]
54
- @name = if options[:prefix]
55
- "#{options[:prefix]}.#{@name}"
56
- else
57
- StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name
34
+ unless Regexp.method_defined?(:match?) # for ruby 2.3
35
+ module RubyBackports
36
+ refine Regexp do
37
+ def match?(str)
38
+ (self =~ str) != nil
39
+ end
58
40
  end
59
41
  end
60
42
 
61
- @value = options[:value] || default_value
62
- @sample_rate = options[:sample_rate] || StatsD.default_sample_rate
63
- @tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
64
- @metadata = options.reject { |k, _| [:type, :name, :value, :sample_rate, :tags].include?(k) }
43
+ using RubyBackports
44
+ end
45
+
46
+ def self.new(
47
+ type:, name:, value: default_value(type), sample_rate: StatsD.default_sample_rate, tags: nil, metadata: nil
48
+ )
49
+ # pass keyword arguments as positional arguments for performance reasons,
50
+ # since MRI's C implementation of new turns keyword arguments into a hash
51
+ super(type, name, value, sample_rate, tags, metadata)
65
52
  end
66
53
 
67
54
  # The default value for this metric, which will be used if it is not set.
@@ -69,40 +56,73 @@ class StatsD::Instrument::Metric
69
56
  # A default value is only defined for counter metrics (<tt>1</tt>). For all other
70
57
  # metric types, this emthod will raise an <tt>ArgumentError</tt>.
71
58
  #
59
+ #
60
+ # A default value is only defined for counter metrics (<tt>1</tt>). For all other
61
+ # metric types, this emthod will raise an <tt>ArgumentError</tt>.
62
+ #
72
63
  # @return [Numeric, String] The default value for this metric.
73
64
  # @raise ArgumentError if the metric type doesn't have a default value
74
- def default_value
65
+ def self.default_value(type)
75
66
  case type
76
- when :c; 1
77
- else raise ArgumentError, "A value is required for metric type #{type.inspect}."
67
+ when :c then 1
68
+ else raise ArgumentError, "A value is required for metric type #{type.inspect}."
78
69
  end
79
70
  end
80
71
 
72
+ attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata
73
+
74
+ # Initializes a new metric instance.
75
+ # Normally, you don't want to call this method directly, but use one of the metric collection
76
+ # methods on the {StatsD} module.
77
+ #
78
+ # @param type [Symbol] The type of the metric.
79
+ # @option name [String] :name The name of the metric without prefix.
80
+ # @option value [Numeric, String, nil] The value to collect for the metric.
81
+ # @option sample_rate [Numeric, nil] The sample rate to use. If not set, it will use
82
+ # {StatsD#default_sample_rate}.
83
+ # @option tags [Array<String>, Hash<String, String>, nil] :tags The tags to apply to this metric.
84
+ # See {.normalize_tags} for more information.
85
+ def initialize(type, name, value, sample_rate, tags, metadata) # rubocop:disable Metrics/ParameterLists
86
+ raise ArgumentError, "Metric :type is required." unless type
87
+ raise ArgumentError, "Metric :name is required." unless name
88
+ raise ArgumentError, "Metric :value is required." unless value
89
+
90
+ @type = type
91
+ @name = normalize_name(name)
92
+ @value = value
93
+ @sample_rate = sample_rate
94
+ @tags = StatsD::Instrument::Metric.normalize_tags(tags)
95
+ if StatsD.default_tags
96
+ @tags = Array(@tags) + StatsD.default_tags
97
+ end
98
+ @metadata = metadata
99
+ end
100
+
81
101
  # @private
82
102
  # @return [String]
83
103
  def to_s
84
- str = "#{TYPES[type]} #{name}:#{value}"
104
+ str = +"#{TYPES[type]} #{name}:#{value}"
85
105
  str << " @#{sample_rate}" if sample_rate != 1.0
86
- str << " " << tags.map { |t| "##{t}"}.join(' ') if tags
106
+ tags&.each { |tag| str << " ##{tag}" }
87
107
  str
88
108
  end
89
109
 
90
110
  # @private
91
111
  # @return [String]
92
112
  def inspect
93
- "#<StatsD::Instrument::Metric #{self.to_s}>"
113
+ "#<StatsD::Instrument::Metric #{self}>"
94
114
  end
95
115
 
96
116
  # The metric types that are supported by this library. Note that every StatsD server
97
117
  # implementation only supports a subset of them.
98
118
  TYPES = {
99
- c: 'increment',
119
+ c: 'increment',
100
120
  ms: 'measure',
101
- g: 'gauge',
102
- h: 'histogram',
103
- d: 'distribution',
121
+ g: 'gauge',
122
+ h: 'histogram',
123
+ d: 'distribution',
104
124
  kv: 'key/value',
105
- s: 'set',
125
+ s: 'set',
106
126
  }
107
127
 
108
128
  # Strip metric names of special characters used by StatsD line protocol, replace with underscore
@@ -110,7 +130,10 @@ class StatsD::Instrument::Metric
110
130
  # @param name [String]
111
131
  # @return [String]
112
132
  def normalize_name(name)
113
- name.tr(':|@'.freeze, '_')
133
+ # fast path when no normalization is needed to avoid copying the string
134
+ return name unless /[:|@]/.match?(name)
135
+
136
+ name.tr(':|@', '_')
114
137
  end
115
138
 
116
139
  # Utility function to convert tags to the canonical form.
@@ -122,7 +145,11 @@ class StatsD::Instrument::Metric
122
145
  # @return [Array<String>, nil] the list of tags in canonical form.
123
146
  def self.normalize_tags(tags)
124
147
  return unless tags
125
- tags = tags.map { |k, v| k.to_s + ":".freeze + v.to_s } if tags.is_a?(Hash)
126
- tags.map { |tag| tag.tr('|,'.freeze, ''.freeze) }
148
+ tags = tags.map { |k, v| k.to_s + ":" + v.to_s } if tags.is_a?(Hash)
149
+
150
+ # fast path when no string replacement is needed
151
+ return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
152
+
153
+ tags.map { |tag| tag.tr('|,', '') }
127
154
  end
128
155
  end
@@ -1,15 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # @private
2
4
  class StatsD::Instrument::MetricExpectation
3
-
4
5
  attr_accessor :times, :type, :name, :value, :sample_rate, :tags
5
6
  attr_reader :ignore_tags
6
7
 
7
8
  def initialize(options = {})
8
- @type = options[:type] or raise ArgumentError, "Metric :type is required."
9
- @name = options[:name] or raise ArgumentError, "Metric :name is required."
9
+ if options[:type]
10
+ @type = options[:type]
11
+ else
12
+ raise ArgumentError, "Metric :type is required."
13
+ end
14
+
15
+ if options[:name]
16
+ @name = options[:name]
17
+ else
18
+ raise ArgumentError, "Metric :name is required."
19
+ end
20
+
21
+ if options[:times]
22
+ @times = options[:times]
23
+ else
24
+ raise ArgumentError, "Metric :times is required."
25
+ end
26
+
10
27
  @name = StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name unless options[:no_prefix]
11
28
  @tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
12
- @times = options[:times] or raise ArgumentError, "Metric :times is required."
13
29
  @sample_rate = options[:sample_rate]
14
30
  @value = options[:value]
15
31
  @ignore_tags = StatsD::Instrument::Metric.normalize_tags(options[:ignore_tags])
@@ -29,7 +45,7 @@ class StatsD::Instrument::MetricExpectation
29
45
  actual_tags -= ignored_tags
30
46
 
31
47
  if ignore_tags.is_a?(Array)
32
- actual_tags.delete_if{ |key| ignore_tags.include?(key.split(":").first) }
48
+ actual_tags.delete_if { |key| ignore_tags.include?(key.split(":").first) }
33
49
  end
34
50
  end
35
51
 
@@ -39,30 +55,28 @@ class StatsD::Instrument::MetricExpectation
39
55
  end
40
56
 
41
57
  def default_value
42
- case type
43
- when :c; 1
44
- end
58
+ 1 if type == :c
45
59
  end
46
60
 
47
61
  TYPES = {
48
- c: 'increment',
49
- ms: 'measure',
50
- g: 'gauge',
51
- h: 'histogram',
52
- d: 'distribution',
53
- kv: 'key/value',
54
- s: 'set',
62
+ c: 'increment',
63
+ ms: 'measure',
64
+ g: 'gauge',
65
+ h: 'histogram',
66
+ d: 'distribution',
67
+ kv: 'key/value',
68
+ s: 'set',
55
69
  }
56
70
 
57
71
  def to_s
58
- str = "#{TYPES[type]} #{name}:#{value}"
72
+ str = +"#{TYPES[type]} #{name}:#{value}"
59
73
  str << " @#{sample_rate}" if sample_rate != 1.0
60
- str << " " << tags.map { |t| "##{t}"}.join(' ') if tags
74
+ str << " " << tags.map { |t| "##{t}" }.join(' ') if tags
61
75
  str << " times:#{times}" if times > 1
62
76
  str
63
77
  end
64
78
 
65
79
  def inspect
66
- "#<StatsD::Instrument::MetricExpectation #{self.to_s}>"
80
+ "#<StatsD::Instrument::MetricExpectation #{self}>"
67
81
  end
68
82
  end
@@ -0,0 +1,13 @@
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::NullSink
6
+ def sample?(_sample_rate)
7
+ false
8
+ end
9
+
10
+ def <<(_datagram)
11
+ self # noop
12
+ end
13
+ end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This Railtie runs some initializers that will set the logger to <tt>Rails#logger</tt>,
2
4
  # and will initialize the {StatsD#backend} based on the Rails environment.
3
5
  #
4
6
  # @see StatsD::Instrument::Environment
5
7
  class StatsD::Instrument::Railtie < Rails::Railtie
6
-
7
8
  initializer 'statsd-instrument.use_rails_logger' do
8
9
  ::StatsD.logger = Rails.logger
9
10
  end
@@ -0,0 +1,39 @@
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 `as_dist: true` keyword argument on `StatsD.measure`
9
+ # and `statsd_measure`. This argument is deprecated. Instead, you can use `StatsD.distribution`
10
+ # (or `statsd_distribution`) directly.
11
+ #
12
+ # To run this cop on your codebase:
13
+ #
14
+ # rubocop --require `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb \
15
+ # --only StatsD/MeasureAsDistArgument
16
+ #
17
+ # This cop will not autocorrect offenses.
18
+ class MeasureAsDistArgument < Cop
19
+ include RuboCop::Cop::StatsD
20
+
21
+ MSG = <<~MSG
22
+ Do not use StatsD.measure(..., as_dist: true). This is deprecated.
23
+
24
+ Use StatsD.distribution (or statsd_distribution) instead.
25
+ MSG
26
+
27
+ def on_send(node)
28
+ if metric_method?(node) && node.method_name == :measure
29
+ add_offense(node) if has_keyword_argument?(node, :as_dist)
30
+ end
31
+
32
+ if metaprogramming_method?(node) && node.method_name == :statsd_measure
33
+ add_offense(node) if has_keyword_argument?(node, :as_dist)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
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 metaprogramming macros for positional
9
+ # argument usage, which is deprecated. These macros include `statd_count_if`,
10
+ # `statsd_measure`, etc.
11
+ #
12
+ # Use the following Rubocop invocation to check your project's codebase:
13
+ #
14
+ # rubocop --only StatsD/MetaprogrammingPositionalArguments
15
+ # -r `bundle show statsd-instrument`/lib/statsd/instrument/rubocop.rb
16
+ #
17
+ #
18
+ # This cop will not autocorrect the offenses it finds, but generally the fixes are easy to fix
19
+ class MetaprogrammingPositionalArguments < Cop
20
+ include RuboCop::Cop::StatsD
21
+
22
+ MSG = 'Use keyword arguments for StatsD metaprogramming macros'
23
+
24
+ def on_send(node)
25
+ if metaprogramming_method?(node)
26
+ arguments = node.arguments.dup
27
+ arguments.shift # method
28
+ arguments.shift # metric
29
+ arguments.pop if arguments.last&.type == :block_pass
30
+ case arguments.length
31
+ when 0
32
+ when 1
33
+ add_offense(node) if arguments.first.type != :hash
34
+ else
35
+ add_offense(node)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end