statsd-instrument 3.0.1 → 3.1.2

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 +2 -1
  5. data/CHANGELOG.md +18 -0
  6. data/Gemfile +8 -10
  7. data/README.md +7 -4
  8. data/Rakefile +6 -6
  9. data/benchmark/send-metrics-to-dev-null-log +14 -14
  10. data/benchmark/send-metrics-to-local-udp-receiver +18 -18
  11. data/lib/statsd/instrument/assertions.rb +7 -7
  12. data/lib/statsd/instrument/batched_udp_sink.rb +159 -0
  13. data/lib/statsd/instrument/client.rb +3 -3
  14. data/lib/statsd/instrument/datagram.rb +1 -1
  15. data/lib/statsd/instrument/datagram_builder.rb +10 -22
  16. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +2 -2
  17. data/lib/statsd/instrument/environment.rb +19 -11
  18. data/lib/statsd/instrument/expectation.rb +6 -18
  19. data/lib/statsd/instrument/matchers.rb +8 -4
  20. data/lib/statsd/instrument/railtie.rb +1 -1
  21. data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +1 -1
  22. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +2 -2
  23. data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +1 -1
  24. data/lib/statsd/instrument/rubocop/metric_return_value.rb +3 -3
  25. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +1 -1
  26. data/lib/statsd/instrument/rubocop/positional_arguments.rb +4 -4
  27. data/lib/statsd/instrument/rubocop/singleton_configuration.rb +1 -1
  28. data/lib/statsd/instrument/rubocop/splat_arguments.rb +2 -2
  29. data/lib/statsd/instrument/rubocop.rb +13 -34
  30. data/lib/statsd/instrument/strict.rb +1 -1
  31. data/lib/statsd/instrument/udp_sink.rb +11 -13
  32. data/lib/statsd/instrument/version.rb +1 -1
  33. data/lib/statsd/instrument.rb +56 -59
  34. data/lib/statsd-instrument.rb +1 -1
  35. data/statsd-instrument.gemspec +2 -0
  36. data/test/assertions_test.rb +200 -155
  37. data/test/benchmark/clock_gettime.rb +1 -1
  38. data/test/benchmark/metrics.rb +8 -8
  39. data/test/benchmark/tags.rb +4 -4
  40. data/test/capture_sink_test.rb +11 -11
  41. data/test/client_test.rb +64 -64
  42. data/test/datagram_builder_test.rb +41 -41
  43. data/test/datagram_test.rb +5 -5
  44. data/test/dogstatsd_datagram_builder_test.rb +22 -22
  45. data/test/environment_test.rb +26 -17
  46. data/test/helpers/rubocop_helper.rb +3 -3
  47. data/test/helpers_test.rb +12 -12
  48. data/test/integration_test.rb +6 -6
  49. data/test/log_sink_test.rb +2 -2
  50. data/test/matchers_test.rb +46 -46
  51. data/test/null_sink_test.rb +2 -2
  52. data/test/rubocop/measure_as_dist_argument_test.rb +2 -2
  53. data/test/rubocop/metaprogramming_positional_arguments_test.rb +2 -2
  54. data/test/rubocop/metric_prefix_argument_test.rb +2 -2
  55. data/test/rubocop/metric_return_value_test.rb +3 -3
  56. data/test/rubocop/metric_value_keyword_argument_test.rb +3 -3
  57. data/test/rubocop/positional_arguments_test.rb +2 -2
  58. data/test/rubocop/singleton_configuration_test.rb +8 -8
  59. data/test/rubocop/splat_arguments_test.rb +2 -2
  60. data/test/statsd_datagram_builder_test.rb +6 -6
  61. data/test/statsd_instrumentation_test.rb +104 -104
  62. data/test/statsd_test.rb +35 -35
  63. data/test/test_helper.rb +13 -6
  64. data/test/udp_sink_test.rb +142 -44
  65. metadata +21 -7
  66. data/test/benchmark/default_tags.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a26da2deac1a048fae156e2e8804ec5a423fde1e383ed1b3c446d34b0c553ec
4
- data.tar.gz: edae81c0c408a10c4729d019930596e2a586abc17863b57642e3f2a0ff1b3084
3
+ metadata.gz: 85b73b161dac9bc8839c3b820e9ea038d05078519fca692e971a48be7077e799
4
+ data.tar.gz: e2d5bfa763a5d53537494c8a5006327a93967d4d2b63f74a685094c551815c81
5
5
  SHA512:
6
- metadata.gz: 87037561be37e4fbf2cdee8e32564b35ae66b9d4c5a308e4784764d879171644ce2e0e5470fca8ac949b3e4ac3bff633e4bf95e83c37449dbbbe365543305bba
7
- data.tar.gz: 9679d5832ffb8406ab800580bbab62ac3ba3d2b936ac9e841a755a90a9f09883ab62777f084a1ad6c979dbb4372f283cfed61e7e200dd32ee164da2673b82669
6
+ metadata.gz: e6db90f921635692d1d19145e3b346029886022c679649fac3e6f5641411ddc557fcd2028baff7be4e0b47a23e53f97e4afe9af4ecb7e537dc0850b32352fbec
7
+ data.tar.gz: 6c8f12361a70596d49dc84b3feb6637bc0ff7e9d62f1e6e7876ec02111cc94a2b561947ac4bfda045f612f57951f9789e578447e67a0b8bda4beec4f0fc76576
@@ -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.7
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,8 +5,9 @@ require:
5
5
  - ./lib/statsd/instrument/rubocop.rb
6
6
 
7
7
  AllCops:
8
- TargetRubyVersion: 2.4
8
+ TargetRubyVersion: 2.7
9
9
  UseCache: true
10
+ SuggestExtensions: false
10
11
  CacheRootDirectory: tmp/rubocop
11
12
  Exclude:
12
13
  - statsd-instrument.gemspec
data/CHANGELOG.md CHANGED
@@ -8,6 +8,24 @@ section below.
8
8
 
9
9
  _Nothing yet_
10
10
 
11
+ ## Version 3.1.2
12
+
13
+ - Fix bug when passing custom client to expectation.
14
+
15
+ ## Version 3.1.1
16
+
17
+ - Improved flushing of buffered datagrams on process exit when using UDP batching.
18
+
19
+ ## Version 3.1.0
20
+
21
+ - Introduced UDP batching using a dispatcher thread, and made it the
22
+ production default.
23
+ - Dropped support for Ruby 2.4 and 2.5.
24
+
25
+ ## Version 3.0.2
26
+
27
+ - Properly handle no_prefix when using StatsD assertions.
28
+
11
29
  ## Version 3.0.1
12
30
 
13
31
  - Fix metaprograming methods to not print keyword argument warnings on
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'
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
@@ -1,6 +1,6 @@
1
1
  # StatsD client for Ruby apps
2
2
 
