microphite 0.5.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.
- data/.gitignore +13 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +164 -0
- data/Rakefile +1 -0
- data/lib/microphite/client/base.rb +225 -0
- data/lib/microphite/client/noop.rb +17 -0
- data/lib/microphite/client/private/prefixed.rb +49 -0
- data/lib/microphite/client/socket.rb +79 -0
- data/lib/microphite/client.rb +4 -0
- data/lib/microphite/exceptions.rb +4 -0
- data/lib/microphite/metric.rb +32 -0
- data/lib/microphite/version.rb +3 -0
- data/lib/microphite.rb +14 -0
- data/microphite.gemspec +23 -0
- data/spec/client/noop_spec.rb +13 -0
- data/spec/client/socket_spec.rb +181 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/client_shared_examples.rb +62 -0
- data/spec/support/single_serve_helper.rb +80 -0
- data/tasks/gem.rake +3 -0
- data/tasks/test.rake +9 -0
- metadata +113 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 BZ Technology Services, LLC
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
Microphite
|
2
|
+
==========
|
3
|
+
|
4
|
+
|
5
|
+
Overview
|
6
|
+
--------
|
7
|
+
|
8
|
+
Microphite is a tiny and fast, asynchronous graphite client. It can be called
|
9
|
+
many times per second with minimal overhead (approx 6-8 microseconds per
|
10
|
+
write/gather call on commodity hardware). It is synchronized internally and
|
11
|
+
can be shared across threads.
|
12
|
+
|
13
|
+
|
14
|
+
Usage
|
15
|
+
-----
|
16
|
+
|
17
|
+
Construct a standard socket client. See the 'Client Options' section below
|
18
|
+
for initializer options.
|
19
|
+
|
20
|
+
client = Microphite.client(
|
21
|
+
host: 'graphite.blah',
|
22
|
+
port: 2003,
|
23
|
+
transport: :udp,
|
24
|
+
prefix: 'some.prefix')
|
25
|
+
|
26
|
+
Construct a client with an error_handler. The client is fault tolerant, but
|
27
|
+
an error_handler is useful for logging failure events.
|
28
|
+
|
29
|
+
handler = Proc.new { |error| Rails.logger.error "Microphite error: #{error.message}" }
|
30
|
+
client = Microphite.client(host: '...', error_handler: handler)
|
31
|
+
|
32
|
+
Construct a no-op/dummy client. This is useful in development. You can leave client API
|
33
|
+
calls in-place and the dummy client will behave appropriately.
|
34
|
+
|
35
|
+
# Initializer options are accepted, but no data is written
|
36
|
+
client = Microphite.noop(host: 'blah', ...)
|
37
|
+
|
38
|
+
Send data points
|
39
|
+
|
40
|
+
client.write('some.key': 300, 'another.key': 25)
|
41
|
+
|
42
|
+
Accumulate counters (flushed every options[:flush_interval] seconds)
|
43
|
+
|
44
|
+
client.gather('some.counter': 22, 'another.counter': 10)
|
45
|
+
|
46
|
+
Time a code block (results are gathered to the specified key)
|
47
|
+
|
48
|
+
client.time('app.timings.important_stuff') do
|
49
|
+
important_stuff()
|
50
|
+
end
|
51
|
+
|
52
|
+
Prepend prefixes on-the-fly:
|
53
|
+
|
54
|
+
client.prefix('app.') do |app_ns|
|
55
|
+
# Key is prefixed with 'app.' automatically
|
56
|
+
app.write(key: 42)
|
57
|
+
|
58
|
+
# Nest as much as you'd like
|
59
|
+
app_ns.prefix('sub.') do |sub_ns|
|
60
|
+
# Keys prefixed by 'app.sub.'
|
61
|
+
sub_ns.gather(something: 5)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Close the client, waiting for data to flush
|
66
|
+
|
67
|
+
client.close
|
68
|
+
|
69
|
+
Alternatively, wait at most 1 second to flush
|
70
|
+
|
71
|
+
flushed = client.close(1)
|
72
|
+
|
73
|
+
|
74
|
+
Client Options
|
75
|
+
--------------
|
76
|
+
|
77
|
+
<table>
|
78
|
+
<tr>
|
79
|
+
<th>Key</th>
|
80
|
+
<th>Type</th>
|
81
|
+
<th>Description</th>
|
82
|
+
<th>Default</th>
|
83
|
+
</tr>
|
84
|
+
<tr>
|
85
|
+
<td><tt>:host</tt></td>
|
86
|
+
<td>String</td>
|
87
|
+
<td>Graphite server host (REQUIRED)</td>
|
88
|
+
<td><tt>nil</tt></td>
|
89
|
+
</tr>
|
90
|
+
<tr>
|
91
|
+
<td><tt>:port</tt></td>
|
92
|
+
<td>Integer</td>
|
93
|
+
<td>Graphite port</td>
|
94
|
+
<td><tt>2003</tt></td>
|
95
|
+
</tr>
|
96
|
+
<tr>
|
97
|
+
<td><tt>:transport</tt></td>
|
98
|
+
<td>Symbol</td>
|
99
|
+
<td>Graphite transport to use (:tcp or :udp)</td>
|
100
|
+
<td><tt>:udp</tt></td>
|
101
|
+
</tr>
|
102
|
+
<tr>
|
103
|
+
<td><tt>:prefix</tt></td>
|
104
|
+
<td>String/Symbol</td>
|
105
|
+
<td>Global prefix for all keys</td>
|
106
|
+
<td><tt>''</tt></td>
|
107
|
+
</tr>
|
108
|
+
<tr>
|
109
|
+
<td><tt>:flush_interval</tt></td>
|
110
|
+
<td>Numeric</td>
|
111
|
+
<td>How often to flush gathered data (in seconds)</td>
|
112
|
+
<td><tt>10.0</tt></td>
|
113
|
+
</tr>
|
114
|
+
<tr>
|
115
|
+
<td><tt>:limit</tt></td>
|
116
|
+
<td>Integer</td>
|
117
|
+
<td>Limit the write and gather stacks to this size</td>
|
118
|
+
<td><tt>1000000</tt></td>
|
119
|
+
</tr>
|
120
|
+
<tr>
|
121
|
+
<td><tt>:min_delay</tt></td>
|
122
|
+
<td>Numeric</td>
|
123
|
+
<td>Initial delay between retry attempts after failure (in seconds)</td>
|
124
|
+
<td><tt>2</tt></td>
|
125
|
+
</tr>
|
126
|
+
<tr>
|
127
|
+
<td><tt>:max_delay</tt></td>
|
128
|
+
<td>Numeric</td>
|
129
|
+
<td>Maximum delay between retry attempts after failure (in seconds)</td>
|
130
|
+
<td><tt>60</tt></td>
|
131
|
+
</tr>
|
132
|
+
<tr>
|
133
|
+
<td><tt>:error_handler</tt></td>
|
134
|
+
<td>Proc</td>
|
135
|
+
<td>Code block called on exception (takes a single exception param)</td>
|
136
|
+
<td><tt>nil</tt></td>
|
137
|
+
</tr>
|
138
|
+
</table>
|
139
|
+
|
140
|
+
|
141
|
+
License
|
142
|
+
-------
|
143
|
+
|
144
|
+
The MIT License (MIT)
|
145
|
+
|
146
|
+
Copyright (c) 2013 BZ Technology Services, LLC
|
147
|
+
|
148
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
149
|
+
of this software and associated documentation files (the "Software"), to deal
|
150
|
+
in the Software without restriction, including without limitation the rights
|
151
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
152
|
+
copies of the Software, and to permit persons to whom the Software is
|
153
|
+
furnished to do so, subject to the following conditions:
|
154
|
+
|
155
|
+
The above copyright notice and this permission notice shall be included in
|
156
|
+
all copies or substantial portions of the Software.
|
157
|
+
|
158
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
159
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
160
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
161
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
162
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
163
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
164
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Dir['tasks/**/*.rake'].each {|f| load f}
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
module Microphite
|
5
|
+
module Client
|
6
|
+
class Base
|
7
|
+
def initialize(options)
|
8
|
+
defaults = {
|
9
|
+
prefix: '', # Prefix every key with this
|
10
|
+
flush_interval: 10, # How often to flush gathered metrics
|
11
|
+
limit: 1000000, # Size limit for gather and write stacks
|
12
|
+
error_handler: nil # Callback block for exceptions (mainly for debug/dev)
|
13
|
+
}
|
14
|
+
params = defaults.merge(options)
|
15
|
+
|
16
|
+
# Read-only state
|
17
|
+
@prefix = params[:prefix]
|
18
|
+
@limit = params[:limit]
|
19
|
+
@flush_interval = params[:flush_interval]
|
20
|
+
@error_handler = params[:error_handler]
|
21
|
+
|
22
|
+
# Shared state
|
23
|
+
@status = :running
|
24
|
+
@gather_stack = []
|
25
|
+
@write_stack = []
|
26
|
+
|
27
|
+
# Worker state
|
28
|
+
@accumulating = {}
|
29
|
+
@next_flush = now + @flush_interval
|
30
|
+
|
31
|
+
# Synchronization primitives
|
32
|
+
@lock = Mutex.new
|
33
|
+
@worker_event = ConditionVariable.new # Signals worker to wake-up
|
34
|
+
@shutdown_event = ConditionVariable.new # Signals close() caller when shutdown is complete
|
35
|
+
|
36
|
+
# The worker thread does all of the data processing and socket writing
|
37
|
+
Thread.new do
|
38
|
+
wrap_errors do
|
39
|
+
startup
|
40
|
+
|
41
|
+
# If startup throws, then worker_loop will never be called (what we want)
|
42
|
+
worker_loop
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def write(metrics)
|
48
|
+
push(@write_stack, metrics.clone)
|
49
|
+
end
|
50
|
+
|
51
|
+
def gather(metrics)
|
52
|
+
push(@gather_stack, metrics.clone)
|
53
|
+
end
|
54
|
+
|
55
|
+
def prefix(prefix, &block)
|
56
|
+
prefixed = Private::Prefixed.new(self, prefix)
|
57
|
+
if block_given?
|
58
|
+
prefixed.instance_eval &block
|
59
|
+
end
|
60
|
+
prefixed
|
61
|
+
end
|
62
|
+
|
63
|
+
def time(key, &block)
|
64
|
+
if block_given?
|
65
|
+
before = now
|
66
|
+
result = instance_eval &block
|
67
|
+
after = now
|
68
|
+
elapsed = after - before
|
69
|
+
gather(key => elapsed)
|
70
|
+
result
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def close(timeout=nil)
|
75
|
+
@lock.synchronize do
|
76
|
+
case @status
|
77
|
+
when :running, :ending
|
78
|
+
@status = :ending
|
79
|
+
@worker_event.signal
|
80
|
+
@shutdown_event.wait(@lock, timeout)
|
81
|
+
return @status == :shutdown
|
82
|
+
when :shutdown
|
83
|
+
return true
|
84
|
+
else
|
85
|
+
error(AssertionError.new("Invalid status: #{@status}"))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
# Lifecycle hook for subclasses
|
93
|
+
def startup
|
94
|
+
end
|
95
|
+
|
96
|
+
# Lifecycle hook for subclasses
|
97
|
+
def shutdown
|
98
|
+
end
|
99
|
+
|
100
|
+
def write_metric(metric)
|
101
|
+
raise(AssertionError, 'write_metric must be implemented in subclasses')
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def worker_loop
|
107
|
+
loop do
|
108
|
+
wrap_errors do
|
109
|
+
writes = nil
|
110
|
+
gathers = nil
|
111
|
+
|
112
|
+
@lock.synchronize do
|
113
|
+
if @write_stack.empty? and @gather_stack.empty?
|
114
|
+
case @status
|
115
|
+
when :running
|
116
|
+
wait_time = @next_flush - now
|
117
|
+
if wait_time > 0
|
118
|
+
@worker_event.wait(@lock, wait_time)
|
119
|
+
end
|
120
|
+
|
121
|
+
when :ending, :shutdown
|
122
|
+
flush_accumulating
|
123
|
+
wrap_errors do
|
124
|
+
shutdown
|
125
|
+
end
|
126
|
+
@status = :shutdown
|
127
|
+
@shutdown_event.broadcast
|
128
|
+
Thread.exit
|
129
|
+
|
130
|
+
else
|
131
|
+
error(AssertionError.new("Invalid status: #{@status}"))
|
132
|
+
Thread.exit
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Swap existing stacks with new ones
|
137
|
+
writes, @write_stack = @write_stack, []
|
138
|
+
gathers, @gather_stack = @gather_stack, []
|
139
|
+
end
|
140
|
+
|
141
|
+
unwind(writes).each { |m| write_metric m }
|
142
|
+
unwind(gathers).each { |m| accumulate m }
|
143
|
+
flush_accumulating if should_flush?
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def push(stack, value)
|
149
|
+
timestamp = now
|
150
|
+
pushed = false
|
151
|
+
@lock.synchronize do
|
152
|
+
if stack.length <= @limit and @status == :running
|
153
|
+
stack << timestamp << value
|
154
|
+
pushed = true
|
155
|
+
@worker_event.signal
|
156
|
+
end
|
157
|
+
end
|
158
|
+
pushed
|
159
|
+
end
|
160
|
+
|
161
|
+
def unwind(stack)
|
162
|
+
metrics = []
|
163
|
+
until stack.empty?
|
164
|
+
value = stack.pop
|
165
|
+
timestamp = stack.pop
|
166
|
+
wrap_errors do
|
167
|
+
case value
|
168
|
+
when Hash
|
169
|
+
value.each_pair { |k, v| metrics << Metric.new(k, v, timestamp) }
|
170
|
+
when Array
|
171
|
+
metrics.each do |m|
|
172
|
+
if m.is_a? Metric
|
173
|
+
metrics << m
|
174
|
+
else
|
175
|
+
error(AssertionError.new("Unhandled metric type: #{value.class}"))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
when Metric
|
179
|
+
metrics << value
|
180
|
+
else
|
181
|
+
error(AssertionError.new("Unhandled metric type: #{value.class}"))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
metrics
|
186
|
+
end
|
187
|
+
|
188
|
+
def accumulate(metric)
|
189
|
+
if @accumulating.has_key? metric.key
|
190
|
+
@accumulating[metric.key] += metric.value
|
191
|
+
else
|
192
|
+
@accumulating[metric.key] = metric.value
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def flush_accumulating
|
197
|
+
@accumulating.each_pair { |k, v| write_metric(Metric.new(k, v, now)) }
|
198
|
+
@accumulating.clear
|
199
|
+
@next_flush = now + @flush_interval
|
200
|
+
end
|
201
|
+
|
202
|
+
def should_flush?
|
203
|
+
now > @next_flush
|
204
|
+
end
|
205
|
+
|
206
|
+
def now
|
207
|
+
Time.now.to_f
|
208
|
+
end
|
209
|
+
|
210
|
+
def error(error)
|
211
|
+
if @error_handler.is_a? Proc
|
212
|
+
@error_handler.call(error)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def wrap_errors(&block)
|
217
|
+
begin
|
218
|
+
block.call
|
219
|
+
rescue Exception => e
|
220
|
+
error(e)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
module Microphite
|
5
|
+
module Client
|
6
|
+
class Noop < Base
|
7
|
+
def initialize(options={})
|
8
|
+
super(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def write_metric(metric)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
module Microphite
|
5
|
+
module Client
|
6
|
+
module Private
|
7
|
+
class Prefixed < Client::Base
|
8
|
+
def initialize(client, prefix)
|
9
|
+
@client = client
|
10
|
+
@prefix = prefix
|
11
|
+
end
|
12
|
+
|
13
|
+
def write(metrics)
|
14
|
+
@client.write(mutate_hash(metrics))
|
15
|
+
end
|
16
|
+
|
17
|
+
def gather(metrics)
|
18
|
+
@client.gather(mutate_hash(metrics))
|
19
|
+
end
|
20
|
+
|
21
|
+
def time(key, &block)
|
22
|
+
@client.time("#{@prefix}#{key}") do
|
23
|
+
block.call
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def prefix(additional, &block)
|
28
|
+
@client.prefix("#{@prefix}#{additional}", &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def close(timeout=nil)
|
32
|
+
@client.close(timeout)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def mutate_hash(hash)
|
38
|
+
return unless hash.is_a? Hash
|
39
|
+
mutated = {}
|
40
|
+
hash.each_pair do |k, v|
|
41
|
+
next unless k.is_a? String or k.is_a? Symbol
|
42
|
+
mutated[@prefix + k.to_s] = v
|
43
|
+
end
|
44
|
+
mutated
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
require 'socket'
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
module Microphite
|
8
|
+
module Client
|
9
|
+
class Socket < Base
|
10
|
+
def initialize(options)
|
11
|
+
defaults = {
|
12
|
+
port: 2003, # Server port
|
13
|
+
transport: :udp, # Transport: tcp or udp
|
14
|
+
min_delay: 2, # Initial delay for write failures (seconds)
|
15
|
+
max_delay: 60, # Maximum delay for write failures (seconds)
|
16
|
+
}
|
17
|
+
params = defaults.merge(options)
|
18
|
+
|
19
|
+
# Read-only state
|
20
|
+
@host = params[:host]
|
21
|
+
@port = params[:port]
|
22
|
+
@transport = params[:transport]
|
23
|
+
@min_delay = params[:min_delay]
|
24
|
+
@max_delay = params[:max_delay]
|
25
|
+
|
26
|
+
# Worker State
|
27
|
+
@socket = nil
|
28
|
+
|
29
|
+
super(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def shutdown
|
35
|
+
@socket.close unless @socket.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def write_metric(metric)
|
41
|
+
sent = false
|
42
|
+
failure_delay = @min_delay
|
43
|
+
until sent
|
44
|
+
begin
|
45
|
+
new_socket if @socket.nil?
|
46
|
+
@socket.send(format_line(metric), 0)
|
47
|
+
sent = true
|
48
|
+
|
49
|
+
rescue Exception => e
|
50
|
+
error(e)
|
51
|
+
|
52
|
+
# TODO: More robust handling?
|
53
|
+
sleep failure_delay
|
54
|
+
new_socket
|
55
|
+
if failure_delay < @max_delay
|
56
|
+
failure_delay += 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def new_socket
|
63
|
+
case @transport
|
64
|
+
when :tcp
|
65
|
+
@socket = TCPSocket.new(@host, @port)
|
66
|
+
when :udp
|
67
|
+
@socket = UDPSocket.new
|
68
|
+
@socket.connect(@host, @port)
|
69
|
+
else
|
70
|
+
error(AssertionError.new("Invalid transport: #{@transport}"))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def format_line(metric)
|
75
|
+
"#{@prefix}#{metric.key} #{format('%f', metric.value)} #{metric.time.to_i}\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Microphite
|
2
|
+
class Metric
|
3
|
+
attr_reader :key, :value, :time
|
4
|
+
|
5
|
+
def initialize(key, value, time=nil)
|
6
|
+
raise(ArgumentError, 'key cannot be nil') if key.nil?
|
7
|
+
raise(ArgumentError, 'value cannot be nil') if value.nil?
|
8
|
+
|
9
|
+
@key = key.to_s
|
10
|
+
@value = to_f! value
|
11
|
+
if time.nil?
|
12
|
+
@time = Time.now.to_f
|
13
|
+
else
|
14
|
+
@time = to_f! time
|
15
|
+
end
|
16
|
+
|
17
|
+
raise(ArgumentError, "invalid key: #{key}") if @key.empty?
|
18
|
+
raise(ArgumentError, "invaild value: #{value} ") if @value.nil?
|
19
|
+
raise(ArgumentError, "invalue time: #{time}") if @time.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def to_f!(value)
|
25
|
+
begin
|
26
|
+
Float(value)
|
27
|
+
rescue ArgumentError
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/microphite.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative 'microphite/client'
|
2
|
+
require_relative 'microphite/exceptions'
|
3
|
+
require_relative 'microphite/metric'
|
4
|
+
require_relative 'microphite/version'
|
5
|
+
|
6
|
+
module Microphite
|
7
|
+
def self.client(options)
|
8
|
+
Client::Socket.new(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.noop(options=nil)
|
12
|
+
Client::Noop.new(options)
|
13
|
+
end
|
14
|
+
end
|
data/microphite.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
3
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
4
|
+
|
5
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
6
|
+
require 'microphite/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |gem|
|
9
|
+
gem.name = 'microphite'
|
10
|
+
gem.version = Microphite::VERSION
|
11
|
+
gem.authors = %w(Bob Ziuchkovski)
|
12
|
+
gem.email = %w(bob@bz-technology.com)
|
13
|
+
gem.summary = 'A blazing fast, thread-safe graphite client'
|
14
|
+
gem.description = 'A blazing fast, thread-safe graphite client'
|
15
|
+
gem.homepage = 'https://github.com/bz-technology/microphite'
|
16
|
+
|
17
|
+
gem.add_development_dependency 'rake'
|
18
|
+
gem.add_development_dependency 'rspec'
|
19
|
+
|
20
|
+
gem.files = `git ls-files`.split("\n")
|
21
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
22
|
+
gem.require_paths = %w(lib)
|
23
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
module Microphite
|
7
|
+
describe Client::Noop do
|
8
|
+
before_block = Proc.new { @client = Client::Noop.new }
|
9
|
+
after_block = Proc.new {}
|
10
|
+
|
11
|
+
it_should_behave_like 'a microphite client', before_block, after_block
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
module Microphite
|
7
|
+
shared_examples 'a microphite socket client' do |transport|
|
8
|
+
before_block = Proc.new do
|
9
|
+
@server = Helpers::SingleServe.new(transport)
|
10
|
+
client_options = { host: 'localhost', port: @server.port, transport: transport }
|
11
|
+
@client = Client::Socket.new(client_options)
|
12
|
+
end
|
13
|
+
after_block = Proc.new {}
|
14
|
+
|
15
|
+
before do
|
16
|
+
instance_eval &before_block
|
17
|
+
end
|
18
|
+
|
19
|
+
it_should_behave_like 'a microphite client', before_block, after_block
|
20
|
+
|
21
|
+
describe :write do
|
22
|
+
it 'should handle Symbol and String keys' do
|
23
|
+
@client.write(key1: 1)
|
24
|
+
@client.write(key2: 2, key3: 3)
|
25
|
+
@client.write('key4' => 4)
|
26
|
+
@client.write('key5' => 5, 'key6' => 6)
|
27
|
+
@client.write('key7' => 7, :key8 => 8)
|
28
|
+
@client.write(:key9 => 9, 'key10' => 10)
|
29
|
+
@client.close
|
30
|
+
|
31
|
+
lines = @server.bytes
|
32
|
+
(1..10).each do |n|
|
33
|
+
lines.should match(/^key#{n} #{n}\.0* \d+$/)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should handle Integer and Float values' do
|
38
|
+
@client.write(key1: 1)
|
39
|
+
@client.write(key2: 2.5)
|
40
|
+
@client.close
|
41
|
+
|
42
|
+
lines = @server.bytes
|
43
|
+
lines.should match(/^key1 1\.0* \d+$/)
|
44
|
+
lines.should match(/^key2 2\.50* \d+$/)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe :gather do
|
49
|
+
it 'should handle Symbol and String keys' do
|
50
|
+
@client.gather(key1: 1)
|
51
|
+
@client.gather(key2: 2, key3: 3)
|
52
|
+
@client.gather('key4' => 4)
|
53
|
+
@client.gather('key5' => 5, 'key6' => 6)
|
54
|
+
@client.gather('key7' => 7, :key8 => 8)
|
55
|
+
@client.gather(:key9 => 9, 'key10' => 10)
|
56
|
+
@client.close
|
57
|
+
|
58
|
+
lines = @server.bytes
|
59
|
+
(1..10).each do |n|
|
60
|
+
lines.should match(/^key#{n} #{n}\.0* \d+$/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should handle Integer and Float values' do
|
65
|
+
@client.gather(key1: 1)
|
66
|
+
@client.gather(key2: 2.5)
|
67
|
+
@client.close
|
68
|
+
|
69
|
+
lines = @server.bytes
|
70
|
+
lines.should match(/^key1 1\.0* \d+$/)
|
71
|
+
lines.should match(/^key2 2\.50* \d+$/)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should flush accumulated data' do
|
75
|
+
(1..10).each do
|
76
|
+
@client.gather(key1: 1)
|
77
|
+
@client.gather('key2' => 2)
|
78
|
+
@client.gather(:key3 => 3, 'key4' => 4)
|
79
|
+
end
|
80
|
+
@client.close
|
81
|
+
|
82
|
+
lines = @server.bytes
|
83
|
+
(1..4).each do |n|
|
84
|
+
lines.should match(/^key#{n} #{n * 10}\.0* \d+$/)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should reset accumulated data after flush_interval' do
|
89
|
+
rst_server = Helpers::SingleServe.new(transport)
|
90
|
+
rst_options = { host: 'localhost', port: rst_server.port, transport: transport, flush_interval: 0.05 }
|
91
|
+
rst_client = Microphite::Client::Socket.new(rst_options)
|
92
|
+
|
93
|
+
rst_client.gather(key: 42)
|
94
|
+
sleep 0.075
|
95
|
+
rst_client.gather(key: 21)
|
96
|
+
rst_client.close
|
97
|
+
|
98
|
+
lines = rst_server.bytes
|
99
|
+
lines.should match(/^key 42\.0* \d+$/)
|
100
|
+
lines.should match(/^key 21\.0* \d+$/)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe :time do
|
105
|
+
it 'should generate sensible values' do
|
106
|
+
before = Time.now.to_f
|
107
|
+
@client.time(:key) { 42 }
|
108
|
+
after = Time.now.to_f
|
109
|
+
outer_timing = after - before
|
110
|
+
|
111
|
+
@client.close
|
112
|
+
lines = @server.bytes
|
113
|
+
pattern = /^key (?<value>[0-9.]+) \d+$/
|
114
|
+
|
115
|
+
lines.should match(pattern)
|
116
|
+
value = pattern.match(lines)[:value]
|
117
|
+
value.should_not eq nil
|
118
|
+
expect { value > 0 and value < outer_timing }.to be_true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe :prefix do
|
123
|
+
it 'should prefix :write' do
|
124
|
+
prefixed = @client.prefix('test.')
|
125
|
+
prefixed.write(:key1 => 1, 'key2' => 2)
|
126
|
+
|
127
|
+
@client.close
|
128
|
+
lines = @server.bytes
|
129
|
+
lines.should match(/^test.key1 1\.0* \d+$/)
|
130
|
+
lines.should match(/^test.key2 2\.0* \d+$/)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'should prefix :gather' do
|
134
|
+
prefixed = @client.prefix('test.')
|
135
|
+
prefixed.gather(:key1 => 1, 'key2' => 2)
|
136
|
+
|
137
|
+
@client.close
|
138
|
+
lines = @server.bytes
|
139
|
+
lines.should match(/^test.key1 1\.0* \d+$/)
|
140
|
+
lines.should match(/^test.key2 2\.0* \d+$/)
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should prefix :time' do
|
144
|
+
prefixed = @client.prefix('test.')
|
145
|
+
prefixed.time(:key) { 42 }
|
146
|
+
|
147
|
+
@client.close
|
148
|
+
lines = @server.bytes
|
149
|
+
lines.should match(/^test.key [0-9.]+ \d+$/)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should prefix recursively' do
|
153
|
+
@client.prefix('p1.') do |p1|
|
154
|
+
p1.write(:key1 => 1)
|
155
|
+
p1.prefix('p2.') do |p2|
|
156
|
+
p2.gather(:key2 => 2)
|
157
|
+
p2.prefix('p3.') do |p3|
|
158
|
+
p3.time('key3') { 42 }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
@client.close
|
164
|
+
lines = @server.bytes
|
165
|
+
lines.should match(/^p1.key1 [0-9.]+ \d+$/)
|
166
|
+
lines.should match(/^p1.p2.key2 [0-9.]+ \d+$/)
|
167
|
+
lines.should match(/^p1.p2.p3.key3 [0-9.]+ \d+$/)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe Client::Socket do
|
173
|
+
context 'tcp' do
|
174
|
+
it_should_behave_like 'a microphite socket client', :tcp
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'udp' do
|
178
|
+
it_should_behave_like 'a microphite socket client', :udp
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
require 'microphite'
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
Dir['./spec/support/**/*.rb'].each { |file| require file }
|
8
|
+
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.include Helpers
|
12
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
|
4
|
+
PUBLIC_METHODS = [:write, :gather, :time, :prefix, :close]
|
5
|
+
|
6
|
+
shared_examples 'a microphite client' do |before, after|
|
7
|
+
before do
|
8
|
+
instance_eval &before
|
9
|
+
end
|
10
|
+
|
11
|
+
after do
|
12
|
+
instance_eval &after
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'interface' do
|
16
|
+
PUBLIC_METHODS.each do |method|
|
17
|
+
it 'should respond to ' + method.to_s do
|
18
|
+
@client.should respond_to method
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe :write do
|
24
|
+
it 'should tolerate valid input' do
|
25
|
+
expect { @client.write(key: 0) }.not_to raise_error
|
26
|
+
expect { @client.write(key1: 1, key2: 2.5) }.not_to raise_error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe :gather do
|
31
|
+
it 'should tolerate valid input' do
|
32
|
+
expect { @client.gather(key: 0) }.not_to raise_error
|
33
|
+
expect { @client.gather(key1: 1, key2: 2.5) }.not_to raise_error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe :time do
|
38
|
+
it 'should tolerate valid input' do
|
39
|
+
expect { @client.time(:key) { 42 } }.not_to raise_error
|
40
|
+
expect { @client.time('key') { 42 } }.not_to raise_error
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should return the evaluated block value' do
|
44
|
+
expect(@client.time(:key) { 42 }).to eq 42
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe :prefix do
|
49
|
+
it 'should return a client-like object' do
|
50
|
+
prefixed = @client.prefix 'test'
|
51
|
+
PUBLIC_METHODS.each do |method|
|
52
|
+
prefixed.should respond_to method
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe :close do
|
58
|
+
it 'should not blow up' do
|
59
|
+
expect { @client.close }.not_to raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Copyright (c) 2013 BZ Technology Services, LLC
|
2
|
+
# Released under the MIT License (http://opensource.org/licenses/MIT)
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Helpers
|
6
|
+
class SingleServe
|
7
|
+
# A super-simple RPC for stateless protocols (udp)
|
8
|
+
CLOSE_MAGIC = 'SingleServe.close()'
|
9
|
+
|
10
|
+
attr_reader :port
|
11
|
+
|
12
|
+
def initialize(transport)
|
13
|
+
@transport = transport.to_sym
|
14
|
+
@bytes = ''
|
15
|
+
|
16
|
+
case @transport
|
17
|
+
when :tcp
|
18
|
+
@socket = TCPServer.new('127.0.0.1', 0)
|
19
|
+
@port = @socket.addr[1]
|
20
|
+
@thread = Thread.new { tcp_loop }
|
21
|
+
when :udp
|
22
|
+
@socket = UDPSocket.new
|
23
|
+
@socket.bind('127.0.0.1', 0)
|
24
|
+
@port = @socket.addr[1]
|
25
|
+
@thread = Thread.new { udp_loop }
|
26
|
+
else
|
27
|
+
raise(ArgumentError, 'Only :tcp and :udp socket types supported')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# End the server, returning the data sent by the client
|
32
|
+
def bytes
|
33
|
+
if @thread.alive?
|
34
|
+
# UDP is stateless, so we have to indicate closure to the server
|
35
|
+
if @transport == :udp
|
36
|
+
closer = UDPSocket.new
|
37
|
+
closer.connect('127.0.0.1', @port)
|
38
|
+
closer.send CLOSE_MAGIC, 0
|
39
|
+
end
|
40
|
+
@thread.join
|
41
|
+
end
|
42
|
+
@bytes
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def tcp_loop
|
48
|
+
client_socket = @socket.accept
|
49
|
+
read_fully client_socket
|
50
|
+
@bytes
|
51
|
+
end
|
52
|
+
|
53
|
+
def udp_loop
|
54
|
+
read_fully @socket
|
55
|
+
@bytes
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_fully(socket)
|
59
|
+
loop do
|
60
|
+
readable = select([socket])[0]
|
61
|
+
if readable.empty?
|
62
|
+
return
|
63
|
+
else
|
64
|
+
begin
|
65
|
+
buffer = ''
|
66
|
+
socket.readpartial(4096, buffer)
|
67
|
+
@bytes << buffer
|
68
|
+
if @bytes.end_with? CLOSE_MAGIC
|
69
|
+
@bytes.chomp! CLOSE_MAGIC
|
70
|
+
return
|
71
|
+
end
|
72
|
+
rescue EOFError
|
73
|
+
@bytes << buffer
|
74
|
+
return
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/tasks/gem.rake
ADDED
data/tasks/test.rake
ADDED
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: microphite
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bob
|
9
|
+
- Ziuchkovski
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-11-20 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rspec
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description: A blazing fast, thread-safe graphite client
|
48
|
+
email:
|
49
|
+
- bob@bz-technology.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- .travis.yml
|
56
|
+
- CHANGELOG.md
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- lib/microphite.rb
|
62
|
+
- lib/microphite/client.rb
|
63
|
+
- lib/microphite/client/base.rb
|
64
|
+
- lib/microphite/client/noop.rb
|
65
|
+
- lib/microphite/client/private/prefixed.rb
|
66
|
+
- lib/microphite/client/socket.rb
|
67
|
+
- lib/microphite/exceptions.rb
|
68
|
+
- lib/microphite/metric.rb
|
69
|
+
- lib/microphite/version.rb
|
70
|
+
- microphite.gemspec
|
71
|
+
- spec/client/noop_spec.rb
|
72
|
+
- spec/client/socket_spec.rb
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
- spec/support/client_shared_examples.rb
|
75
|
+
- spec/support/single_serve_helper.rb
|
76
|
+
- tasks/gem.rake
|
77
|
+
- tasks/test.rake
|
78
|
+
homepage: https://github.com/bz-technology/microphite
|
79
|
+
licenses: []
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
hash: 800344433168954481
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
hash: 800344433168954481
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 1.8.23
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: A blazing fast, thread-safe graphite client
|
108
|
+
test_files:
|
109
|
+
- spec/client/noop_spec.rb
|
110
|
+
- spec/client/socket_spec.rb
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
- spec/support/client_shared_examples.rb
|
113
|
+
- spec/support/single_serve_helper.rb
|