statsd-instrument 3.0.2 → 3.1.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +22 -0
  3. data/.github/workflows/{ci.yml → tests.yml} +3 -21
  4. data/.rubocop.yml +1 -1
  5. data/CHANGELOG.md +6 -0
  6. data/Gemfile +8 -10
  7. data/README.md +3 -0
  8. data/Rakefile +6 -6
  9. data/benchmark/send-metrics-to-dev-null-log +12 -12
  10. data/benchmark/send-metrics-to-local-udp-receiver +16 -16
  11. data/lib/statsd-instrument.rb +1 -1
  12. data/lib/statsd/instrument.rb +56 -59
  13. data/lib/statsd/instrument/assertions.rb +1 -1
  14. data/lib/statsd/instrument/batched_udp_sink.rb +154 -0
  15. data/lib/statsd/instrument/client.rb +3 -3
  16. data/lib/statsd/instrument/datagram.rb +1 -1
  17. data/lib/statsd/instrument/datagram_builder.rb +10 -10
  18. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +2 -2
  19. data/lib/statsd/instrument/environment.rb +19 -11
  20. data/lib/statsd/instrument/expectation.rb +3 -3
  21. data/lib/statsd/instrument/matchers.rb +8 -4
  22. data/lib/statsd/instrument/railtie.rb +1 -1
  23. data/lib/statsd/instrument/rubocop.rb +8 -8
  24. data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +1 -1
  25. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +2 -2
  26. data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +1 -1
  27. data/lib/statsd/instrument/rubocop/metric_return_value.rb +2 -2
  28. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +1 -1
  29. data/lib/statsd/instrument/rubocop/positional_arguments.rb +4 -4
  30. data/lib/statsd/instrument/rubocop/singleton_configuration.rb +1 -1
  31. data/lib/statsd/instrument/rubocop/splat_arguments.rb +2 -2
  32. data/lib/statsd/instrument/strict.rb +1 -1
  33. data/lib/statsd/instrument/udp_sink.rb +10 -12
  34. data/lib/statsd/instrument/version.rb +1 -1
  35. data/statsd-instrument.gemspec +2 -0
  36. data/test/assertions_test.rb +167 -169
  37. data/test/benchmark/clock_gettime.rb +1 -1
  38. data/test/benchmark/default_tags.rb +9 -9
  39. data/test/benchmark/metrics.rb +8 -8
  40. data/test/benchmark/tags.rb +4 -4
  41. data/test/capture_sink_test.rb +11 -11
  42. data/test/client_test.rb +64 -64
  43. data/test/datagram_builder_test.rb +40 -40
  44. data/test/datagram_test.rb +5 -5
  45. data/test/dogstatsd_datagram_builder_test.rb +22 -22
  46. data/test/environment_test.rb +26 -17
  47. data/test/helpers/rubocop_helper.rb +2 -2
  48. data/test/helpers_test.rb +12 -12
  49. data/test/integration_test.rb +6 -6
  50. data/test/log_sink_test.rb +2 -2
  51. data/test/matchers_test.rb +46 -46
  52. data/test/null_sink_test.rb +2 -2
  53. data/test/rubocop/measure_as_dist_argument_test.rb +2 -2
  54. data/test/rubocop/metaprogramming_positional_arguments_test.rb +2 -2
  55. data/test/rubocop/metric_prefix_argument_test.rb +2 -2
  56. data/test/rubocop/metric_return_value_test.rb +3 -3
  57. data/test/rubocop/metric_value_keyword_argument_test.rb +2 -2
  58. data/test/rubocop/positional_arguments_test.rb +2 -2
  59. data/test/rubocop/singleton_configuration_test.rb +8 -8
  60. data/test/rubocop/splat_arguments_test.rb +2 -2
  61. data/test/statsd_datagram_builder_test.rb +6 -6
  62. data/test/statsd_instrumentation_test.rb +104 -104
  63. data/test/statsd_test.rb +35 -35
  64. data/test/test_helper.rb +13 -6
  65. data/test/udp_sink_test.rb +117 -45
  66. metadata +20 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 032db29a498269753044a778380369fc44d3f87b3143e051df69f1d8a480595a
