statsd-instrument 3.11.2 → 4.0.1

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.
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class NewlineNormalizationTest < Minitest::Test
6
+ def setup
7
+ @old_client = StatsD.singleton_client
8
+ @sink = StatsD::Instrument::CaptureSink.new(parent: StatsD::Instrument::NullSink.new)
9
+ @clients = []
10
+ end
11
+
12
+ def teardown
13
+ StatsD.singleton_client = @old_client
14
+ @clients.each do |client|
15
+ aggregator = client.instance_variable_get(:@aggregator)
16
+ next unless aggregator
17
+
18
+ aggregator.instance_variable_get(:@flush_thread)&.kill
19
+ ObjectSpace.undefine_finalizer(aggregator)
20
+ end
21
+ end
22
+
23
+ def test_all_generic_metric_types_keep_line_breaks_inside_the_prefix
24
+ builder = StatsD::Instrument::DatagramBuilder.new(prefix: "storefront")
25
+ [:c, :g, :ms, :s, :h, :d, :kv].each do |type|
26
+ ["\n", "\r", "\r\n"].each do |delimiter|
27
+ datagram = builder.public_send(type, "http.requests#{delimiter}checkout", 1, nil, nil)
28
+ assert_equal("storefront.http.requests#{"_" * delimiter.size}checkout:1|#{type}", datagram)
29
+ end
30
+ end
31
+ end
32
+
33
+ def test_prefix_and_name_are_sanitized_without_mutating_inputs
34
+ prefix = "app\r\nnamespace"
35
+ name = "name\r\nchild"
36
+ builder = StatsD::Instrument::DatagramBuilder.new(prefix: prefix)
37
+ assert_equal("app__namespace.name__child:1|c", builder.c(name, 1, nil, nil))
38
+ assert_equal("app\r\nnamespace", prefix)
39
+ assert_equal("name\r\nchild", name)
40
+ assert_equal("name__child", builder.send(:normalize_name, name))
41
+ end
42
+
43
+ def test_array_and_hash_tags_remove_delimiters_without_mutating_inputs
44
+ builder = StatsD::Instrument::DatagramBuilder.new
45
+ array = ["k\r\ney:v\r\na|l,ue", "good:tag"].freeze
46
+ hash = { "k\r\ney" => "v\r\na|l,ue", good: "tag" }.freeze
47
+ [array, hash].each do |tags|
48
+ assert_equal("name:1|c|#key:value,good:tag", builder.c("name", 1, nil, tags))
49
+ end
50
+ assert_equal("k\r\ney:v\r\na|l,ue", array.first)
51
+ assert_equal("v\r\na|l,ue", hash.values.first)
52
+ end
53
+
54
+ def test_serialized_tags_preserve_commas_as_separators
55
+ builder = StatsD::Instrument::DatagramBuilder.new
56
+ assert_equal("name:1|c|#a:one_,b:two_", builder.c("name", 1, nil, "a:one\r,b:two\n"))
57
+ assert_equal("name:1|c|#a:one,b:two", builder.c("name", 1, nil, "a:one,b:two"))
58
+ end
59
+
60
+ def test_default_tags_in_each_supported_representation
61
+ [["k:v\n"], { k: "v\n" }, "k:v\n"].each do |tags|
62
+ builder = StatsD::Instrument::DatagramBuilder.new(default_tags: tags)
63
+ datagram = builder.c("name", 1, nil, ["extra:tag"])
64
+ refute_match(/[\r\n]/, datagram)
65
+ assert_includes(datagram, ",extra:tag")
66
+ end
67
+ end
68
+
69
+ def test_set_values_replace_newlines
70
+ builder = StatsD::Instrument::DatagramBuilder.new(prefix: "app")
71
+ assert_equal("app.users:alice__smith|s", builder.s("users", "alice\r\nsmith", nil, nil))
72
+ assert_equal("app.users:42|s", builder.s("users", 42, nil, nil))
73
+ end
74
+
75
+ def test_unicode_and_valid_inputs_are_unchanged
76
+ builder = StatsD::Instrument::DatagramBuilder.new(prefix: "shop")
77
+ assert_equal("shop.café:1|c|#city:東京", builder.c("café", 1, nil, ["city:東京"]))
78
+ name = +"safe.name"
79
+ assert_same(name, builder.send(:normalize_name, name))
80
+ end
81
+
82
+ def test_dogstatsd_service_check_name_and_tags
83
+ builder = StatsD::Instrument::DogStatsDDatagramBuilder.new(prefix: "app\n")
84
+ assert_equal("_sc|app_.service_check|0|#key:value", builder._sc("service\rcheck", :ok, tags: ["key:va\nlue"]))
85
+ # Event text has its own documented escaping; tag normalization must preserve it.
86
+ event = builder._e("title", "first\nsecond", tags: ["key:va\nlue"])
87
+ refute_match(/[\r\n]/, event)
88
+ assert_includes(event, 'first\nsecond')
89
+ assert_includes(event, "|#key:value")
90
+ end
91
+
92
+ def test_normal_and_aggregated_client_paths
93
+ [false, true].each do |aggregate|
94
+ client = new_client(aggregate)
95
+ [:increment, :gauge, :measure, :distribution, :histogram].each do |method|
96
+ client.public_send(method, "metric\r\nname", 1, tags: { "k\r\ney" => "va\nlue" })
97
+ client.public_send(method, "unprefixed\nname", 1, tags: ["key:va\nlue"], no_prefix: true)
98
+ end
99
+ client.set("set\nname", "v\nalue", tags: ["key:va\nlue"])
100
+ client.force_flush
101
+ assert_equal(11, @sink.datagrams.size)
102
+ @sink.datagrams.each do |datagram|
103
+ refute_match(/[\r\n]/, datagram.source)
104
+ assert_includes(datagram.source, "key:value")
105
+ end
106
+ @sink.clear
107
+ end
108
+ end
109
+
110
+ def test_aggregator_finalizer_and_unhealthy_thread_fallback
111
+ client = new_client(true)
112
+ aggregator = client.instance_variable_get(:@aggregator)
113
+ client.increment("name\nchild", tags: ["key:va\nlue"])
114
+ finalizer = aggregator.instance_variable_get(:@finalizer)
115
+ StatsD::Instrument::Aggregator.finalize(finalizer).call
116
+ assert_equal(1, @sink.datagrams.size)
117
+ refute_match(/[\r\n]/, @sink.datagrams.first.source)
118
+
119
+ @sink.clear
120
+ aggregator.stubs(:thread_healthcheck).returns(false)
121
+ client.increment("name\nchild", tags: ["key:va\nlue"])
122
+ assert_equal(1, @sink.datagrams.size)
123
+ refute_match(/[\r\n]/, @sink.datagrams.first.source)
124
+ end
125
+
126
+ def test_compiled_static_and_dynamic_fields_with_and_without_aggregation
127
+ [false, true].each do |aggregate|
128
+ client = new_client(aggregate)
129
+ StatsD.singleton_client = client
130
+ [
131
+ StatsD::Instrument::CompiledMetric::Counter,
132
+ StatsD::Instrument::CompiledMetric::Gauge,
133
+ StatsD::Instrument::CompiledMetric::Distribution,
134
+ ].each do |base|
135
+ metric = Class.new(base) do
136
+ define(name: "name\r\nchild", static_tags: { "static\nkey" => "va\rlue" }, tags: { dynamic: String, symbol: Symbol })
137
+ end
138
+ 2.times { metric.public_send(base.method_name, 1, dynamic: "va\nlue", symbol: :"sym\rbol") }
139
+ client.force_flush
140
+ assert_equal(aggregate ? 1 : 2, @sink.datagrams.size)
141
+ @sink.datagrams.each do |datagram|
142
+ refute_match(/[\r\n]/, datagram.source)
143
+ assert_includes(datagram.source, "app_.name__child:")
144
+ assert_includes(datagram.source, "statickey:value")
145
+ assert_includes(datagram.source, "dynamic:value,symbol:symbol")
146
+ end
147
+ @sink.clear
148
+
149
+ metric = Class.new(base) { define(name: "static\nname", static_tags: { key: "va\nlue" }) }
150
+ metric.public_send(base.method_name, 1)
151
+ client.force_flush
152
+ assert_equal(1, @sink.datagrams.size)
153
+ refute_match(/[\r\n]/, @sink.datagrams.first.source)
154
+ @sink.clear
155
+ end
156
+ end
157
+ end
158
+
159
+ def test_compiled_default_tags_and_dynamic_keys
160
+ builder = StatsD::Instrument::CompiledMetric::DatagramBlueprintBuilder
161
+ [["default:va\nlue"], { default: "va\nlue" }, "default:va\nlue"].each do |tags|
162
+ blueprint = builder.build(
163
+ name: "name\n",
164
+ type: "c",
165
+ client_prefix: "app\r",
166
+ no_prefix: false,
167
+ default_tags: tags,
168
+ static_tags: {},
169
+ dynamic_tags: { "dy\nnamic" => String },
170
+ sample_rate: 1,
171
+ )
172
+ datagram = StatsD::Instrument::CompiledMetric::PrecompiledDatagram.new(["va\nlue"], blueprint, 1).to_datagram(1)
173
+ assert_equal("app_.name_:1|c|#default:value,dynamic:value", datagram)
174
+ end
175
+ end
176
+
177
+ def test_compiled_dynamic_tags_are_checked_after_mutation_and_with_cache_disabled
178
+ StatsD.singleton_client = new_client(false)
179
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
180
+ define(name: "name", tags: { key: String }, max_cache_size: 0)
181
+ end
182
+ tag = +"value"
183
+ metric.increment(key: tag)
184
+ @sink.clear
185
+ tag << "\nwith|newline"
186
+ metric.increment(key: tag)
187
+ assert_equal(1, @sink.datagrams.size)
188
+ refute_match(/[\r\n]/, @sink.datagrams.first.source)
189
+ assert_includes(@sink.datagrams.first.source, "|#default:value,key:valuewithnewline")
190
+ end
191
+
192
+ def test_compiled_name_does_not_freeze_the_input_string
193
+ StatsD.singleton_client = new_client(false)
194
+ name = +"name"
195
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
196
+ metric.define(name: name)
197
+ name << ".changed"
198
+ assert_equal("name", metric.metric_name)
199
+ assert_predicate(metric.metric_name, :frozen?)
200
+ end
201
+
202
+ def test_normalization_is_silent
203
+ StatsD.logger.expects(:warn).never
204
+ StatsD.logger.expects(:error).never
205
+ client = new_client(false)
206
+ client.increment("name\n", tags: ["key:va\nlue"])
207
+ assert_equal(1, @sink.datagrams.size)
208
+ assert_equal("app_.name_:1|c|#default:value,key:value", @sink.datagrams.first.source)
209
+ end
210
+
211
+ def test_names_and_prefixes_share_normalization_across_all_paths
212
+ [false, true].each do |aggregate|
213
+ [" ", "\t", "\n", "\r", "\f", "\v", ":", "|", "@"].each do |character|
214
+ client = new_client(aggregate).clone_with_options(prefix: "app#{character}prefix")
215
+ @clients << client
216
+ StatsD.singleton_client = client
217
+ client.increment("my#{character}value")
218
+ client.increment("my#{character}value", no_prefix: true)
219
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
220
+ metric.define(name: "my#{character}value")
221
+ metric.increment
222
+ unprefixed = Class.new(StatsD::Instrument::CompiledMetric::Counter)
223
+ unprefixed.define(name: "my#{character}value", no_prefix: true)
224
+ unprefixed.increment
225
+ client.force_flush
226
+ assert_equal(4, @sink.datagrams.size)
227
+ assert_equal(["app_prefix.my_value", "app_prefix.my_value", "my_value", "my_value"], @sink.datagrams.map(&:name).sort)
228
+ assert_equal("my_value", metric.metric_name)
229
+ @sink.clear
230
+ end
231
+ end
232
+ end
233
+
234
+ def test_tag_values_and_service_check_messages_keep_spaces
235
+ builder = StatsD::Instrument::DogStatsDDatagramBuilder.new(prefix: "my app")
236
+ assert_equal("my_app.my_metric:1|c|#key:hello world", builder.c("my metric", 1, nil, ["key:hello world"]))
237
+ assert_equal("my_app.my_metric:1|c|#key:hello world", builder.c("my metric", 1, nil, { key: "hello world" }))
238
+ assert_equal("my_app.users:Alice Smith|s", builder.s("users", "Alice Smith", nil, nil))
239
+ assert_equal("_sc|my_app.my_service|0|m:hello world_", builder._sc("my service", :ok, message: "hello world\n"))
240
+
241
+ StatsD.singleton_client = new_client(false)
242
+ metric = Class.new(StatsD::Instrument::CompiledMetric::Counter)
243
+ metric.define(name: "my metric", static_tags: { static: "hello world" }, tags: { dynamic: String })
244
+ metric.increment(dynamic: "hello world")
245
+ assert_includes(@sink.datagrams.last.tags, "static:hello world")
246
+ assert_includes(@sink.datagrams.last.tags, "dynamic:hello world")
247
+ end
248
+
249
+ private
250
+
251
+ def new_client(aggregate)
252
+ client = StatsD::Instrument::Client.new(
253
+ sink: @sink,
254
+ prefix: "app\n",
255
+ default_tags: ["default:va\nlue"],
256
+ enable_aggregation: aggregate,
257
+ aggregation_flush_interval: 3600,
258
+ )
259
+ @clients << client
260
+ client
261
+ end
262
+ end
@@ -19,9 +19,7 @@ module Rubocop
19
19
  def test_offense_statsd_prefix
