Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit 01f05c7

Browse files
author
Vicent Marti
committed
Merge pull request #18 from github/vmg/buffered
Buffered multi-metric packets
2 parents 1ddb9a4 + 6cc02da commit 01f05c7

File tree

1 file changed

+89
-31
lines changed

1 file changed

+89
-31
lines changed

lib/statsd.rb

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,61 @@
1414
# statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
1515
# statsd.increment 'activate'
1616
class Statsd
17-
class RubyUdpClient
18-
attr_reader :key, :sock
17+
class UDPClient
18+
attr_reader :sock
1919

20-
def initialize(address, port, key = nil)
20+
def initialize(address, port = nil)
21+
address, port = address.split(':') if address.include?(':')
2122
addrinfo = Addrinfo.ip(address)
23+
2224
@sock = UDPSocket.new(addrinfo.pfamily)
2325
@sock.connect(addrinfo.ip_address, port)
24-
@key = key
2526
end
2627

2728
def send(msg)
2829
sock.write(msg)
29-
rescue => boom
30+
rescue SystemCallError
3031
nil
3132
end
3233
end
3334

35+
class SecureUDPClient < UDPClient
36+
def initialize(address, port, key)
37+
super(address, port)
38+
@key = key
39+
end
40+
41+
def send(msg)
42+
super(signed_payload(msg))
43+
end
44+
45+
private
46+
# defer loading openssl and securerandom unless needed. this shaves ~10ms off
47+
# of baseline require load time for environments that don't require message signing.
48+
def self.setup_openssl
49+
@sha256 ||= begin
50+
require 'securerandom'
51+
require 'openssl'
52+
OpenSSL::Digest::SHA256.new
53+
end
54+
end
55+
56+
def signed_payload(message)
57+
sha256 = SecureUDPClient.setup_openssl
58+
payload = timestamp + nonce + message
59+
signature = OpenSSL::HMAC.digest(sha256, @key, payload)
60+
signature + payload
61+
end
62+
63+
def timestamp
64+
[Time.now.to_i].pack("Q<")
65+
end
66+
67+
def nonce
68+
SecureRandom.random_bytes(4)
69+
end
70+
end
71+
3472
# A namespace to prepend to all statsd calls.
3573
attr_reader :namespace
3674

@@ -52,20 +90,38 @@ def namespace=(namespace)
5290

5391
def initialize(client_class = nil)
5492
@shards = []
55-
@client_class = client_class || RubyUdpClient
93+
@client_class = client_class || UDPClient
5694
self.namespace = nil
5795
end
5896

5997
def self.simple(addr, port = nil)
6098
self.new.add_shard(addr, port)
6199
end
62100

63-
def add_shard(addr, port = nil, key = nil)
64-
addr, port = addr.split(':') if addr.include?(':')
65-
@shards << @client_class.new(addr, port.to_i, key)
101+
def add_shard(*args)
102+
@shards << @client_class.new(*args)
66103
self
67104
end
68105

106+
def enable_buffering(buffer_size = nil)
107+
return if @buffering
108+
@shards.map! { |client| Buffer.new(client, buffer_size) }
109+
@buffering = true
110+
end
111+
112+
def disable_buffering
113+
return unless @buffering
114+
flush_all
115+
@shards.map! { |client| client.base_client }
116+
@buffering = false
117+
end
118+
119+
def flush_all
120+
return unless @buffering
121+
@shards.each { |client| client.flush }
122+
end
123+
124+
69125
# Sends an increment (count = 1) for the given stat to the statsd server.
70126
#
71127
# @param stat (see #count)
@@ -129,7 +185,6 @@ def time(stat, sample_rate=1)
129185
def histogram(stat, value, sample_rate=1); send stat, value, HISTOGRAM_TYPE, sample_rate end
130186

131187
private
132-
133188
def sampled(sample_rate)
134189
yield unless sample_rate < 1 and rand > sample_rate
135190
end
@@ -140,7 +195,7 @@ def send(stat, delta, type, sample_rate=1)
140195
stat.gsub!(/::/, ".".freeze)
141196
stat.gsub!(RESERVED_CHARS_REGEX, "_".freeze)
142197

143-
msg = ""
198+
msg = String.new
144199
msg << @prefix
145200
msg << stat
146201
msg << ":".freeze
@@ -153,7 +208,7 @@ def send(stat, delta, type, sample_rate=1)
153208
end
154209

155210
shard = select_shard(stat)
156-
shard.send(shard.key ? signed_payload(shard.key, msg) : msg)
211+
shard.send(msg)
157212
end
158213
end
159214

@@ -165,28 +220,31 @@ def select_shard(stat)
165220
end
166221
end
167222

168-
def signed_payload(key, message)
169-
sha256 = Statsd.setup_openssl
170-
payload = timestamp + nonce + message
171-
signature = OpenSSL::HMAC.digest(sha256, key, payload)
172-
signature + payload
173-
end
223+
class Buffer
224+
DEFAULT_BUFFER_CAP = 512
225+
226+
attr_reader :base_client
227+
attr_accessor :flush_count
174228

175-
# defer loading openssl and securerandom unless needed. this shaves ~10ms off
176-
# of baseline require load time for environments that don't require message signing.
177-
def self.setup_openssl
178-
@sha256 ||= begin
179-
require 'securerandom'
180-
require 'openssl'
181-
OpenSSL::Digest::SHA256.new
229+
def initialize(client, buffer_cap = nil)
230+
@base_client = client
231+
@buffer = String.new
232+
@buffer_cap = buffer_cap || DEFAULT_BUFFER_CAP
233+
@flush_count = 0
182234
end
183-
end
184235

185-
def timestamp
186-
[Time.now.to_i].pack("Q<")
187-
end
236+
def flush
237+
return unless @buffer.bytesize > 0
238+
@base_client.send(@buffer)
239+
@buffer.clear
240+
@flush_count += 1
241+
end
188242

189-
def nonce
190-
SecureRandom.random_bytes(4)
243+
def send(msg)
244+
flush if @buffer.bytesize + msg.bytesize >= @buffer_cap
245+
@buffer << msg
246+
@buffer << "\n".freeze
247+
nil
248+
end
191249
end
192250
end

0 commit comments

Comments
 (0)