4
- data.tar.gz: 10d0ccc0cb4039d8846862e82283de004a7fb2b770648fb165281dd5e2d5f68c
3
+ metadata.gz: 3a481f6cd0897b93e3e992fc5315b496c033a98a8cccd02948146cb638e0a6b2
4
+ data.tar.gz: ab454f2fecc89aa662ac2f16632fe2fc3c4e4b2b102d3c4058642e5706bf64c1
5
5
  SHA512:
6
- metadata.gz: 61b4c7cf6c84144767201dea7491cfa217dbd48b45821e6f1173eac8d45de6c9ddd9239ce50491839425d8cbe672f69c9ebfc9263916ec2b8a027fab43824f41
7
- data.tar.gz: e726adb888e41bdfc8c9049c36b3df9b0f6ee2328a7a9e04f871652483dd57ade4ec04bd11e7010b275d77c733917c10441481c588c58bc9cb573b9364648dac
6
+ metadata.gz: f3fae39281925135491d66805ff8dada34b0ea798d3403fdf0e6d3b9c1e2d3eda4a78295d7eea046ba7a0db55fb5d357d72e853558f12fdf81c71eff3713c816
7
+ data.tar.gz: 852e0838027594b8451d4fb617ca9aa5f614b570c50180bd513fb2986419adf3e9ea4f59ce86e63fbe799ccc3a07720954c87778c9a9dca63cd62fb339906d43
@@ -0,0 +1,22 @@
1
+ name: Lint
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ test:
7
+ name: Rubocop
8
+ runs-on: ubuntu-18.04
9
+
10
+ steps:
11
+ - uses: actions/checkout@v1
12
+
13
+ - name: Setup Ruby
14
+ uses: actions/setup-ruby@v1
15
+ with:
16
+ ruby-version: 2.6
17
+
18
+ - name: Install dependencies
19
+ run: gem install bundler && bundle install --jobs 4 --retry 3
20
+
21
+ - name: Run Rubocop
22
+ run: bin/rubocop
@@ -9,27 +9,12 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- # Windows on macOS builds started failing, so they are disabled for noew
12
+ ruby: ['2.6', '2.7', '3.0']
13
13
 
14
- ruby: [2.4, 2.5, 2.6, 2.7]
14
+ # Windows on macOS builds started failing, so they are disabled for noew
15
15
  # platform: [windows-2019, macOS-10.14, ubuntu-18.04]
16
-
17
16
  # exclude:
18
- # The Windows environment does not support older Ruby versions. We only test against the latest version
19
- # - platform: windows-2019
20
- # ruby: 2.3
21
- # - platform: windows-2019
22
- # ruby: 2.4
23
- # - platform: windows-2019
24
- # ruby: 2.5
25
-
26
- # On macOS, we only test against the Ruby version macOS ships with (2.3)
27
- # - platform: macOS-10.14
28
- # ruby: 2.4
29
- # - platform: macOS-10.14
30
- # ruby: 2.5
31
- # - platform: macOS-10.14
32
- # ruby: 2.6
17
+ # ...
33
18
 
34
19
  steps:
35
20
  - uses: actions/checkout@v1
@@ -44,6 +29,3 @@ jobs:
44
29
 
45
30
  - name: Run test suite
46
31
  run: rake test
47
-
48
- - name: Run Rubocop
49
- run: bin/rubocop
data/.rubocop.yml CHANGED
@@ -5,7 +5,7 @@ require:
5
5
  - ./lib/statsd/instrument/rubocop.rb
6
6
 
7
7
  AllCops:
8
- TargetRubyVersion: 2.4
8
+ TargetRubyVersion: 2.6
9
9
  UseCache: true