20
20
  assert_offense('StatsD.prefix = "foo"')
21
21
  # The interpolation is intentionally literal source passed to the cop under test.
22
- # rubocop:disable Lint/InterpolationCheck
23
- assert_offense('"#{StatsD.prefix}.foo"')
24
- # rubocop:enable Lint/InterpolationCheck
22
+ assert_offense("\"\#{StatsD.prefix}.foo\"")
25
23
  end
26
24
 
27
25
  def test_offense_statsd_default_tags
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class SanitizationTest < Minitest::Test
6
+ def test_clean_input_is_returned_without_copying_or_freezing
7
+ [:name, :tag, :service_check_message].each do |method|
8
+ mutable = +"café.safe_value"
9
+ assert_same(mutable, StatsD::Instrument::Sanitization.public_send(method, mutable))
10
+ refute_predicate(mutable, :frozen?)
11
+ assert_same(mutable.freeze, StatsD::Instrument::Sanitization.public_send(method, mutable))
12
+ end
13
+ end
14
+
15
+ def test_name_replaces_each_ascii_whitespace_and_protocol_delimiter
16
+ [" ", "\t", "\n", "\r", "\f", "\v", ":", "|", "@"].each do |character|
17
+ name = +"prefix.#{character}my#{character}value"
18
+ expected_original = name.dup
19
+ assert_equal("prefix._my_value", StatsD::Instrument::Sanitization.name(name))
20
+ assert_equal(expected_original, name)
21
+ refute_predicate(name, :frozen?)
22
+ end
23
+ assert_equal("name___value", StatsD::Instrument::Sanitization.name("name \t\nvalue"))
24
+ end
25
+
26
+ def test_name_does_not_expand_whitespace_matching_to_unicode
27
+ name = "café\u00a0value"
28
+ assert_same(name, StatsD::Instrument::Sanitization.name(name))
29
+ end
30
+
31
+ def test_tags_and_values_keep_ordinary_whitespace
32
+ text = "hello \tworld\f\v"
33
+ [:tag, :service_check_message].each do |method|
34
+ assert_same(text, StatsD::Instrument::Sanitization.public_send(method, text))
35
+ end
36
+ end
37
+
38
+ def test_each_field_preserves_its_existing_replacement_rules
39
+ sanitizer = StatsD::Instrument::Sanitization
40
+ assert_equal("tag:value", sanitizer.tag("tag:va|,\r\nlue"))
41
+ assert_equal("hello world____", sanitizer.service_check_message("hello world:|@\n"))
42
+ end
43
+
44
+ def test_compiled_name_helper_returns_clean_input_unchanged
45
+ name = +"name"
46
+ assert_same(name, StatsD::Instrument::CompiledMetric::DatagramBlueprintBuilder.normalize_name(name))
47
+ refute_predicate(name, :frozen?)
48
+ end
49
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsd-instrument
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.11.2
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Storimer
@@ -71,6 +71,7 @@ files:
71
71
  - lib/statsd/instrument/rubocop/positional_arguments.rb
