statsd-instrument 3.7.0 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2e01a49d8bb83318bef84628164ab1dec1047a72c1f717969a609f783d3b8be
4
- data.tar.gz: bf308ae3bb23958705efcc39c03fa95f99b58bb33df47b7588fe4a7b22d742e9
3
+ metadata.gz: 9776ac7b9f3dc12364a1ba286d7b77330c52b0aa8560c29c0ff47c8033c066df
4
+ data.tar.gz: ecfc23f01f345eabc82d1c696c2fb280cc3185f2d30b47e8e5c75326077f10bd
5
5
  SHA512:
6
- metadata.gz: 57f0022a72e600871df9f054ad4a003ed2c361287949bfa4af94789460ffa1a9257ffba1f552ac77b786d5c9dc295efb7ccd82edf632f7151d1dda365b282a61
7
- data.tar.gz: c458ec4b7177cd746f3fd173641db3127ac97a7683f0f55ff00f84ebe5a81b827d53340566b79ea39adb7327ac967298657d23fcc5f51bd9edced6ea05314beb
6
+ metadata.gz: e831388ae5824bd7d1e53b9eefc399dd639210648976adeafd48fabd9d4a37705af2d5814868e75922cc27840cba42d5066054751a8120e4ea8d055efdaea379
7
+ data.tar.gz: 91f63290ef5d9dcc032e0fcdad8e34acdc5448d563e1eedd71b23a68330060ba58af3999ace24b4a82567f2e5c96182996c3f8dda940684a1df93fe9acee02ef
@@ -0,0 +1,14 @@
1
+ ## ✅ What
2
+ <!-- A brief description of the changes in this PR. -->
3
+
4
+ ## 🤔 Why
5
+ <!-- A brief description of the reason for these changes. -->
6
+
7
+ ## 👩🔬 How to validate
8
+ <!-- Step-by-step instructions for how reviewers can verify these changes work as expected. -->
9
+
10
+ ## Checklist
11
+
12
+ - [ ] I documented the changes in the CHANGELOG file.
13
+ <!-- If this is a user-facing change, you must update the CHANGELOG file. OR -->
14
+ <!-- - [ ] This change is not user-facing and does not require a CHANGELOG update. -->
@@ -8,12 +8,11 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
 
10
10
  steps:
11
- - uses: actions/checkout@v3
11
+ - uses: actions/checkout@v4
12
12
 
13
13
  - name: Set up Ruby
14
14
  uses: ruby/setup-ruby@v1
15
15
  with:
16
- ruby-version: 3.1
17
16
  bundler-cache: true
18
17
 
19
18
  - name: Run benchmark on branch
@@ -22,7 +21,7 @@ jobs:
22
21
  - name: Run throughput benchmark on branch
23
22
  run: benchmark/local-udp-throughput
24
23
 
25
- - uses: actions/checkout@v3
24
+ - uses: actions/checkout@v4
26
25
  with:
27
26
  ref: 'main'
28
27
 
@@ -8,12 +8,11 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
 
10
10
  steps:
11
- - uses: actions/checkout@v3
11
+ - uses: actions/checkout@v4
12
12
 
13
13
  - name: Set up Ruby
14
14
  uses: ruby/setup-ruby@v1
15
15
  with:
16
- ruby-version: 2.7
17
16
  bundler-cache: true
18
17
 
19
18
  - name: Run Rubocop
@@ -9,14 +9,14 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', 'ruby-head', 'jruby-9.3.7.0', 'truffleruby-22.2.0']
12
+ ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head', 'jruby-9.4.8.0', 'truffleruby-22.3.1']
13
13
  # Windows on macOS builds started failing, so they are disabled for now
14
14
  # platform: [windows-2019, macOS-10.14, ubuntu-18.04]
15
15
  # exclude:
16
16
  # ...
17
17
 
18
18
  steps:
19
- - uses: actions/checkout@v3
19
+ - uses: actions/checkout@v4
20
20
 
21
21
  - name: Set up Ruby
22
22
  uses: ruby/setup-ruby@v1
data/.rubocop.yml CHANGED
@@ -5,7 +5,6 @@ require:
5
5
  - ./lib/statsd/instrument/rubocop.rb
6
6
 
7
7
  AllCops:
8
- TargetRubyVersion: 2.7
9
8
  UseCache: true
10
9
  SuggestExtensions: false
11
10
  CacheRootDirectory: tmp/rubocop
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.1
data/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ section below.
6
6
 