10
10
  CacheRootDirectory: tmp/rubocop
11
11
  Exclude:
data/CHANGELOG.md CHANGED
@@ -8,6 +8,12 @@ section below.
8
8
 
9
9
  _Nothing yet_
10
10
 
11
+ ## Version 3.1.0
12
+
13
+ - Introduced UDP batching using a dispatcher thread, and made it the
14
+ production default.
15
+ - Dropped support for Ruby 2.4 and 2.5.
16
+
11
17
  ## Version 3.0.2
12
18
 
13
19
  - Properly handle no_prefix when using StatsD assertions.
data/Gemfile CHANGED
@@ -3,13 +3,11 @@
3
3
  source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
- gem 'rake'
7
- gem 'minitest'
8
- gem 'rspec'
9
- gem 'mocha'
10
- gem 'yard'
11
- gem 'rubocop', '>= 1.0'
12
- gem 'rubocop-shopify', require: false
13
-
14
- # benchmark-ips save! method is not part of a released version yet.
15
- gem 'benchmark-ips', git: 'https://github.com/evanphx/benchmark-ips', branch: 'master'
6
+ gem "rake"
7
+ gem "minitest"
8
+ gem "rspec"
9
+ gem "mocha"
10
+ gem "yard"
11
+ gem "rubocop", ">= 1.0"
12
+ gem "rubocop-shopify", require: false
13
+ gem "benchmark-ips"
data/README.md CHANGED
@@ -42,6 +42,9 @@ The following environment variables are supported:
42
42
  overridden in a metric method call.
43
43
  - `STATSD_DEFAULT_TAGS`: A comma-separated list of tags to apply to all metrics.
44
44
  (Note: tags are not supported by all implementations.)
45
+ - `STATSD_FLUSH_INTERVAL`: (default: `1.0`) The interval in seconds at which
46
+ events are sent in batch. Only applicable to the UDP configuration. If set
47
+ to `0.0`, metrics are sent immediately.
45
48
 
46
49
  ## StatsD keys
47
50
 
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rake/testtask'
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
5
 
6
- Rake::TestTask.new('test') do |t|
7
- t.ruby_opts << '-r rubygems'
8
- t.libs << 'lib' << 'test'
9
- t.test_files = FileList['test/**/*_test.rb']
6
+ Rake::TestTask.new("test") do |t|
7
+ t.ruby_opts << "-r rubygems"
8
+ t.libs << "lib" << "test"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
10
  end
11
11
 
12
12
  task(default: :test)
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/setup'
5
- require 'tmpdir'
6
- require 'benchmark/ips'
4
+ require "bundler/setup"
5
+ require "tmpdir"
6
+ require "benchmark/ips"
7
7
 
8
8
  revision = %x(git rev-parse HEAD).rstrip
9
9
  master_revision = %x(git rev-parse origin/master).rstrip
10
10
  branch = if revision == master_revision
11
- 'master'
11
+ "master"
12
12
  else
13
13
  %x(git rev-parse --abbrev-ref HEAD).rstrip
14
14
  end
@@ -16,18 +16,18 @@ end
16
16
  intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}"
17
17
  FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
18
18
 
19
- ENV['ENV'] = "development"
20
- require 'statsd-instrument'
19
+ ENV["ENV"] = "development"
20
+ require "statsd-instrument"
21
21
  StatsD.logger = Logger.new(File::NULL)
22
22
 
23
23
  report = Benchmark.ips do |bench|
24
24
  bench.report("StatsD metrics to /dev/null log (branch: #{branch}, sha: #{revision[0, 7]})") do
