dogstatsd-ruby 4.0.0 → 5.3.3

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,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ # Sender is using a companion thread to flush and pack messages
6
+ # in a `MessageBuffer`.
7
+ # The communication with this thread is done using a `Queue`.
8
+ # If the thread is dead, it is starting a new one to avoid having a blocked
9
+ # Sender with no companion thread to communicate with (most of the time, having
10
+ # a dead companion thread means that a fork just happened and that we are
11
+ # running in the child process).
12
+ class Sender
13
+ CLOSEABLE_QUEUES = Queue.instance_methods.include?(:close)
14
+
15
+ def initialize(message_buffer, telemetry: nil, queue_size: UDP_DEFAULT_BUFFER_SIZE, logger: nil, flush_interval: nil, queue_class: Queue, thread_class: Thread)
16
+ @message_buffer = message_buffer
17
+ @telemetry = telemetry
18
+ @queue_size = queue_size
19
+ @logger = logger
20
+ @mx = Mutex.new
21
+ @queue_class = queue_class
22
+ @thread_class = thread_class
23
+ if flush_interval
24
+ @flush_timer = Datadog::Statsd::Timer.new(flush_interval) { flush(sync: true) }
25
+ end
26
+ end
27
+
28
+ def flush(sync: false)
29
+ # keep a copy around in case another thread is calling #stop while this method is running
30
+ current_message_queue = message_queue
31
+
32
+ # don't try to flush if there is no message_queue instantiated or
33
+ # no companion thread running
34
+ if !current_message_queue
35
+ @logger.debug { "Statsd: can't flush: no message queue ready" } if @logger
36
+ return
37
+ end
38
+ if !sender_thread.alive?
39
+ @logger.debug { "Statsd: can't flush: no sender_thread alive" } if @logger
40
+ return
41
+ end
42
+
43
+ current_message_queue.push(:flush)
44
+ rendez_vous if sync
45
+ end
46
+
47
+ def rendez_vous
48
+ # could happen if #start hasn't be called
49
+ return unless message_queue
50
+
51
+ # Initialize and get the thread's sync queue
52
+ queue = (@thread_class.current[:statsd_sync_queue] ||= @queue_class.new)
53
+ # tell sender-thread to notify us in the current
54
+ # thread's queue
55
+ message_queue.push(queue)
56
+ # wait for the sender thread to send a message
57
+ # once the flush is done
58
+ queue.pop
59
+ end
60
+
61
+ def add(message)
62
+ raise ArgumentError, 'Start sender first' unless message_queue
63
+
64
+ # if the thread does not exist, we assume we are running in a forked process,
65
+ # empty the message queue and message buffers (these messages belong to
66
+ # the parent process) and spawn a new companion thread.
67
+ if !sender_thread.alive?
68
+ @mx.synchronize {
69
+ # a call from another thread has already re-created
70
+ # the companion thread before this one acquired the lock
71
+ break if sender_thread.alive?
72
+ @logger.debug { "Statsd: companion thread is dead, re-creating one" } if @logger
73
+
74
+ message_queue.close if CLOSEABLE_QUEUES
75
+ @message_queue = nil
76
+ message_buffer.reset
77
+ start
78
+ @flush_timer.start if @flush_timer && @flush_timer.stop?
79
+ }
80
+ end
81
+
82
+ if message_queue.length <= @queue_size
83
+ message_queue << message
84
+ else
85
+ @telemetry.dropped_queue(packets: 1, bytes: message.bytesize) if @telemetry
86
+ end
87
+ end
88
+
89
+ def start
90
+ raise ArgumentError, 'Sender already started' if message_queue
91
+
92
+ # initialize a new message queue for the background thread
93
+ @message_queue = @queue_class.new
94
+ # start background thread
95
+ @sender_thread = @thread_class.new(&method(:send_loop))
96
+ @sender_thread.name = "Statsd Sender" unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
97
+ @flush_timer.start if @flush_timer
98
+ end
99
+
100
+ if CLOSEABLE_QUEUES
101
+ # when calling stop, make sure that no other threads is trying
102
+ # to close the sender nor trying to continue to `#add` more message
103
+ # into the sender.
104
+ def stop(join_worker: true)
105
+ message_queue = @message_queue
106
+ message_queue.close if message_queue
107
+
108
+ @flush_timer.stop if @flush_timer
109
+ sender_thread = @sender_thread
110
+ sender_thread.join if sender_thread && join_worker
111
+ end
112
+ else
113
+ # when calling stop, make sure that no other threads is trying
114
+ # to close the sender nor trying to continue to `#add` more message
115
+ # into the sender.
116
+ def stop(join_worker: true)
117
+ message_queue = @message_queue
118
+ message_queue << :close if message_queue
119
+
120
+ @flush_timer.stop if @flush_timer
121
+ sender_thread = @sender_thread
122
+ sender_thread.join if sender_thread && join_worker
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ attr_reader :message_buffer
129
+ attr_reader :message_queue
130
+ attr_reader :sender_thread
131
+
132
+ if CLOSEABLE_QUEUES
133
+ def send_loop
134
+ until (message = message_queue.pop).nil? && message_queue.closed?
135
+ # skip if message is nil, e.g. when message_queue
136
+ # is empty and closed
137
+ next unless message
138
+
139
+ case message
140
+ when :flush
141
+ message_buffer.flush
142
+ when @queue_class
143
+ message.push(:go_on)
144
+ else
145
+ message_buffer.add(message)
146
+ end
147
+ end
148
+
149
+ @message_queue = nil
150
+ @sender_thread = nil
151
+ end
152
+ else
153
+ def send_loop
154
+ loop do
155
+ message = message_queue.pop
156
+
157
+ next unless message
158
+
159
+ case message
160
+ when :close
161
+ break
162
+ when :flush
163
+ message_buffer.flush
164
+ when @queue_class
165
+ message.push(:go_on)
166
+ else
167
+ message_buffer.add(message)
168
+ end
169
+ end
170
+
171
+ @message_queue = nil
172
+ @sender_thread = nil
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,71 @@
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
+ # also returns the global tags from serializer
45
+ if tags = tag_serializer.format(options[:tags])
46
+ event << '|#'
47
+ event << tags
48
+ end
49
+
50
+ if event.bytesize > MAX_EVENT_SIZE
51
+ if options[:truncate_if_too_long]
52
+ event.slice!(MAX_EVENT_SIZE..event.length)
53
+ else
54
+ raise "Event #{title} payload is too big (more that 8KB), event discarded"
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ protected
61
+ attr_reader :tag_serializer
62
+
63
+ def escape(text)
64
+ text.delete('|').tap do |t|
65
+ t.gsub!("\n", '\n')
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ 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,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,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,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ # The SingleThreadSender is a sender synchronously buffering messages
6
+ # in a `MessageBuffer`.
7
+ # It is using current Process.PID to check it is the result of a recent fork
8
+ # and it is reseting the MessageBuffer if that's the case.
9
+ class SingleThreadSender
10
+ def initialize(message_buffer, logger: nil, flush_interval: nil)
11
+ @message_buffer = message_buffer
12
+ @logger = logger
13
+ @mx = Mutex.new
14
+ if flush_interval
15
+ @flush_timer = Datadog::Statsd::Timer.new(flush_interval) { flush }
16
+ end
17
+ # store the pid for which this sender has been created
18
+ update_fork_pid
19
+ end
20
+
21
+ def add(message)
22
+ @mx.synchronize {
23
+ # we have just forked, meaning we have messages in the buffer that we should
24
+ # not send, they belong to the parent process, let's clear the buffer.
25
+ if forked?
26
+ @message_buffer.reset
27
+ @flush_timer.start if @flush_timer && @flush_timer.stop?
28
+ update_fork_pid
29
+ end
30
+ @message_buffer.add(message)
31
+ }
32
+ end
33
+
34
+ def flush(*)
35
+ @mx.synchronize {
36
+ @message_buffer.flush()
37
+ }
38
+ end
39
+
40
+ def start()
41
+ @flush_timer.start if @flush_timer
42
+ end
43
+
44
+ def stop()
45
+ @flush_timer.stop if @flush_timer
46
+ end
47
+
48
+ # Compatibility with `Sender`
49
+ def rendez_vous()
50
+ end
51
+
52
+ private
53
+
54
+ # below are "fork management" methods to be able to clean the MessageBuffer
55
+ # if it detects that it is running in a unknown PID.
56
+
57
+ def forked?
58
+ Process.pid != @fork_pid
59
+ end
60
+
61
+ def update_fork_pid
62
+ @fork_pid = Process.pid
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,117 @@
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 :bytes_dropped_queue
13
+ attr_reader :bytes_dropped_writer
14
+ attr_reader :packets_sent
15
+ attr_reader :packets_dropped
16
+ attr_reader :packets_dropped_queue
17
+ attr_reader :packets_dropped_writer
18
+
19
+ # Rough estimation of maximum telemetry message size without tags
20
+ MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
21
+
22
+ def initialize(flush_interval, global_tags: [], transport_type: :udp)
23
+ @flush_interval = flush_interval
24
+ @global_tags = global_tags
25
+ @transport_type = transport_type
26
+ reset
27
+
28
+ # TODO: Karim: I don't know why but telemetry tags are serialized
29
+ # before global tags so by refactoring this, I am keeping the same behavior
30
+ @serialized_tags = Serialization::TagSerializer.new(
31
+ client: 'ruby',
32
+ client_version: VERSION,
33
+ client_transport: transport_type,
34
+ ).format(global_tags)
35
+ end
36
+
37
+ def would_fit_in?(max_buffer_payload_size)
38
+ MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS + serialized_tags.size < max_buffer_payload_size
39
+ end
40
+
41
+ def reset
42
+ @metrics = 0
43
+ @events = 0
44
+ @service_checks = 0
45
+ @bytes_sent = 0
46
+ @bytes_dropped = 0
47
+ @bytes_dropped_queue = 0
48
+ @bytes_dropped_writer = 0
49
+ @packets_sent = 0
50
+ @packets_dropped = 0
51
+ @packets_dropped_queue = 0
52
+ @packets_dropped_writer = 0
53
+ @next_flush_time = now_in_s + @flush_interval
54
+ end
55
+
56
+ def sent(metrics: 0, events: 0, service_checks: 0, bytes: 0, packets: 0)
57
+ @metrics += metrics
58
+ @events += events
59
+ @service_checks += service_checks
60
+
61
+ @bytes_sent += bytes
62
+ @packets_sent += packets
63
+ end
64
+
65
+ def dropped_queue(bytes: 0, packets: 0)
66
+ @bytes_dropped += bytes
67
+ @bytes_dropped_queue += bytes
68
+ @packets_dropped += packets
69
+ @packets_dropped_queue += packets
70
+ end
71
+
72
+ def dropped_writer(bytes: 0, packets: 0)
73
+ @bytes_dropped += bytes
74
+ @bytes_dropped_writer += bytes
75
+ @packets_dropped += packets
76
+ @packets_dropped_writer += packets
77
+ end
78
+
79
+ def should_flush?
80
+ @next_flush_time < now_in_s
81
+ end
82
+
83
+ def flush
84
+ [
85
+ sprintf(pattern, 'metrics', @metrics),
86
+ sprintf(pattern, 'events', @events),
87
+ sprintf(pattern, 'service_checks', @service_checks),
88
+ sprintf(pattern, 'bytes_sent', @bytes_sent),
89
+ sprintf(pattern, 'bytes_dropped', @bytes_dropped),
90
+ sprintf(pattern, 'bytes_dropped_queue', @bytes_dropped_queue),
91
+ sprintf(pattern, 'bytes_dropped_writer', @bytes_dropped_writer),
92
+ sprintf(pattern, 'packets_sent', @packets_sent),
93
+ sprintf(pattern, 'packets_dropped', @packets_dropped),
94
+ sprintf(pattern, 'packets_dropped_queue', @packets_dropped_queue),
95
+ sprintf(pattern, 'packets_dropped_writer', @packets_dropped_writer),
96
+ ]
97
+ end
98
+
99
+ private
100
+ attr_reader :serialized_tags
101
+
102
+ def pattern
103
+ @pattern ||= "datadog.dogstatsd.client.%s:%d|#{COUNTER_TYPE}|##{serialized_tags}"
104
+ end
105
+
106
+ if Kernel.const_defined?('Process') && Process.respond_to?(:clock_gettime)
107
+ def now_in_s
108
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
109
+ end
110
+ else
111
+ def now_in_s
112
+ Time.now.to_i
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end