statsd-instrument 4.0.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +5 -0
- data/lib/statsd/instrument/client.rb +2 -2
- data/lib/statsd/instrument/compiled_metric.rb +9 -9
- data/lib/statsd/instrument/datagram.rb +3 -3
- data/lib/statsd/instrument/datagram_builder.rb +12 -12
- data/lib/statsd/instrument/dogstatsd_datagram.rb +11 -11
- data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +1 -1
- data/lib/statsd/instrument/sanitization.rb +34 -0
- data/lib/statsd/instrument/version.rb +1 -1
- data/lib/statsd/instrument.rb +1 -0
- data/test/newline_normalization_test.rb +262 -0
- data/test/rubocop/singleton_configuration_test.rb +1 -3
- data/test/sanitization_test.rb +49 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7184b3fb0852d7afea02fa71a292812ef26b491eb94ab2427f603b49f01202c
|
|
4
|
+
data.tar.gz: e40e7aa3d4cb034467b6d59fdfdb3bcbf55a2901e82c48368fd1e904d0aa67c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1d01801fcbedc88df3e57bf948e6a4b599b52365e80d55e66ced52d4b308427632d5dc8161614705a7524e9a7f1b56e85e717ef9e3f0509969091b801ec30c2
|
|
7
|
+
data.tar.gz: 97bf3cf218ef6be51d72dc96f067464cc8f21eb0b070be9e48d7097cc4863735f41a58a3ed6967829524b6df64af1d8a1122197fec46944ce8154dcf539a4721
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,10 @@ section below.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased changes
|
|
8
8
|
|
|
9
|
+
## Version 4.0.1
|
|
10
|
+
|
|
11
|
+
- Normalize ASCII whitespace in metric names and prefixes, and newlines (`\n` and `\r`) in tags and set values, including aggregated and compiled metrics.
|
|
12
|
+
|
|
9
13
|
## Version 4.0.0
|
|
10
14
|
|
|
11
15
|
- Make the internal aggregator interface positional-only and injectable through `Client`, with fixed-arity `increment`, `gauge`, and `aggregate_timing` entrypoints for native aggregation backends. Public Client metric methods retain their keyword API.
|
data/README.md
CHANGED
|
@@ -114,6 +114,11 @@ Please note that since aggregation is an experimental feature, it should be used
|
|
|
114
114
|
## StatsD keys
|
|
115
115
|
|
|
116
116
|
StatsD keys look like 'admin.logins.api.success'. Dots are used as namespace separators.
|
|
117
|
+
Metric names and prefixes normalize `:`, `|`, `@`, and ASCII whitespace (spaces,
|
|
118
|
+
tabs, and line breaks) to `_`, one underscore per character.
|
|
119
|
+
Newlines (`\n`, `\r`) in set values are also replaced with `_`; in array/hash tags,
|
|
120
|
+
newlines are removed alongside `|` and `,`. Other tag/value whitespace is preserved.
|
|
121
|
+
Normalization is silent.
|
|
117
122
|
|
|
118
123
|
## Usage
|
|
119
124
|
|
|
@@ -213,8 +213,8 @@ module StatsD
|
|
|
213
213
|
#
|
|
214
214
|
# - We recommend using `snake_case.metric_names` as naming scheme.
|
|
215
215
|
# - A `.` should be used for namespacing, e.g. `foo.bar.baz`
|
|
216
|
-
# - A metric name should not include
|
|
217
|
-
# The library
|
|
216
|
+
# - A metric name should not include `|`, `@`, `:`, or ASCII whitespace.
|
|
217
|
+
# The library converts each such character to `_`, including in prefixes.
|
|
218
218
|
#
|
|
219
219
|
# @param value [Integer] (default: 1) The value to increment the counter by.
|
|
220
220
|
#
|
|
@@ -70,7 +70,8 @@ module StatsD
|
|
|
70
70
|
# Create a new class for this specific metric
|
|
71
71
|
# Using classes instead of instances for better YJIT optimization
|
|
72
72
|
metric_class = tap do
|
|
73
|
-
|
|
73
|
+
# Own a frozen name without freezing a mutable string from the caller.
|
|
74
|
+
@name = -DatagramBlueprintBuilder.normalize_name(name)
|
|
74
75
|
@datagram_blueprint = datagram_blueprint
|
|
75
76
|
@tag_combination_cache = {}
|
|
76
77
|
@max_cache_size = max_cache_size
|
|
@@ -300,9 +301,10 @@ module StatsD
|
|
|
300
301
|
|
|
301
302
|
# Normalizes metric names by replacing special characters
|
|
302
303
|
# @param name [String] The metric name
|
|
303
|
-
# @return [String] The
|
|
304
|
+
# @return [String] The original string if clean, otherwise a new string.
|
|
305
|
+
# Does not copy or freeze clean input; retaining callers own that step.
|
|
304
306
|
def normalize_name(name)
|
|
305
|
-
name
|
|
307
|
+
Sanitization.name(name)
|
|
306
308
|
end
|
|
307
309
|
|
|
308
310
|
private
|
|
@@ -314,16 +316,14 @@ module StatsD
|
|
|
314
316
|
def build_prefix(client_prefix, no_prefix)
|
|
315
317
|
return "" if no_prefix || client_prefix.nil?
|
|
316
318
|
|
|
317
|
-
"#{client_prefix}."
|
|
319
|
+
"#{Sanitization.name(client_prefix.to_s)}."
|
|
318
320
|
end
|
|
319
321
|
|
|
320
322
|
# Normalizes tag names/values by removing StatsD protocol special characters
|
|
321
323
|
# @param str [Symbol, String, Integer, Float] The string to normalize
|
|
322
324
|
# @return [String] The normalized string
|
|
323
325
|
def normalize_statsd_string(str)
|
|
324
|
-
str
|
|
325
|
-
str = str.tr("|,", "") if /[|,]/.match?(str)
|
|
326
|
-
str
|
|
326
|
+
Sanitization.tag(str.to_s)
|
|
327
327
|
end
|
|
328
328
|
|
|
329
329
|
# Compiles all tags (default_tags, static_tags, dynamic_tags) into a single string
|
|
@@ -412,10 +412,10 @@ module StatsD
|
|
|
412
412
|
# Sanitize string and symbol values (other types handled by sprintf %s)
|
|
413
413
|
values = @tag_values.map do |arg|
|
|
414
414
|
if arg.is_a?(String)
|
|
415
|
-
|
|
415
|
+
Sanitization::TAG_PATTERN.match?(arg) ? arg.tr(Sanitization::TAG_CHARACTERS, "") : arg
|
|
416
416
|
elsif arg.is_a?(Symbol)
|
|
417
417
|
str = arg.to_s
|
|
418
|
-
|
|
418
|
+
Sanitization::TAG_PATTERN.match?(str) ? str.tr(Sanitization::TAG_CHARACTERS, "") : str
|
|
419
419
|
else
|
|
420
420
|
arg
|
|
421
421
|
end
|
|
@@ -72,9 +72,9 @@ module StatsD
|
|
|
72
72
|
|
|
73
73
|
PARSER = %r{
|
|
74
74
|
\A
|
|
75
|
-
(?<name>[
|
|
76
|
-
(
|
|
77
|
-
(?:\|\#(?<tags>(?:[
|
|
75
|
+
(?<name>[^:|@]+):(?<value>(?:[^:|@]+:)*[^:|@]+)\|(?<type>c|ms|g|s|h|d)
|
|
76
|
+
(?:\|@(?<sample_rate>\d*(?:\.\d*)?))?
|
|
77
|
+
(?:\|\#(?<tags>(?:[^|,]+(?:,[^|,]+)*)))?
|
|
78
78
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
79
79
|
\z
|
|
80
80
|
}x.freeze
|
|
@@ -20,13 +20,13 @@ module StatsD
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def normalize_string(string)
|
|
23
|
-
string = string.tr("
|
|
23
|
+
string = string.tr("|#\r\n", "_") if /[|#\r\n]/.match?(string)
|
|
24
24
|
string
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def initialize(prefix: nil, default_tags: nil)
|
|
29
|
-
@prefix = prefix.nil? ? "" : "#{prefix}."
|
|
29
|
+
@prefix = prefix.nil? ? "" : "#{Sanitization.name(prefix.to_s)}."
|
|
30
30
|
@default_tags = default_tags.nil? || default_tags.empty? ? nil : compile_tags(default_tags, "|#".b)
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -43,6 +43,8 @@ module StatsD
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def s(name, value, sample_rate, tags)
|
|
46
|
+
value = value.to_s
|
|
47
|
+
value = value.tr("\r\n", "_") if /[\r\n]/.match?(value)
|
|
46
48
|
generate_generic_datagram(name, value, "s", sample_rate, tags)
|
|
47
49
|
end
|
|
48
50
|
|
|
@@ -76,16 +78,13 @@ module StatsD
|
|
|
76
78
|
|
|
77
79
|
# Utility function to remove invalid characters from a StatsD metric name
|
|
78
80
|
def normalize_name(name)
|
|
79
|
-
|
|
80
|
-
return name unless /[:|@]/.match?(name)
|
|
81
|
-
|
|
82
|
-
name.tr(":|@", "_")
|
|
81
|
+
Sanitization.name(name)
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
def generate_generic_datagram(name, value, type, sample_rate, tags)
|
|
86
85
|
datagram = "".b <<
|
|
87
86
|
@prefix <<
|
|
88
|
-
(
|
|
87
|
+
(Sanitization::NAME_PATTERN.match?(name) ? name.tr(Sanitization::NAME_CHARACTERS, "_") : name) <<
|
|
89
88
|
":" << value.to_s <<
|
|
90
89
|
"|" << type
|
|
91
90
|
|
|
@@ -105,7 +104,8 @@ module StatsD
|
|
|
105
104
|
|
|
106
105
|
def compile_tags(tags, buffer = "".b)
|
|
107
106
|
if tags.is_a?(String)
|
|
108
|
-
|
|
107
|
+
# String tags are already serialized: commas separate tags here.
|
|
108
|
+
tags = self.class.normalize_string(tags) if Sanitization::TAG_PATTERN.match?(tags)
|
|
109
109
|
buffer << tags
|
|
110
110
|
return buffer
|
|
111
111
|
end
|
|
@@ -118,14 +118,14 @@ module StatsD
|
|
|
118
118
|
buffer << ","
|
|
119
119
|
end
|
|
120
120
|
key = key.to_s
|
|
121
|
-
key = key.tr(
|
|
121
|
+
key = key.tr(Sanitization::TAG_CHARACTERS, "") if Sanitization::TAG_PATTERN.match?(key)
|
|
122
122
|
value = value.to_s
|
|
123
|
-
value = value.tr(
|
|
123
|
+
value = value.tr(Sanitization::TAG_CHARACTERS, "") if Sanitization::TAG_PATTERN.match?(value)
|
|
124
124
|
buffer << key << ":" << value
|
|
125
125
|
end
|
|
126
126
|
else
|
|
127
|
-
if tags.any? { |tag|
|
|
128
|
-
tags = tags.map { |tag| tag
|
|
127
|
+
if tags.any? { |tag| Sanitization::TAG_PATTERN.match?(tag) }
|
|
128
|
+
tags = tags.map { |tag| Sanitization.tag(tag) }
|
|
129
129
|
end
|
|
130
130
|
buffer << tags.join(",")
|
|
131
131
|
end
|
|
@@ -62,11 +62,11 @@ module StatsD
|
|
|
62
62
|
|
|
63
63
|
SERVICE_CHECK_PARSER = %r{
|
|
64
64
|
\A
|
|
65
|
-
(?<type>_sc)\|(?<name>[
|
|
66
|
-
(?:\|h:(?<hostname>[
|
|
65
|
+
(?<type>_sc)\|(?<name>[^|]+)\|(?<value>\d+)
|
|
66
|
+
(?:\|h:(?<hostname>[^|]+))?
|
|
67
67
|
(?:\|d:(?<timestamp>\d+))?
|
|
68
|
-
(?:\|\#(?<tags>(?:[
|
|
69
|
-
(?:\|m:(?<message>[
|
|
68
|
+
(?:\|\#(?<tags>(?:[^|,]+(?:,[^|,]+)*)))?
|
|
69
|
+
(?:\|m:(?<message>[^|]+))?
|
|
70
70
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
71
71
|
\z
|
|
72
72
|
}x.freeze
|
|
@@ -74,14 +74,14 @@ module StatsD
|
|
|
74
74
|
# |k:my-key|p:low|s:source|t:success|
|
|
75
75
|
EVENT_PARSER = %r{
|
|
76
76
|
\A
|
|
77
|
-
(?<type>_e)\{\d
|
|
78
|
-
(?:\|h:(?<hostname>[
|
|
77
|
+
(?<type>_e)\{\d+,\d+\}:(?<name>[^|]+)\|(?<value>[^|]+)
|
|
78
|
+
(?:\|h:(?<hostname>[^|]+))?
|
|
79
79
|
(?:\|d:(?<timestamp>\d+))?
|
|
80
|
-
(?:\|k:(?<aggregation_key>[
|
|
81
|
-
(?:\|p:(?<priority>[
|
|
82
|
-
(?:\|s:(?<source_type_name>[
|
|
83
|
-
(?:\|t:(?<alert_type>[
|
|
84
|
-
(?:\|\#(?<tags>(?:[
|
|
80
|
+
(?:\|k:(?<aggregation_key>[^|]+))?
|
|
81
|
+
(?:\|p:(?<priority>[^|]+))?
|
|
82
|
+
(?:\|s:(?<source_type_name>[^|]+))?
|
|
83
|
+
(?:\|t:(?<alert_type>[^|]+))?
|
|
84
|
+
(?:\|\#(?<tags>(?:[^|,]+(?:,[^|,]+)*)))?
|
|
85
85
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
86
86
|
\z
|
|
87
87
|
}x.freeze
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StatsD
|
|
4
|
+
module Instrument
|
|
5
|
+
# Field-specific wire normalization. Clean strings are returned unchanged;
|
|
6
|
+
# callers retaining a frozen value must take ownership themselves.
|
|
7
|
+
# @api private
|
|
8
|
+
module Sanitization
|
|
9
|
+
# Hot serializers use these same rules inline to avoid per-field Ruby calls.
|
|
10
|
+
NAME_PATTERN = /[:|@\s]/.freeze
|
|
11
|
+
NAME_CHARACTERS = ":|@ \t\r\n\f\v"
|
|
12
|
+
TAG_PATTERN = /[|,\r\n]/.freeze
|
|
13
|
+
TAG_CHARACTERS = "|,\r\n"
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Replace each ASCII whitespace or protocol delimiter with one underscore.
|
|
17
|
+
# @return [String] The original string if clean, otherwise a new string.
|
|
18
|
+
def name(string)
|
|
19
|
+
NAME_PATTERN.match?(string) ? string.tr(NAME_CHARACTERS, "_") : string
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Tag components allow spaces and colons, but not field/tag separators.
|
|
23
|
+
def tag(string)
|
|
24
|
+
TAG_PATTERN.match?(string) ? string.tr(TAG_CHARACTERS, "") : string
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Keep message whitespace intact; this is not a metric name.
|
|
28
|
+
def service_check_message(string)
|
|
29
|
+
/[:|@\r\n]/.match?(string) ? string.tr(":|@\r\n", "_") : string
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/statsd/instrument.rb
CHANGED
|
@@ -391,6 +391,7 @@ require "statsd/instrument/client"
|
|
|
391
391
|
require "statsd/instrument/datagram"
|
|
392
392
|
require "statsd/instrument/aggregator"
|
|
393
393
|
require "statsd/instrument/dogstatsd_datagram"
|
|
394
|
+
require "statsd/instrument/sanitization"
|
|
394
395
|
require "statsd/instrument/datagram_builder"
|
|
395
396
|
require "statsd/instrument/statsd_datagram_builder"
|
|
396
397
|
require "statsd/instrument/dogstatsd_datagram_builder"
|
|
@@ -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
|
-
|
|
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: 4.0.
|
|
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.
|
|
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
|