25
- StatsD.increment('StatsD.increment', 10, sample_rate: 15)
26
- StatsD.measure('StatsD.measure') { 1 + 1 }
27
- StatsD.gauge('StatsD.gauge', 12.0, tags: ["foo:bar", "quc"])
28
- StatsD.set('StatsD.set', 'value', tags: { foo: 'bar', baz: 'quc' })
25
+ StatsD.increment("StatsD.increment", 10, sample_rate: 15)
26
+ StatsD.measure("StatsD.measure") { 1 + 1 }
27
+ StatsD.gauge("StatsD.gauge", 12.0, tags: ["foo:bar", "quc"])
28
+ StatsD.set("StatsD.set", "value", tags: { foo: "bar", baz: "quc" })
29
29
  if StatsD.singleton_client.datagram_builder_class == StatsD::Instrument::DogStatsDDatagramBuilder
30
- StatsD.event('StasD.event', "12345")
30
+ StatsD.event("StasD.event", "12345")
31
31
  StatsD.service_check("StatsD.service_check", "ok")
32
32
  end
33
33
  end
@@ -41,7 +41,7 @@ if report.entries.length == 1
41
41
  puts
42
42
  puts "To compare the performance of this revision against another revision (e.g. master),"
43
43
  puts "check out a different branch and run this benchmark script again."
44
- elsif ENV['KEEP_RESULTS']
44
+ elsif ENV["KEEP_RESULTS"]
45
45
  puts
46
46
  puts "The intermediate results have been stored in #{intermediate_results_filename}"
47
47
  else
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/setup'
5
- require 'benchmark/ips'
6
- require 'tmpdir'
7
- require 'socket'
8
- require 'statsd-instrument'
4
+ require "bundler/setup"
5
+ require "benchmark/ips"
6
+ require "tmpdir"
7
+ require "socket"
8
+ require "statsd-instrument"
9
9
 
10
10
  revision = %x(git rev-parse HEAD).rstrip
11
11
  master_revision = %x(git rev-parse origin/master).rstrip
12
12
  branch = if revision == master_revision
13
- 'master'
13
+ "master"
14
14
  else
15
15
  %x(git rev-parse --abbrev-ref HEAD).rstrip
16
16
  end
@@ -20,22 +20,22 @@ FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
20
20
 
21
21
  # Set up an UDP listener to which we can send StatsD packets
22
22
  receiver = UDPSocket.new
23
- receiver.bind('localhost', 0)
23
+ receiver.bind("localhost", 0)
24
24
 
25
25
  StatsD.singleton_client = StatsD::Instrument::Environment.new(
26
- 'STATSD_ADDR' => "#{receiver.addr[2]}:#{receiver.addr[1]}",
27
- 'STATSD_IMPLEMENTATION' => 'dogstatsd',
28
- 'STATSD_ENV' => 'production',
26
+ "STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
27
+ "STATSD_IMPLEMENTATION" => "dogstatsd",
28
+ "STATSD_ENV" => "production",
29
29
  ).client
30
30
 
31
31
  report = Benchmark.ips do |bench|
32
32
  bench.report("StatsD metrics to local UDP receiver (branch: #{branch}, sha: #{revision[0, 7]})") do
33
- StatsD.increment('StatsD.increment', 10)
34
- StatsD.measure('StatsD.measure') { 1 + 1 }
35
- StatsD.gauge('StatsD.gauge', 12.0, tags: ["foo:bar", "quc"])
36
- StatsD.set('StatsD.set', 'value', tags: { foo: 'bar', baz: 'quc' })
33
+ StatsD.increment("StatsD.increment", 10)
34
+ StatsD.measure("StatsD.measure") { 1 + 1 }
35
+ StatsD.gauge("StatsD.gauge", 12.0, tags: ["foo:bar", "quc"])
36
+ StatsD.set("StatsD.set", "value", tags: { foo: "bar", baz: "quc" })
37
37
  if StatsD.singleton_client.datagram_builder_class == StatsD::Instrument::DogStatsDDatagramBuilder
38
- StatsD.event('StasD.event', "12345")
38
+ StatsD.event("StasD.event", "12345")
39
39
  StatsD.service_check("StatsD.service_check", "ok")
40
40
  end
41
41
  end