7
7
  ## Unreleased changes
8
8
 
9
+ ## Version 3.9.0
10
+
11
+ - Introduced an experimental aggregation feature to improve the efficiency of metrics reporting by aggregating
12
+ multiple metric events into a single sample. This reduces the number of network requests and can significantly
13
+ decrease the overhead associated with high-frequency metric reporting. To enable metric aggregation, set the
14
+ `STATSD_ENABLE_AGGREGATION` environment variable to true. More information on this feature is available in the README.
15
+ - Added support for sending StatsD via Unix domain sockets. This feature is enabled by
16
+ setting the `STATSD_SOCKET` environment variable to the path of the Unix domain socket.
17
+ - :warning: **Possible breaking change**: We removed/renamed some classes and now Sinks are generic, so the classes `UDPSink` and `UDPBatchedSink` are now called
18
+ `StatsD::Instrument::Sink` and `StatsD::Instrument::Sink` respectively.
19
+ If you used those internal classes, you will need to update your code to use the new classes.
20
+
21
+ ## Version 3.8.0
22
+
23
+ - UDP batching will now track statistics about its own batching performance, and
24
+ emit those statistics to the default sink when `STATSD_BATCH_STATISTICS_INTERVAL`
25
+ is set to any non-zero value. The default value is zero; additional information
26
+ on statistics tracked is available in the README.
27
+
9
28
  ## Version 3.7.0
10
29
 
11
30
  - Add public `.flush` method to sink classes.
data/Gemfile CHANGED
@@ -11,3 +11,10 @@ gem "yard"
11
11
  gem "rubocop", ">= 1.0"
12
12
  gem "rubocop-shopify", require: false
13
13
  gem "benchmark-ips"
14
+ gem "dogstatsd-ruby", "~> 5.0", require: false
15
+ platform :mri do
16
+ # only if Ruby is MRI && >= 3.2
17
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2")
18
+ gem "vernier", require: false
19
+ end
20
+ end
data/README.md CHANGED
@@ -54,6 +54,62 @@ The following environment variables are supported:
54
54
  If your network is properly configured to handle larger packets you may try
55
55
  to increase this value for better performance, but most network can't handle
56
56
  larger packets.
57
+ - `STATSD_BATCH_STATISTICS_INTERVAL`: (default: "0") If non-zero, the `BatchedUDPSink`
58
+ will track and emit statistics on this interval to the default sink for your environment.
59
+ The current tracked statistics are:
60
+
61
+ - `statsd_instrument.batched_udp_sink.batched_sends`: The number of batches sent, of any size.
62
+ - `statsd_instrument.batched_udp_sink.synchronous_sends`: The number of times the batched udp sender needed to send a statsd line synchronously, due to the buffer being full.
63
+ - `statsd_instrument.batched_udp_sink.avg_buffer_length`: The average buffer length, measured at the beginning of each batch.
64
+ - `statsd_instrument.batched_udp_sink.avg_batched_packet_size`: The average per-batch byte size of the packet sent to the underlying UDPSink.
65
+ - `statsd_instrument.batched_udp_sink.avg_batch_length`: The average number of statsd lines per batch.
66
+
67
+
68
+ ### Experimental aggregation feature
69
+
70
+ The aggregation feature is currently experimental and aims to improve the efficiency of metrics reporting by aggregating
71
+ multiple metric events into a single sample. This reduces the number of network requests and can significantly decrease the overhead
72
+ associated with high-frequency metric reporting.
73
+
74
+ This means that instead of sending each metric event individually, the library will aggregate multiple events into a single sample and send it to the StatsD server.
75
+ Example:
76
+
77
+ Instead of sending counters in multiple packets like this:
78
+ ```
79
+ my.counter:1|c
80
+ my.counter:1|c
81
+ my.counter:1|c
82
+ ```
83
+
84
+ The library will aggregate them into a single packet like this:
85
+ ```
86
+ my.counter:3|c
87
+ ```
88
+
89
+ and for histograms/distributions:
90
+ ```
91
+ my.histogram:1|h
92
+ my.histogram:2|h
93
+ my.histogram:3|h
94
+ ```
95
+
96
+ The library will aggregate them into a single packet like this:
97
+ ```
98
+ my.histogram:1:2:3|h
99
+ ```
100
+
101
+ #### Enabling Aggregation
102
+
103
+ To enable metric aggregation, set the following environment variables:
104
+
105
+ - `STATSD_ENABLE_AGGREGATION`: Set this to `true` to enable the experimental aggregation feature. Aggregation is disabled by default.
106
+ - `STATSD_AGGREGATION_INTERVAL`: Specifies the interval (in seconds) at which aggregated metrics are flushed and sent to the StatsD server.
107
+ For example, setting this to `2` will aggregate and send metrics every 2 seconds. Two seconds is also the default value if this environment variable is not set.
108
+
109
+ Please note that since aggregation is an experimental feature, it should be used with caution in production environments.
110
+
111
+ > [!WARNING]
112
+ > This feature is only compatible with Datadog Agent's version >=6.25.0 && <7.0.0 or Agent's versions >=7.25.0.
57
113
 