3
- This is a ruby client for statsd (http://github.com/etsy/statsd). It provides
3
+ This is a ruby client for statsd (https://github.com/statsd/statsd). It provides
4
4
  a lightweight way to track and measure metrics in your application.
5
5
 
6
6
  We call out to statsd by sending data over a UDP socket. UDP sockets are fast,
@@ -10,8 +10,8 @@ because it means your code doesn't get bogged down trying to log statistics.
10
10
  We send data to statsd several times per request and haven't noticed a
11
11
  performance hit.
12
12
 
13
- For more information about StatsD, see the [README of the Etsy
14
- project](http://github.com/etsy/statsd).
13
+ For more information about StatsD, see the [README of the StatsD
14
+ project](https://github.com/statsd/statsd).
15
15
 
16
16
  ## Configuration
17
17
 
@@ -20,7 +20,7 @@ The following environment variables are supported:
20
20
 
21
21
  - `STATSD_ADDR`: (default `localhost:8125`) The address to send the StatsD UDP
22
22
  datagrams to.
23
- - `STATSD_IMPLEMENTATION`: (default: `statsd`). The StatsD implementation you
23
+ - `STATSD_IMPLEMENTATION`: (default: `datadog`). The StatsD implementation you
24
24
  are using. `statsd`, `statsite` and `datadog` are supported. Some features
25
25
  are only available on certain implementations,
26
26
  - `STATSD_ENV`: The environment StatsD will run in. If this is not set
@@ -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
- master_revision = %x(git rev-parse origin/master).rstrip
10
- branch = if revision == master_revision
11
- 'master'
9
+ base_revision = %x(git rev-parse origin/master).rstrip
10
+ branch = if revision == base_revision
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
- master_revision = %x(git rev-parse origin/master).rstrip
12
- branch = if revision == master_revision
13
- 'master'
11
+ base_revision = %x(git rev-parse origin/master).rstrip
12
+ branch = if revision == base_revision
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
@@ -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.
@@ -74,7 +74,7 @@ module StatsD
74
74
  # @raise [Minitest::Assertion] If an exception occurs, or if the metric did
75
75
  # not occur as specified during the execution the block.
76
76
  def assert_statsd_increment(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
77
- expectation = StatsD::Instrument::Expectation.increment(metric_name, value, **options)
77
+ expectation = StatsD::Instrument::Expectation.increment(metric_name, value, client: client, **options)
78
78
  assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
79
79
  end
80
80
 
@@ -86,7 +86,7 @@ module StatsD
86
86
  # @return [void]
87
87
  # @raise (see #assert_statsd_increment)
88
88
  def assert_statsd_measure(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
89
- expectation = StatsD::Instrument::Expectation.measure(metric_name, value, **options)
89
+ expectation = StatsD::Instrument::Expectation.measure(metric_name, value, client: client, **options)
90
90
  assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
91
91
  end
92
92
 
@@ -98,7 +98,7 @@ module StatsD
98
98
  # @return [void]
99
99
  # @raise (see #assert_statsd_increment)
100
100
  def assert_statsd_gauge(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
101
- expectation = StatsD::Instrument::Expectation.gauge(metric_name, value, **options)
101
+ expectation = StatsD::Instrument::Expectation.gauge(metric_name, value, client: client, **options)
102
102
  assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
103
103
  end
104
104
 
@@ -110,7 +110,7 @@ module StatsD
110
110
  # @return [void]
111
111
  # @raise (see #assert_statsd_increment)
112
112
  def assert_statsd_histogram(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
113
- expectation = StatsD::Instrument::Expectation.histogram(metric_name, value, **options)
113
+ expectation = StatsD::Instrument::Expectation.histogram(metric_name, value, client: client, **options)
114
114
  assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
115
115
  end
116
116
 
@@ -122,7 +122,7 @@ module StatsD
122
122
  # @return [void]
123
123
  # @raise (see #assert_statsd_increment)
124
124
  def assert_statsd_distribution(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
125
- expectation = StatsD::Instrument::Expectation.distribution(metric_name, value, **options)
125
+ expectation = StatsD::Instrument::Expectation.distribution(metric_name, value, client: client, **options)
126
126
  assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
127
127
  end
128
128
 
@@ -134,7 +134,7 @@ module StatsD
134
134
  # @return [void]
135
135
  # @raise (see #assert_statsd_increment)
136
136
  def assert_statsd_set(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
137
- expectation = StatsD::Instrument::Expectation.set(metric_name, value, **options)
137
+ expectation = StatsD::Instrument::Expectation.set(metric_name, value, client: client, **options)
138
138
  assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
139
139
  end
140
140
 
@@ -0,0 +1,159 @@
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.0 || 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(wait = @flush_interval * 2)
75
+ @interrupted = true
76
+ if @dispatcher_thread&.alive?
77
+ @dispatcher_thread.join(wait)
78
+ else
79
+ flush
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ NEWLINE = "\n".b.freeze
86
+ def flush
87
+ return if @buffer.empty?
88
+
89
+ datagrams = @buffer.shift(@buffer.size)
90
+
91
+ until datagrams.empty?
92
+ packet = String.new(datagrams.pop, encoding: Encoding::BINARY, capacity: MAX_PACKET_SIZE)
93
+
94
+ until datagrams.empty? || packet.bytesize + datagrams.first.bytesize + 1 > MAX_PACKET_SIZE
95
+ packet << NEWLINE << datagrams.shift
96
+ end
97
+
98
+ send_packet(packet)
99
+ end
100
+ end
101
+
102
+ def dispatch
103
+ until @interrupted
104
+ begin
105
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
106
+ flush
107
+ next_sleep_duration = @flush_interval - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
108
+
109
+ sleep(next_sleep_duration) if next_sleep_duration > 0
110
+ rescue => error
111
+ report_error(error)
112
+ end
113
+ end
114
+
115
+ flush
116
+ invalidate_socket
117
+ end
118
+
119
+ def report_error(error)
120
+ StatsD.logger.error do
121
+ "[#{self.class.name}] The dispatcher thread encountered an error #{error.class}: #{error.message}"
122
+ end
123
+ end
124
+
125
+ def send_packet(packet)
126
+ retried = false
127
+ socket.send(packet, 0)
128
+ rescue SocketError, IOError, SystemCallError => error
129
+ StatsD.logger.debug do
130
+ "[#{self.class.name}] Resetting connection because of #{error.class}: #{error.message}"
131
+ end
132
+ invalidate_socket
133
+ if retried
134
+ StatsD.logger.warning do
135
+ "[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
136
+ end
137
+ else
138
+ retried = true
139
+ retry
140
+ end
141
+ end
142
+
143
+ def socket
144
+ @socket ||= begin
145
+ socket = UDPSocket.new
146
+ socket.connect(@host, @port)
147
+ socket
148
+ end
149
+ end
150
+
151
+ def invalidate_socket
152
+ @socket&.close
153
+ ensure
154
+ @socket = nil
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end