@@ -51,7 +51,7 @@ if report.entries.length == 1
51
51
  puts
52
52
  puts "To compare the performance of this revision against another revision (e.g. master),"
53
53
  puts "check out a different branch and run this benchmark script again."
54
- elsif ENV['KEEP_RESULTS']
54
+ elsif ENV["KEEP_RESULTS"]
55
55
  puts
56
56
  puts "The intermediate results have been stored in #{intermediate_results_filename}"
57
57
  else
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'statsd/instrument'
3
+ require "statsd/instrument"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'socket'
4
- require 'logger'
5
- require 'forwardable'
3
+ require "socket"
4
+ require "logger"
5
+ require "forwardable"
6
6
 
7
7
  # The `StatsD` module contains low-level metrics for collecting metrics and
8
8
  # sending them to the backend.
@@ -31,7 +31,7 @@ module StatsD
31
31
  # @private
32
32
  # @return [String]
33
33
  def self.generate_metric_name(name, callee, *args)
34
- name.respond_to?(:call) ? name.call(callee, args).gsub('::', '.') : name.gsub('::', '.')
34
+ name.respond_to?(:call) ? name.call(callee, args).gsub("::", ".") : name.gsub("::", ".")
35
35
  end
36
36
 
37
37
  # Even though this method is considered private, and is no longer used internally,
@@ -113,26 +113,24 @@ module StatsD
113
113
  def statsd_count_success(method, name, sample_rate: nil, tags: nil, no_prefix: false, client: nil)
114
114
  add_to_method(method, name, :count_success) do
115
115
  define_method(method) do |*args, &block|
116
- begin
117
- truthiness = result = super(*args, &block)
118
- rescue
119
- truthiness = false
120
- raise
121
- else
122
- if block_given?
123
- begin
124
- truthiness = yield(result)
125
- rescue
126
- truthiness = false
127
- end
116
+ truthiness = result = super(*args, &block)
117
+ rescue
118
+ truthiness = false
119
+ raise
120
+ else
121
+ if block_given?
122
+ begin
123
+ truthiness = yield(result)
124
+ rescue
125
+ truthiness = false
128
126
  end