58
114
  ## StatsD keys
59
115
 
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
+ require "rubocop/rake_task"
5
6
 
6
7
  Rake::TestTask.new("test") do |t|
7
8
  t.ruby_opts << "-r rubygems"
@@ -9,4 +10,14 @@ Rake::TestTask.new("test") do |t|
9
10
  t.test_files = FileList["test/**/*_test.rb"]
10
11
  end
11
12
 
13
+ RuboCop::RakeTask.new(:lint) do |task|
14
+ task.options = ["-D", "-S", "-E"]
15
+ end
16
+
17
+ RuboCop::RakeTask.new(:lint_fix) do |task|
18
+ task.options = ["-a"]
19
+ end
20
+
21
+ task lf: :lint_fix
22
+
12
23
  task(default: :test)
@@ -6,28 +6,113 @@ require "benchmark/ips"
6
6
  require "tmpdir"
7
7
  require "socket"
8
8
  require "statsd-instrument"
9
+ require "datadog/statsd"
10
+ require "forwardable"
11
+ require "vernier"
12
+
13
+ class DatadogShim
14
+ extend Forwardable
15
+
16
+ def_delegator :@client, :close
17
+ # This is a shim to make the Datadog client compatible with the StatsD client
18
+ # interface. It's not a complete implementation, but it's enough to run the
19
+ # benchmarks.
20
+ # @param [Datadog::Statsd] client
21
+ def initialize(client)
22
+ @client = client
23
+ end
24
+
25
+ class NullSink
26
+ def flush(blocking: false)
27
+ end
28
+ end
29
+
30
+ def sink
31
+ @sink ||= NullSink.new
32
+ end
33
+
34
+ def increment(stat, value = 1, tags: nil)
35
+ @client.increment(stat, value: value, tags: tags)
36
+ end
37
+
38
+ def measure(stat, value = nil, tags: nil, &block)
39
+ @client.time(stat, value: value, tags: tags, &block)
40
+ end
41
+
42
+ def histogram(stat, value = nil, tags: nil, &block)
43
+ @client.histogram(stat, value: value, tags: tags, &block)
44
+ end
45
+
46
+ def gauge(stat, value, tags: nil)
47
+ @client.gauge(stat, value: value, tags: tags)
48
+ end
49
+
50
+ def set(stat, value, tags: nil)
51
+ @client.set(stat, value: value, tags: tags)
52
+ end
53
+
54
+ def event(title, text, tags: nil)
55
+ @client.event(title, text, tags: tags)
56
+ end
57
+
58
+ def service_check(name, status, tags: nil)
59
+ @client.service_check(name, status, tags: tags)
60
+ end
61
+ end
9
62
 
10
63
  def send_metrics(client)
11
64
  client.increment("StatsD.increment", 10)
12
65
  client.measure("StatsD.measure") { 1 + 1 }
13
66
  client.gauge("StatsD.gauge", 12.0, tags: ["foo:bar", "quc"])
14
- client.set("StatsD.set", "value", tags: { foo: "bar", baz: "quc" })
15
- client.event("StasD.event", "12345")
16
- client.service_check("StatsD.service_check", "ok")
17
67
  end
18
68
 
69
+ def send_metrics_high_cardinality(client)
70
+ SERIES_COUNT.times do |i|
71
+ tags = ["series:#{i}", "foo:bar", "baz:quc"]
72
+ client.increment("StatsD.increment", 10, tags: tags)
73
+ client.measure("StatsD.measure", tags: tags) { 1 + 1 }
74
+ client.gauge("StatsD.gauge", 12.0, tags: tags)
75
+ end
76
+ end
77
+
78
+ SOCKET_PATH = File.join(Dir.pwd, "tmp/metric.sock")
19
79
  THREAD_COUNT = Integer(ENV.fetch("THREAD_COUNT", 5))
