dogstatsd-ruby 4.0.0 → 5.2.0
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/README.md +176 -56
- data/lib/datadog/statsd.rb +169 -333
- data/lib/datadog/statsd/connection.rb +59 -0
- data/lib/datadog/statsd/forwarder.rb +122 -0
- data/lib/datadog/statsd/message_buffer.rb +88 -0
- data/lib/datadog/statsd/sender.rb +117 -0
- data/lib/datadog/statsd/serialization.rb +15 -0
- data/lib/datadog/statsd/serialization/event_serializer.rb +71 -0
- data/lib/datadog/statsd/serialization/serializer.rb +41 -0
- data/lib/datadog/statsd/serialization/service_check_serializer.rb +60 -0
- data/lib/datadog/statsd/serialization/stat_serializer.rb +55 -0
- data/lib/datadog/statsd/serialization/tag_serializer.rb +96 -0
- data/lib/datadog/statsd/single_thread_sender.rb +31 -0
- data/lib/datadog/statsd/telemetry.rb +96 -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 +30 -10
@@ -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,60 @@
|
|
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
|
+
# also returns the global tags from serializer
|
41
|
+
if tags = tag_serializer.format(options[:tags])
|
42
|
+
service_check << '|#'
|
43
|
+
service_check << tags
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
attr_reader :tag_serializer
|
50
|
+
|
51
|
+
def escape_message(message)
|
52
|
+
message.delete('|').tap do |m|
|
53
|
+
m.gsub!("\n", '\n')
|
54
|
+
m.gsub!('m:', 'm\:')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,55 @@
|
|
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
|
+
@prefix_str = prefix.to_s
|
10
|
+
@tag_serializer = TagSerializer.new(global_tags)
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(name, delta, type, tags: [], sample_rate: 1)
|
14
|
+
name = formated_name(name)
|
15
|
+
|
16
|
+
if sample_rate != 1
|
17
|
+
if tags_list = tag_serializer.format(tags)
|
18
|
+
"#{@prefix_str}#{name}:#{delta}|#{type}|@#{sample_rate}|##{tags_list}"
|
19
|
+
else
|
20
|
+
"#{@prefix_str}#{name}:#{delta}|#{type}|@#{sample_rate}"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
if tags_list = tag_serializer.format(tags)
|
24
|
+
"#{@prefix_str}#{name}:#{delta}|#{type}|##{tags_list}"
|
25
|
+
else
|
26
|
+
"#{@prefix_str}#{name}:#{delta}|#{type}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def global_tags
|
32
|
+
tag_serializer.global_tags
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :prefix
|
38
|
+
attr_reader :tag_serializer
|
39
|
+
|
40
|
+
def formated_name(name)
|
41
|
+
if name.is_a?(String)
|
42
|
+
# DEV: gsub is faster than dup.gsub!
|
43
|
+
formated = name.gsub('::', '.')
|
44
|
+
else
|
45
|
+
formated = name.to_s
|
46
|
+
formated.gsub!('::', '.')
|
47
|
+
end
|
48
|
+
|
49
|
+
formated.tr!(':|@', '_')
|
50
|
+
formated
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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
|
+
if @global_tags.any?
|
17
|
+
@global_tags_formatted = @global_tags.join(',')
|
18
|
+
else
|
19
|
+
@global_tags_formatted = nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def format(message_tags)
|
24
|
+
if !message_tags || message_tags.empty?
|
25
|
+
return @global_tags_formatted
|
26
|
+
end
|
27
|
+
|
28
|
+
tags = if @global_tags_formatted
|
29
|
+
[@global_tags_formatted, to_tags_list(message_tags)]
|
30
|
+
else
|
31
|
+
to_tags_list(message_tags)
|
32
|
+
end
|
33
|
+
|
34
|
+
tags.join(',')
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :global_tags
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def to_tags_hash(tags)
|
42
|
+
case tags
|
43
|
+
when Hash
|
44
|
+
tags.dup
|
45
|
+
when Array
|
46
|
+
Hash[
|
47
|
+
tags.map do |string|
|
48
|
+
tokens = string.split(':')
|
49
|
+
tokens << nil if tokens.length == 1
|
50
|
+
tokens.length == 2 ? tokens : nil
|
51
|
+
end.compact
|
52
|
+
]
|
53
|
+
else
|
54
|
+
{}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_tags_list(tags)
|
59
|
+
case tags
|
60
|
+
when Hash
|
61
|
+
tags.map do |name, value|
|
62
|
+
if value
|
63
|
+
escape_tag_content("#{name}:#{value}")
|
64
|
+
else
|
65
|
+
escape_tag_content(name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
when Array
|
69
|
+
tags.map { |tag| escape_tag_content(tag) }
|
70
|
+
else
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def escape_tag_content(tag)
|
76
|
+
tag.to_s.delete('|,')
|
77
|
+
end
|
78
|
+
|
79
|
+
def dd_tags(env = ENV)
|
80
|
+
return {} unless dd_tags = env['DD_TAGS']
|
81
|
+
|
82
|
+
to_tags_hash(dd_tags.split(','))
|
83
|
+
end
|
84
|
+
|
85
|
+
def default_tags(env = ENV)
|
86
|
+
dd_tags(env).tap do |tags|
|
87
|
+
tags['dd.internal.entity_id'] = env['DD_ENTITY_ID'] if env.key?('DD_ENTITY_ID')
|
88
|
+
tags['env'] = env['DD_ENV'] if env.key?('DD_ENV')
|
89
|
+
tags['service'] = env['DD_SERVICE'] if env.key?('DD_SERVICE')
|
90
|
+
tags['version'] = env['DD_VERSION'] if env.key?('DD_VERSION')
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class SingleThreadSender
|
6
|
+
def initialize(message_buffer)
|
7
|
+
@message_buffer = message_buffer
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(message)
|
11
|
+
@message_buffer.add(message)
|
12
|
+
end
|
13
|
+
|
14
|
+
def flush(*)
|
15
|
+
@message_buffer.flush()
|
16
|
+
end
|
17
|
+
|
18
|
+
# Compatibility with `Sender`
|
19
|
+
def start()
|
20
|
+
end
|
21
|
+
|
22
|
+
# Compatibility with `Sender`
|
23
|
+
def stop()
|
24
|
+
end
|
25
|
+
|
26
|
+
# Compatibility with `Sender`
|
27
|
+
def rendez_vous()
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Datadog
|
5
|
+
class Statsd
|
6
|
+
class Telemetry
|
7
|
+
attr_reader :metrics
|
8
|
+
attr_reader :events
|
9
|
+
attr_reader :service_checks
|
10
|
+
attr_reader :bytes_sent
|
11
|
+
attr_reader :bytes_dropped
|
12
|
+
attr_reader :packets_sent
|
13
|
+
attr_reader :packets_dropped
|
14
|
+
|
15
|
+
# Rough estimation of maximum telemetry message size without tags
|
16
|
+
MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
|
17
|
+
|
18
|
+
def initialize(flush_interval, global_tags: [], transport_type: :udp)
|
19
|
+
@flush_interval = flush_interval
|
20
|
+
@global_tags = global_tags
|
21
|
+
@transport_type = transport_type
|
22
|
+
reset
|
23
|
+
|
24
|
+
# TODO: Karim: I don't know why but telemetry tags are serialized
|
25
|
+
# before global tags so by refactoring this, I am keeping the same behavior
|
26
|
+
@serialized_tags = Serialization::TagSerializer.new(
|
27
|
+
client: 'ruby',
|
28
|
+
client_version: VERSION,
|
29
|
+
client_transport: transport_type,
|
30
|
+
).format(global_tags)
|
31
|
+
end
|
32
|
+
|
33
|
+
def would_fit_in?(max_buffer_payload_size)
|
34
|
+
MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS + serialized_tags.size < max_buffer_payload_size
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset
|
38
|
+
@metrics = 0
|
39
|
+
@events = 0
|
40
|
+
@service_checks = 0
|
41
|
+
@bytes_sent = 0
|
42
|
+
@bytes_dropped = 0
|
43
|
+
@packets_sent = 0
|
44
|
+
@packets_dropped = 0
|
45
|
+
@next_flush_time = now_in_s + @flush_interval
|
46
|
+
end
|
47
|
+
|
48
|
+
def sent(metrics: 0, events: 0, service_checks: 0, bytes: 0, packets: 0)
|
49
|
+
@metrics += metrics
|
50
|
+
@events += events
|
51
|
+
@service_checks += service_checks
|
52
|
+
|
53
|
+
@bytes_sent += bytes
|
54
|
+
@packets_sent += packets
|
55
|
+
end
|
56
|
+
|
57
|
+
def dropped(bytes: 0, packets: 0)
|
58
|
+
@bytes_dropped += bytes
|
59
|
+
@packets_dropped += packets
|
60
|
+
end
|
61
|
+
|
62
|
+
def should_flush?
|
63
|
+
@next_flush_time < now_in_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def flush
|
67
|
+
[
|
68
|
+
sprintf(pattern, 'metrics', @metrics),
|
69
|
+
sprintf(pattern, 'events', @events),
|
70
|
+
sprintf(pattern, 'service_checks', @service_checks),
|
71
|
+
sprintf(pattern, 'bytes_sent', @bytes_sent),
|
72
|
+
sprintf(pattern, 'bytes_dropped', @bytes_dropped),
|
73
|
+
sprintf(pattern, 'packets_sent', @packets_sent),
|
74
|
+
sprintf(pattern, 'packets_dropped', @packets_dropped),
|
75
|
+
]
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
attr_reader :serialized_tags
|
80
|
+
|
81
|
+
def pattern
|
82
|
+
@pattern ||= "datadog.dogstatsd.client.%s:%d|#{COUNTER_TYPE}|##{serialized_tags}"
|
83
|
+
end
|
84
|
+
|
85
|
+
if Kernel.const_defined?('Process') && Process.respond_to?(:clock_gettime)
|
86
|
+
def now_in_s
|
87
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
def now_in_s
|
91
|
+
Time.now.to_i
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'connection'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
class Statsd
|
7
|
+
class UDPConnection < Connection
|
8
|
+
DEFAULT_HOST = '127.0.0.1'
|
9
|
+
DEFAULT_PORT = 8125
|
10
|
+
|
11
|
+
# StatsD host. Defaults to 127.0.0.1.
|
12
|
+
attr_reader :host
|
13
|
+
|
14
|
+
# StatsD port. Defaults to 8125.
|
15
|
+
attr_reader :port
|
16
|
+
|
17
|
+
def initialize(host, port, **kwargs)
|
18
|
+
super(**kwargs)
|
19
|
+
|
20
|
+
@host = host || ENV.fetch('DD_AGENT_HOST', DEFAULT_HOST)
|
21
|
+
@port = port || ENV.fetch('DD_DOGSTATSD_PORT', DEFAULT_PORT).to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def connect
|
27
|
+
UDPSocket.new.tap do |socket|
|
28
|
+
socket.connect(host, port)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def send_message(message)
|
33
|
+
socket.send(message, 0)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'connection'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
class Statsd
|
7
|
+
class UDSConnection < Connection
|
8
|
+
class BadSocketError < StandardError; end
|
9
|
+
|
10
|
+
# DogStatsd unix socket path
|
11
|
+
attr_reader :socket_path
|
12
|
+
|
13
|
+
def initialize(socket_path, **kwargs)
|
14
|
+
super(**kwargs)
|
15
|
+
|
16
|
+
@socket_path = socket_path
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def connect
|
22
|
+
socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
|
23
|
+
socket.connect(Socket.pack_sockaddr_un(@socket_path))
|
24
|
+
socket
|
25
|
+
end
|
26
|
+
|
27
|
+
def send_message(message)
|
28
|
+
socket.sendmsg_nonblock(message)
|
29
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENOENT => e
|
30
|
+
@socket = nil
|
31
|
+
# TODO: FIXME: This error should be considered as a retryable error in the
|
32
|
+
# Connection class. An even better solution would be to make BadSocketError inherit
|
33
|
+
# from a specific retryable error class in the Connection class.
|
34
|
+
raise BadSocketError, "#{e.class}: #{e}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|