72
72
  - lib/statsd/instrument/rubocop/singleton_configuration.rb
73
73
  - lib/statsd/instrument/rubocop/splat_arguments.rb
74
+ - lib/statsd/instrument/sanitization.rb
74
75
  - lib/statsd/instrument/sink.rb
75
76
  - lib/statsd/instrument/statsd_datagram_builder.rb
76
77
  - lib/statsd/instrument/strict.rb
@@ -101,6 +102,7 @@ files:
101
102
  - test/integration_test.rb
102
103
  - test/log_sink_test.rb
103
104
  - test/matchers_test.rb
105
+ - test/newline_normalization_test.rb
104
106
  - test/null_sink_test.rb
105
107
  - test/rubocop/measure_as_dist_argument_test.rb
106
108
  - test/rubocop/metaprogramming_positional_arguments_test.rb
@@ -110,6 +112,7 @@ files:
110
112
  - test/rubocop/positional_arguments_test.rb
111
113
  - test/rubocop/singleton_configuration_test.rb
112
114
  - test/rubocop/splat_arguments_test.rb
115
+ - test/sanitization_test.rb
113
116
  - test/statsd_datagram_builder_test.rb
114
117
  - test/statsd_instrumentation_test.rb
115
118
  - test/statsd_test.rb
@@ -135,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
138
  - !ruby/object:Gem::Version
136
139
  version: '0'
137
140
  requirements: []
138
- rubygems_version: 4.0.18
141
+ rubygems_version: 4.0.21
139
142
  specification_version: 4
140
143
  summary: A StatsD client for Ruby apps
141
144
  test_files:
@@ -161,6 +164,7 @@ test_files:
161
164
  - test/integration_test.rb
162
165
  - test/log_sink_test.rb
163
166
  - test/matchers_test.rb
167
+ - test/newline_normalization_test.rb
164
168
  - test/null_sink_test.rb
165
169
  - test/rubocop/measure_as_dist_argument_test.rb
166
170
  - test/rubocop/metaprogramming_positional_arguments_test.rb
@@ -170,6 +174,7 @@ test_files:
170
174
  - test/rubocop/positional_arguments_test.rb
171
175
  - test/rubocop/singleton_configuration_test.rb
172
176
  - test/rubocop/splat_arguments_test.rb
177
+ - test/sanitization_test.rb
173
178
  - test/statsd_datagram_builder_test.rb
174
179
  - test/statsd_instrumentation_test.rb
175
180
  - test/statsd_test.rb