20
- EVENTS_PER_ITERATION = 6
21
- ITERATIONS = 50_000
22
- def benchmark_implementation(name, env = {})
80
+ EVENTS_PER_ITERATION = 3
81
+ ITERATIONS = Integer(ENV.fetch("ITERATIONS", 10_000))
82
+ SERIES_COUNT = Integer(ENV.fetch("SERIES_COUNT", 0))
83
+ ENABLE_PROFILING = ENV.key?("ENABLE_PROFILING")
84
+ UDS_MAX_SEND_SIZE = 32_768
85
+
86
+ LOG_DIR = File.join(Dir.tmpdir, "statsd-instrument-benchmarks")
87
+ FileUtils.mkdir_p(LOG_DIR)
88
+ puts "Logs are stored in #{LOG_DIR}"
89
+
90
+ def benchmark_implementation(name, env = {}, datadog_client = false)
23
91
  intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/"
24
- log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}.log"
92
+ log_filename = File.join(LOG_DIR, "#{File.basename($PROGRAM_NAME)}-#{name}.log".tr(" ", "_"))
25
93
  FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
94
+ FileUtils.mkdir_p(File.dirname(log_filename))
26
95
 
27
96
  # Set up an UDP listener to which we can send StatsD packets
28
97
  receiver = UDPSocket.new
29
98
  receiver.bind("localhost", 0)
30
99
 
100
+ FileUtils.mkdir_p(File.dirname(SOCKET_PATH))
101
+ FileUtils.rm_f(SOCKET_PATH)
102
+ receiver_uds = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
103
+ receiver_uds.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
104
+ receiver_uds.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, UDS_MAX_SEND_SIZE * THREAD_COUNT)
105
+ receiver_uds.bind(Socket.pack_sockaddr_un(SOCKET_PATH))
106
+ # with UDS we have to take data out of the socket, otherwise it will fill up
107
+ # and we will block writing to it (which is what we are testing)
108
+ consume = Thread.new do
109
+ loop do
110
+ receiver_uds.recv(32768)
111
+ rescue
112
+ # Ignored
113
+ end
114
+ end
115
+
31
116
  log_file = File.open(log_filename, "w+", level: Logger::WARN)
32
117
  StatsD.logger = Logger.new(log_file)
33
118
 
@@ -37,23 +122,103 @@ def benchmark_implementation(name, env = {})
37
122
  "STATSD_ENV" => "production",
38
123
  ).merge(env)).client
39
124
 
40
- puts "===== #{name} throughtput (#{THREAD_COUNT} threads) ====="
125
+ if datadog_client
126
+ statsd = Datadog::Statsd.new(receiver.addr[2], receiver.addr[1], **env)
127
+ udp_client = DatadogShim.new(statsd)
128
+ end
129
+
130
+ series = SERIES_COUNT.zero? ? 1 : SERIES_COUNT
131
+ events_sent = THREAD_COUNT * EVENTS_PER_ITERATION * ITERATIONS * series
132
+ puts "===== #{name} throughput (#{THREAD_COUNT} threads) - total events: #{events_sent} ====="
133
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
41
134
  threads = THREAD_COUNT.times.map do
42
135
  Thread.new do
43
136
  count = ITERATIONS
44
137
  while (count -= 1) > 0
45
- send_metrics(udp_client)
138
+ if SERIES_COUNT.zero?
139
+ send_metrics(udp_client)
140
+ else
141
+ send_metrics_high_cardinality(udp_client)
142
+ end
46
143
  end
47
144
  end
48
145
  end
49
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
146
+
50
147
  threads.each(&:join)
148
+ udp_client.shutdown if udp_client.respond_to?(:shutdown)
149
+ if datadog_client
150
+ udp_client.close
151
+ end
152
+
51
153
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
52
- events_sent = THREAD_COUNT * EVENTS_PER_ITERATION * ITERATIONS
53
- puts "events: #{(events_sent / duration).round(1)}/s"
154
+
155
+ consume.kill
54
156
  receiver.close
55
- udp_client.shutdown if udp_client.respond_to?(:shutdown)
157
+ receiver_uds.close
158
+
159
+ series = SERIES_COUNT.zero? ? 1 : SERIES_COUNT
160
+ events_sent = THREAD_COUNT * EVENTS_PER_ITERATION * ITERATIONS * series
161
+ puts "events: #{(events_sent / duration).round(1).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}/s"
56
162
  end