129
- result
130
- ensure
131
- client ||= StatsD.singleton_client
132
- suffix = truthiness == false ? 'failure' : 'success'
133
- key = StatsD::Instrument.generate_metric_name(name, self, *args)
134
- client.increment("#{key}.#{suffix}", sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
135
127
  end
128
+ result
129
+ ensure
130
+ client ||= StatsD.singleton_client
131
+ suffix = truthiness == false ? "failure" : "success"
132
+ key = StatsD::Instrument.generate_metric_name(name, self, *args)
133
+ client.increment("#{key}.#{suffix}", sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
136
134
  end
137
135
  end
138
136
  end
@@ -152,27 +150,25 @@ module StatsD
152
150
  def statsd_count_if(method, name, sample_rate: nil, tags: nil, no_prefix: false, client: nil)
153
151
  add_to_method(method, name, :count_if) do
154
152
  define_method(method) do |*args, &block|
155
- begin
156
- truthiness = result = super(*args, &block)
157
- rescue
158
- truthiness = false
159
- raise
160
- else
161
- if block_given?
162
- begin
163
- truthiness = yield(result)
164
- rescue
165
- truthiness = false
166
- end
167
- end
168
- result
169
- ensure
170
- if truthiness
171
- client ||= StatsD.singleton_client
172
- key = StatsD::Instrument.generate_metric_name(name, self, *args)
173
- client.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
153
+ truthiness = result = super(*args, &block)
154
+ rescue
155
+ truthiness = false
156
+ raise
157
+ else
158
+ if block_given?
159
+ begin
160
+ truthiness = yield(result)
161
+ rescue
162
+ truthiness = false
174
163
  end
175
164
  end
165
+ result
166
+ ensure
167
+ if truthiness
168
+ client ||= StatsD.singleton_client
169
+ key = StatsD::Instrument.generate_metric_name(name, self, *args)
170
+ client.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
171
+ end
176
172
  end
177
173
  end
178
174
  end
@@ -347,21 +343,22 @@ module StatsD
347
343
  end
348
344
  end
349
345
 
350
- require 'statsd/instrument/version'
351
- require 'statsd/instrument/client'
352
- require 'statsd/instrument/datagram'
353
- require 'statsd/instrument/dogstatsd_datagram'
354
- require 'statsd/instrument/datagram_builder'
355
- require 'statsd/instrument/statsd_datagram_builder'
356
- require 'statsd/instrument/dogstatsd_datagram_builder'
357
- require 'statsd/instrument/null_sink'
358
- require 'statsd/instrument/udp_sink'
359
- require 'statsd/instrument/capture_sink'
360
- require 'statsd/instrument/log_sink'
361
- require 'statsd/instrument/environment'
362
- require 'statsd/instrument/helpers'
363
- require 'statsd/instrument/assertions'
364
- require 'statsd/instrument/expectation'
365
- require 'statsd/instrument/matchers' if defined?(::RSpec)
366
- require 'statsd/instrument/railtie' if defined?(::Rails::Railtie)
367
- require 'statsd/instrument/strict' if ENV['STATSD_STRICT_MODE']
346
+ require "statsd/instrument/version"
347
+ require "statsd/instrument/client"
348
+ require "statsd/instrument/datagram"
349
+ require "statsd/instrument/dogstatsd_datagram"
350
+ require "statsd/instrument/datagram_builder"
351
+ require "statsd/instrument/statsd_datagram_builder"
352
+ require "statsd/instrument/dogstatsd_datagram_builder"
353
+ require "statsd/instrument/null_sink"
354
+ require "statsd/instrument/udp_sink"
355
+ require "statsd/instrument/batched_udp_sink"
356
+ require "statsd/instrument/capture_sink"
357
+ require "statsd/instrument/log_sink"
358
+ require "statsd/instrument/environment"
359
+ require "statsd/instrument/helpers"
360
+ require "statsd/instrument/assertions"
361
+ require "statsd/instrument/expectation"
362
+ require "statsd/instrument/matchers" if defined?(::RSpec)
363
+ require "statsd/instrument/railtie" if defined?(::Rails::Railtie)
364
+ require "statsd/instrument/strict" if ENV["STATSD_STRICT_MODE"]
@@ -61,7 +61,7 @@ module StatsD
61
61
  end
62
62
 
63
63
  datagrams.select! { |metric| metric_names.include?(metric.name) } unless metric_names.empty?
64
- assert(datagrams.empty?, "No StatsD calls for metric #{datagrams.map(&:name).join(', ')} expected.")
64
+ assert(datagrams.empty?, "No StatsD calls for metric #{datagrams.map(&:name).join(", ")} expected.")
65
65
  end
66
66
 
67
67
  # Asserts that a given counter metric occurred inside the provided block.
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatsD
4
+ module Instrument
5
+ # @note This class is part of the new Client implementation that is intended
6
+ # to become the new default in the next major release of this library.
7
+ class BatchedUDPSink
8
+ DEFAULT_FLUSH_INTERVAL = 1.0
9
+ MAX_PACKET_SIZE = 508
10
+
11
+ def self.for_addr(addr, flush_interval: DEFAULT_FLUSH_INTERVAL)
12
+ host, port_as_string = addr.split(":", 2)
13
+ new(host, Integer(port_as_string), flush_interval: flush_interval)
14
+ end
15
+
16
+ attr_reader :host, :port
17
+
18
+ class << self
19
+ def finalize(dispatcher)
20
+ proc { dispatcher.shutdown }
21
+ end
22
+ end
23
+
24
+ def initialize(host, port, flush_interval: DEFAULT_FLUSH_INTERVAL)
25
+ @host = host
26
+ @port = port
27
+ @dispatcher = Dispatcher.new(host, port, flush_interval)
28
+ ObjectSpace.define_finalizer(self, self.class.finalize(@dispatcher))
29
+ end
30
+
31
+ def sample?(sample_rate)
32
+ sample_rate == 1 || rand < sample_rate
33
+ end
34
+
35
+ def <<(datagram)
36
+ @dispatcher << datagram
37
+ self
38
+ end
39
+
40
+ class Dispatcher
41
+ BUFFER_CLASS = if !::Object.const_defined?(:RUBY_ENGINE) || RUBY_ENGINE == "ruby"
42
+ ::Array
43
+ else
44
+ begin
45
+ gem("concurrent-ruby")
46
+ rescue Gem::MissingSpecError
47
+ raise Gem::MissingSpecError, "statsd-instrument depends on `concurrent-ruby` on #{RUBY_ENGINE}"
48
+ end
49
+ require "concurrent/array"
50
+ Concurrent::Array
51
+ end
52
+
53
+ def initialize(host, port, flush_interval)
54
+ @host = host
55
+ @port = port
56
+ @interrupted = false
57
+ @flush_interval = flush_interval
58
+ @buffer = BUFFER_CLASS.new
59
+ @dispatcher_thread = Thread.new { dispatch }
60
+ end
61
+
62
+ def <<(datagram)
63
+ unless @dispatcher_thread&.alive?
64
+ # If the dispatcher thread is dead, we assume it is because
65
+ # the process was forked. So to avoid ending datagrams twice
66
+ # we clear the buffer.
67
+ @buffer.clear
68
+ @dispatcher_thread = Thread.new { dispatch }
69
+ end
70
+ @buffer << datagram
71
+ self
72
+ end
73
+
74
+ def shutdown
75
+ @interrupted = true
76
+ end
77
+
78
+ private
79
+
80
+ NEWLINE = "\n".b.freeze
81
+ def flush
82
+ return if @buffer.empty?
83
+
84
+ datagrams = @buffer.shift(@buffer.size)
85
+
86
+ until datagrams.empty?
87
+ packet = String.new(datagrams.pop, encoding: Encoding::BINARY, capacity: MAX_PACKET_SIZE)
88
+
89
+ until datagrams.empty? || packet.bytesize + datagrams.first.bytesize + 1 > MAX_PACKET_SIZE
90
+ packet << NEWLINE << datagrams.shift
91
+ end
92
+
93
+ send_packet(packet)
94
+ end
95
+ end
96
+
97
+ def dispatch
98
+ until @interrupted
99
+ begin
100
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
101
+ flush
102
+ next_sleep_duration = @flush_interval - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
103
+
104
+ sleep(next_sleep_duration) if next_sleep_duration > 0
105
+ rescue => error
106
+ report_error(error)
107
+ end
108
+ end
109
+
110
+ flush
111
+ invalidate_socket
112
+ end
113
+
114
+ def report_error(error)
115
+ StatsD.logger.error do
116
+ "[#{self.class.name}] The dispatcher thread encountered an error #{error.class}: #{error.message}"
117
+ end
118
+ end
119
+
120
+ def send_packet(packet)
121
+ retried = false
122
+ socket.send(packet, 0)
123
+ rescue SocketError, IOError, SystemCallError => error
124
+ StatsD.logger.debug do
125
+ "[#{self.class.name}] Resetting connection because of #{error.class}: #{error.message}"
126
+ end
127
+ invalidate_socket
128
+ if retried
129
+ StatsD.logger.warning do
130
+ "[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
131
+ end
132
+ else
133
+ retried = true
134
+ retry
135
+ end
136
+ end
137
+
138
+ def socket
139
+ @socket ||= begin
140
+ socket = UDPSocket.new
141
+ socket.connect(@host, @port)
142
+ socket
143
+ end
144
+ end
145
+
146
+ def invalidate_socket
147
+ @socket&.close
148
+ ensure
149
+ @socket = nil
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end