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
@@ -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