57
163
 
164
+ if ENABLE_PROFILING
165
+ Vernier.start_profile(out: "tmp/benchmark_profile_udp_sync.json")
166
+ end
58
167
  benchmark_implementation("UDP sync", "STATSD_BUFFER_CAPACITY" => "0")
168
+ if ENABLE_PROFILING
169
+ Vernier.stop_profile
170
+ end
171
+
172
+ if ENABLE_PROFILING
173
+ Vernier.start_profile(out: "tmp/benchmark_profile_udp_async.json")
174
+ end
59
175
  benchmark_implementation("UDP batched")
176
+ if ENABLE_PROFILING
177
+ Vernier.stop_profile
178
+ end
179
+
180
+ if ENABLE_PROFILING
181
+ Vernier.start_profile(out: "tmp/benchmark_profile_uds_small_packet.json")
182
+ end
183
+ benchmark_implementation("UDS batched with small packet", "STATSD_SOCKET_PATH" => SOCKET_PATH)
184
+ if ENABLE_PROFILING
185
+ Vernier.stop_profile
186
+ end
187
+
188
+ if ENABLE_PROFILING
189
+ Vernier.start_profile(out: "tmp/benchmark_profile_uds_batched_async.json")
190
+ end
191
+ benchmark_implementation(
192
+ "UDS batched with jumbo packet",
193
+ "STATSD_SOCKET_PATH" => SOCKET_PATH,
194
+ "STATSD_MAX_PACKET_SIZE" => UDS_MAX_SEND_SIZE.to_s,
195
+ )
196
+ if ENABLE_PROFILING
197
+ Vernier.stop_profile
198
+ end
199
+
200
+ if ENABLE_PROFILING
201
+ Vernier.start_profile(out: "tmp/benchmark_udp_batched_with_aggregation.json")
202
+ end
203
+ benchmark_implementation(
204
+ "UDP batched with aggregation and 5 second interval",
205
+ "STATSD_ENABLE_AGGREGATION" => "true",
206
+ "STATSD_AGGREGATION_FLUSH_INTERVAL" => "5",
207
+ )
208
+ if ENABLE_PROFILING
209
+ Vernier.stop_profile
210
+ end
211
+
212
+ if ENABLE_PROFILING
213
+ Vernier.start_profile(out: "tmp/benchmark_uds_with_aggregation.json")
214
+ end
215
+ benchmark_implementation(
216
+ "UDS batched with aggregation and 5 second interval",
217
+ "STATSD_ENABLE_AGGREGATION" => "true",
218
+ "STATSD_AGGREGATION_FLUSH_INTERVAL" => "5",
219
+ "STATSD_SOCKET_PATH" => SOCKET_PATH,
220
+ "STATSD_MAX_PACKET_SIZE" => UDS_MAX_SEND_SIZE.to_s,
221
+ )
222
+ if ENABLE_PROFILING
223
+ Vernier.stop_profile
224
+ end
@@ -27,9 +27,10 @@ def benchmark_implementation(name, env = {})
27
27
  %x(git rev-parse --abbrev-ref HEAD).rstrip
28
28
  end
29
29
 
30
- intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}"
31
- log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}.log"
32
- FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
30
+ log_dir = "#{Dir.tmpdir}/statsd-instrument-benchmarks"
31
+ intermediate_results_filename = File.join(log_dir, "#{File.basename($PROGRAM_NAME)}-#{name}")
32
+ log_filename = File.join(log_dir, "#{File.basename($PROGRAM_NAME)}-#{name}.log")
33
+ FileUtils.mkdir_p(log_dir)
33
34
 
34
35
  # Set up an UDP listener to which we can send StatsD packets
35
36
  receiver = UDPSocket.new
@@ -69,7 +70,7 @@ def benchmark_implementation(name, env = {})
69
70
  File.unlink(intermediate_results_filename)
70
71
  end
71
72
 
72
- log_file.close
73
+ # log_file.close
73
74
  logs = File.read(log_filename)
74
75
  unless logs.empty?
75
76
  puts
@@ -81,3 +82,4 @@ end
81
82
 
82
83
  benchmark_implementation("UDP sync", "STATSD_BUFFER_CAPACITY" => "0")
83
84
  benchmark_implementation("UDP batched")
85
+ benchmark_implementation("UDP batched with aggregation", "STATSD_ENABLE_AGGREGATION" => "true")