dogstatsd-ruby 4.3.0 → 4.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +36 -78
- data/lib/datadog/statsd.rb +108 -343
- data/lib/datadog/statsd/batch.rb +56 -0
- data/lib/datadog/statsd/connection.rb +62 -0
- data/lib/datadog/statsd/serialization.rb +15 -0
- data/lib/datadog/statsd/serialization/event_serializer.rb +68 -0
- data/lib/datadog/statsd/serialization/serializer.rb +41 -0
- data/lib/datadog/statsd/serialization/service_check_serializer.rb +61 -0
- data/lib/datadog/statsd/serialization/stat_serializer.rb +62 -0
- data/lib/datadog/statsd/serialization/tag_serializer.rb +91 -0
- data/lib/datadog/statsd/telemetry.rb +98 -0
- data/lib/datadog/statsd/udp_connection.rb +37 -0
- data/lib/datadog/statsd/uds_connection.rb +38 -0
- data/lib/datadog/statsd/version.rb +9 -0
- metadata +21 -5
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class Batch
|
6
|
+
def initialize(connection, max_buffer_bytes)
|
7
|
+
@connection = connection
|
8
|
+
@max_buffer_bytes = max_buffer_bytes
|
9
|
+
@depth = 0
|
10
|
+
reset
|
11
|
+
end
|
12
|
+
|
13
|
+
def open
|
14
|
+
@depth += 1
|
15
|
+
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
@depth -= 1
|
19
|
+
flush if !open?
|
20
|
+
end
|
21
|
+
|
22
|
+
def open?
|
23
|
+
@depth > 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(message)
|
27
|
+
message_bytes = message.bytesize
|
28
|
+
|
29
|
+
unless @buffer_bytes == 0
|
30
|
+
if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
|
31
|
+
flush
|
32
|
+
else
|
33
|
+
@buffer << "\n"
|
34
|
+
@buffer_bytes += 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@buffer << message
|
39
|
+
@buffer_bytes += message_bytes
|
40
|
+
end
|
41
|
+
|
42
|
+
def flush
|
43
|
+
return if @buffer_bytes == 0
|
44
|
+
@connection.write(@buffer)
|
45
|
+
reset
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def reset
|
51
|
+
@buffer = String.new
|
52
|
+
@buffer_bytes = 0
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class Connection
|
6
|
+
def initialize(telemetry)
|
7
|
+
@telemetry = telemetry
|
8
|
+
end
|
9
|
+
|
10
|
+
# Close the underlying socket
|
11
|
+
def close
|
12
|
+
begin
|
13
|
+
@socket && @socket.close if instance_variable_defined?(:@socket)
|
14
|
+
rescue StandardError => boom
|
15
|
+
logger.error { "Statsd: #{boom.class} #{boom}" } if logger
|
16
|
+
end
|
17
|
+
@socket = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(payload)
|
21
|
+
logger.debug { "Statsd: #{payload}" } if logger
|
22
|
+
|
23
|
+
flush_telemetry = telemetry.flush?
|
24
|
+
|
25
|
+
payload += telemetry.flush if flush_telemetry
|
26
|
+
|
27
|
+
send_message(payload)
|
28
|
+
|
29
|
+
telemetry.reset if flush_telemetry
|
30
|
+
|
31
|
+
telemetry.sent(packets: 1, bytes: payload.length)
|
32
|
+
rescue StandardError => boom
|
33
|
+
# Try once to reconnect if the socket has been closed
|
34
|
+
retries ||= 1
|
35
|
+
if retries <= 1 &&
|
36
|
+
(boom.is_a?(Errno::ENOTCONN) or
|
37
|
+
boom.is_a?(Errno::ECONNREFUSED) or
|
38
|
+
boom.is_a?(IOError) && boom.message =~ /closed stream/i)
|
39
|
+
retries += 1
|
40
|
+
begin
|
41
|
+
close
|
42
|
+
retry
|
43
|
+
rescue StandardError => e
|
44
|
+
boom = e
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
telemetry.dropped(packets: 1, bytes: payload.length)
|
49
|
+
logger.error { "Statsd: #{boom.class} #{boom}" } if logger
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
attr_reader :telemetry
|
55
|
+
attr_reader :logger
|
56
|
+
|
57
|
+
def socket
|
58
|
+
@socket ||= connect
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require_relative 'serialization/tag_serializer'
|
11
|
+
require_relative 'serialization/service_check_serializer'
|
12
|
+
require_relative 'serialization/event_serializer'
|
13
|
+
require_relative 'serialization/stat_serializer'
|
14
|
+
|
15
|
+
require_relative 'serialization/serializer'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
class EventSerializer
|
7
|
+
EVENT_BASIC_OPTIONS = {
|
8
|
+
date_happened: 'd:',
|
9
|
+
hostname: 'h:',
|
10
|
+
aggregation_key: 'k:',
|
11
|
+
priority: 'p:',
|
12
|
+
source_type_name: 's:',
|
13
|
+
alert_type: 't:',
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(global_tags: [])
|
17
|
+
@tag_serializer = TagSerializer.new(global_tags)
|
18
|
+
end
|
19
|
+
|
20
|
+
def format(title, text, options = EMPTY_OPTIONS)
|
21
|
+
title = escape(title)
|
22
|
+
text = escape(text)
|
23
|
+
|
24
|
+
String.new.tap do |event|
|
25
|
+
event << '_e{'
|
26
|
+
event << title.bytesize.to_s
|
27
|
+
event << ','
|
28
|
+
event << text.bytesize.to_s
|
29
|
+
event << '}:'
|
30
|
+
event << title
|
31
|
+
event << '|'
|
32
|
+
event << text
|
33
|
+
|
34
|
+
# we are serializing the generic service check options
|
35
|
+
# before serializing specialized options that need edge-cases
|
36
|
+
EVENT_BASIC_OPTIONS.each do |option_key, shortcut|
|
37
|
+
if value = options[option_key]
|
38
|
+
event << '|'
|
39
|
+
event << shortcut
|
40
|
+
event << value.to_s.delete('|')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if raw_tags = options[:tags]
|
45
|
+
if tags = tag_serializer.format(raw_tags)
|
46
|
+
event << '|#'
|
47
|
+
event << tags
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if event.bytesize > MAX_EVENT_SIZE
|
52
|
+
raise "Event #{title} payload is too big (more that 8KB), event discarded"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
attr_reader :tag_serializer
|
59
|
+
|
60
|
+
def escape(text)
|
61
|
+
text.delete('|').tap do |t|
|
62
|
+
t.gsub!("\n", '\n')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# require 'forwardable'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
class Statsd
|
7
|
+
module Serialization
|
8
|
+
class Serializer
|
9
|
+
def initialize(prefix: nil, global_tags: [])
|
10
|
+
@stat_serializer = StatSerializer.new(prefix, global_tags: global_tags)
|
11
|
+
@service_check_serializer = ServiceCheckSerializer.new(global_tags: global_tags)
|
12
|
+
@event_serializer = EventSerializer.new(global_tags: global_tags)
|
13
|
+
end
|
14
|
+
|
15
|
+
# using *args would make new allocations
|
16
|
+
def to_stat(name, delta, type, tags: [], sample_rate: 1)
|
17
|
+
stat_serializer.format(name, delta, type, tags: tags, sample_rate: sample_rate)
|
18
|
+
end
|
19
|
+
|
20
|
+
# using *args would make new allocations
|
21
|
+
def to_service_check(name, status, options = EMPTY_OPTIONS)
|
22
|
+
service_check_serializer.format(name, status, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
# using *args would make new allocations
|
26
|
+
def to_event(title, text, options = EMPTY_OPTIONS)
|
27
|
+
event_serializer.format(title, text, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def global_tags
|
31
|
+
stat_serializer.global_tags
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
attr_reader :stat_serializer
|
36
|
+
attr_reader :service_check_serializer
|
37
|
+
attr_reader :event_serializer
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
class ServiceCheckSerializer
|
7
|
+
SERVICE_CHECK_BASIC_OPTIONS = {
|
8
|
+
timestamp: 'd:',
|
9
|
+
hostname: 'h:',
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def initialize(global_tags: [])
|
13
|
+
@tag_serializer = TagSerializer.new(global_tags)
|
14
|
+
end
|
15
|
+
|
16
|
+
def format(name, status, options = EMPTY_OPTIONS)
|
17
|
+
String.new.tap do |service_check|
|
18
|
+
# line basics
|
19
|
+
service_check << "_sc"
|
20
|
+
service_check << "|"
|
21
|
+
service_check << name.to_s
|
22
|
+
service_check << "|"
|
23
|
+
service_check << status.to_s
|
24
|
+
|
25
|
+
# we are serializing the generic service check options
|
26
|
+
# before serializing specialized options that need edge-cases
|
27
|
+
SERVICE_CHECK_BASIC_OPTIONS.each do |option_key, shortcut|
|
28
|
+
if value = options[option_key]
|
29
|
+
service_check << '|'
|
30
|
+
service_check << shortcut
|
31
|
+
service_check << value.to_s.delete('|')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if message = options[:message]
|
36
|
+
service_check << '|m:'
|
37
|
+
service_check << escape_message(message)
|
38
|
+
end
|
39
|
+
|
40
|
+
if raw_tags = options[:tags]
|
41
|
+
if tags = tag_serializer.format(raw_tags)
|
42
|
+
service_check << '|#'
|
43
|
+
service_check << tags
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
attr_reader :tag_serializer
|
51
|
+
|
52
|
+
def escape_message(message)
|
53
|
+
message.delete('|').tap do |m|
|
54
|
+
m.gsub!("\n", '\n')
|
55
|
+
m.gsub!('m:', 'm\:')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
class StatSerializer
|
7
|
+
def initialize(prefix, global_tags: [])
|
8
|
+
@prefix = prefix
|
9
|
+
@tag_serializer = TagSerializer.new(global_tags)
|
10
|
+
end
|
11
|
+
|
12
|
+
def format(name, delta, type, tags: [], sample_rate: 1)
|
13
|
+
String.new.tap do |stat|
|
14
|
+
stat << prefix if prefix
|
15
|
+
|
16
|
+
# stat value
|
17
|
+
stat << formated_name(name)
|
18
|
+
stat << ':'
|
19
|
+
stat << delta.to_s
|
20
|
+
|
21
|
+
# stat type
|
22
|
+
stat << '|'
|
23
|
+
stat << type
|
24
|
+
|
25
|
+
# sample_rate
|
26
|
+
if sample_rate != 1
|
27
|
+
stat << '|'
|
28
|
+
stat << '@'
|
29
|
+
stat << sample_rate.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
# tags
|
33
|
+
if tags_list = tag_serializer.format(tags)
|
34
|
+
stat << '|'
|
35
|
+
stat << '#'
|
36
|
+
stat << tags_list
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def global_tags
|
42
|
+
tag_serializer.global_tags
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
attr_reader :prefix
|
47
|
+
attr_reader :tag_serializer
|
48
|
+
|
49
|
+
def formated_name(name)
|
50
|
+
formated = name.is_a?(String) ? name.dup : name.to_s
|
51
|
+
|
52
|
+
formated.tap do |f|
|
53
|
+
# replace Ruby module scoping with '.'
|
54
|
+
f.gsub!('::', '.')
|
55
|
+
# replace reserved chars (: | @) with underscores.
|
56
|
+
f.tr!(':|@', '_')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
class TagSerializer
|
7
|
+
def initialize(global_tags = [], env = ENV)
|
8
|
+
# Convert to hash
|
9
|
+
global_tags = to_tags_hash(global_tags)
|
10
|
+
|
11
|
+
# Merge with default tags
|
12
|
+
global_tags = default_tags(env).merge(global_tags)
|
13
|
+
|
14
|
+
# Convert to tag list and set
|
15
|
+
@global_tags = to_tags_list(global_tags)
|
16
|
+
end
|
17
|
+
|
18
|
+
def format(message_tags)
|
19
|
+
# fast return global tags if there's no message_tags
|
20
|
+
# to avoid more allocations
|
21
|
+
tag_list = if message_tags && message_tags.any?
|
22
|
+
global_tags + to_tags_list(message_tags)
|
23
|
+
else
|
24
|
+
global_tags
|
25
|
+
end
|
26
|
+
|
27
|
+
tag_list.join(',') if tag_list.any?
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :global_tags
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def to_tags_hash(tags)
|
35
|
+
case tags
|
36
|
+
when Hash
|
37
|
+
tags.dup
|
38
|
+
when Array
|
39
|
+
Hash[
|
40
|
+
tags.map do |string|
|
41
|
+
tokens = string.split(':')
|
42
|
+
tokens << nil if tokens.length == 1
|
43
|
+
tokens.length == 2 ? tokens : nil
|
44
|
+
end.compact
|
45
|
+
]
|
46
|
+
else
|
47
|
+
{}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_tags_list(tags)
|
52
|
+
case tags
|
53
|
+
when Hash
|
54
|
+
tags.each_with_object([]) do |tag_pair, formatted_tags|
|
55
|
+
if tag_pair.last.nil?
|
56
|
+
formatted_tags << "#{tag_pair.first}"
|
57
|
+
else
|
58
|
+
formatted_tags << "#{tag_pair.first}:#{tag_pair.last}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
when Array
|
62
|
+
tags.dup
|
63
|
+
else
|
64
|
+
[]
|
65
|
+
end.map! do |tag|
|
66
|
+
escape_tag_content(tag)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def escape_tag_content(tag)
|
71
|
+
tag.to_s.delete('|,')
|
72
|
+
end
|
73
|
+
|
74
|
+
def dd_tags(env = ENV)
|
75
|
+
return {} unless dd_tags = env['DD_TAGS']
|
76
|
+
|
77
|
+
to_tags_hash(dd_tags.split(','))
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_tags(env = ENV)
|
81
|
+
dd_tags(env).tap do |tags|
|
82
|
+
tags['dd.internal.entity_id'] = env['DD_ENTITY_ID'] if env.key?('DD_ENTITY_ID')
|
83
|
+
tags['env'] = env['DD_ENV'] if env.key?('DD_ENV')
|
84
|
+
tags['service'] = env['DD_SERVICE'] if env.key?('DD_SERVICE')
|
85
|
+
tags['version'] = env['DD_VERSION'] if env.key?('DD_VERSION')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|