afstatsd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,69 @@
1
+ require 'afstatsd'
2
+
3
+ #$statsd = Statsd.new 'statsd_server.my_company.com', 8125, 20
4
+
5
+ $statsd = Statsd.new # use defaults
6
+ $statsd.namespace = 'test.ruby'
7
+
8
+
9
+ $statsd.increment 'counter1'
10
+ $statsd.increment 'counter1'
11
+ $statsd.decrement 'counter1' #counters accumulate
12
+
13
+ $statsd.gauge 'gauge1', 1024
14
+ $statsd.gauge 'gauge1', 1025
15
+ $statsd.gauge 'gauge1', 1026
16
+ $statsd.gauge 'gauge1', 1027
17
+ $statsd.gauge 'gauge1', 1028 # gauges get averaged when aggregated
18
+
19
+ $statsd.time('timing1' ){sleep 0.01}
20
+ $statsd.time('timing1' ){sleep 0.02}
21
+ $statsd.time('timing1' ){sleep 0.03}
22
+ $statsd.time('timing1' ){sleep 0.04} # timings get averaged when aggregated
23
+
24
+
25
+ =begin
26
+
27
+ 100.times do
28
+ #$statsd.increment 'sampled', 0.1, 'sampled'
29
+ $statsd.increment 'sampled'
30
+ end
31
+
32
+ $statsd.set 'set1', 1099, "ez"
33
+
34
+ for i in 10..19 do
35
+ $statsd.increment "counter#{i}" # create a group of counters
36
+ end
37
+
38
+ 1000.times do
39
+ $statsd.increment 'fast' # don't do this if aggregation is off
40
+ end
41
+
42
+ # In this test program, this will give the aggregator time to run.
43
+ 15.times do
44
+ sleep 2
45
+ $statsd.increment 'slow'
46
+ end
47
+
48
+ =end
49
+
50
+ =begin
51
+ # test for thread safety
52
+ threads = []
53
+ start = Time.now
54
+ for i in 0..9 do
55
+ threads << Thread.new(i) do |j|
56
+ start = Time.now
57
+ 1000000.times do
58
+ $statsd.increment 'inthethread'
59
+ # sleep(0.01)
60
+ end
61
+ puts "thread #{j} says: I took #{((Time.now - start)*1000).round} ms"
62
+ end
63
+ end
64
+ threads.each { |t| t.join }
65
+
66
+ puts "total time: #{((Time.now - start)*1000).round} ms"
67
+ =end
68
+
69
+ puts "#{$statsd.dropped} messages dropped"
@@ -0,0 +1,132 @@
1
+ # Statsd Aggregator
2
+ #
3
+ # Used to aggregate metrics in a threaded environment. Only one of these
4
+ # should be created, in the main thread.
5
+ # For each thread, we create 2 buffers. The thread will be writing to
6
+ # one, while the aggregator reads from the other. The aggregator will
7
+ # control which set is which.
8
+
9
+
10
+ class StatsdAggregator
11
+ attr_accessor :transport
12
+
13
+ def initialize(interval=20)
14
+ @interval = interval
15
+ @timer = nil
16
+ @mutex = Mutex.new
17
+ @running = false
18
+ @left_buffers = {} # 2 buffer groups
19
+ @right_buffers = {} # each buffer group is a hash
20
+ @rbufs = @left_buffers # buffer group currently being read from
21
+ @wbufs = @right_buffers # buffer group currently being written to
22
+ at_exit do
23
+ if @running
24
+ flush_buffers
25
+ swap_buffers
26
+ flush_buffers
27
+ end
28
+ end
29
+ end
30
+
31
+ def start(transport)
32
+ @transport = transport
33
+ return if @running # already started
34
+ # Spin up a thread to periodically send the aggregated stats.
35
+ # Divide the interval in half to allow other threads to finish
36
+ # their writes after we swap, and before we start reading.
37
+ @timer = Thread.new do
38
+ loop do
39
+ sleep @interval/2
40
+ swap_buffers
41
+ sleep @interval/2
42
+ flush_buffers
43
+ end
44
+ end
45
+ @running = true
46
+ #puts "aggregation started. Interval=#{@interval}"
47
+ end
48
+
49
+ def stop
50
+ return if not @running # already stopped
51
+ flush_buffers
52
+ @timer.kill if @timer
53
+ @timer = nil
54
+ @running = false
55
+ #puts "aggregation stopped"
56
+ end
57
+
58
+ def set_interval(interval)
59
+ @interval = interval
60
+ end
61
+
62
+ # the following methods are thread safe
63
+
64
+ def running
65
+ @running
66
+ end
67
+
68
+ # this is the only method that should be used by child threads.
69
+ def add(metric)
70
+ # We should have a write buffer assigned to our thread.
71
+ # Create one if not.
72
+ unless write_buffer = @wbufs[Thread.current]
73
+ #puts "Thread #{Thread.current}: creating write_buffer"
74
+ write_buffer = {}
75
+ # get a lock before we mess with the global hash
76
+ @mutex.synchronize do
77
+ @wbufs[Thread.current] = write_buffer
78
+ end
79
+ end
80
+ if m = write_buffer[metric.name]
81
+ # if we are already collecting this metric, just aggregate the new value
82
+ m.aggregate metric.value
83
+ else
84
+ # otherwise, add this metric to the aggregation buffer
85
+ #puts "Thread #{Thread.current}: creating metric"
86
+ write_buffer[metric.name] = metric
87
+ end
88
+ #puts "Thread #{Thread.current}: Added metric: #{metric}"
89
+ end
90
+
91
+ private
92
+
93
+ # Next two methods are called at different times during the interval,
94
+ # so any writes in progress after the swap will have time to complete.
95
+
96
+ def swap_buffers
97
+ if @rbufs == @left_buffers
98
+ @rbufs = @right_buffers
99
+ @wbufs = @left_buffers
100
+ else
101
+ @rbufs = @left_buffers
102
+ @wbufs = @right_buffers
103
+ end
104
+ end
105
+
106
+ def flush_buffers
107
+ # Each thread has it's own read buffer. If it's empty, the
108
+ # thread might be dead. We'll delete it's read buffer.
109
+ @rbufs.delete_if { |k, rb| rb.empty? }
110
+
111
+ # If not empty, aggregate all the data across all the threads,
112
+ # then send.
113
+ send_buffer = {}
114
+ @rbufs.each_value do |rb|
115
+ rb.each_value do |metric|
116
+ if m = send_buffer[metric.name]
117
+ m.aggregate metric.value
118
+ else
119
+ send_buffer[metric.name] = metric
120
+ end
121
+ end
122
+ # once we've aggregated all the metrics from this
123
+ # thread, clear out the buffer, but don't remove it.
124
+ rb.clear
125
+ end
126
+ #puts "nothing to send" if send_buffer.empty?
127
+ send_buffer.each_value do |metric|
128
+ @transport.call(metric)
129
+ end
130
+ end
131
+
132
+ end # class StatsdAggregator
@@ -0,0 +1,106 @@
1
+ # Classes used to store and manipulate each type of metric
2
+ # each type must implement initialize, aggregate, and to_s
3
+
4
+ module StatsdMetrics
5
+
6
+ class Metric
7
+ # all metrics share these
8
+ attr_accessor :name
9
+ attr_accessor :value
10
+ attr_accessor :message
11
+ end
12
+
13
+ class CMetric < Metric
14
+ # Counter
15
+ def initialize(name, value, rate=1, msg="")
16
+ @name = name
17
+ @value = value
18
+ @message = msg
19
+ @sample_rate = rate
20
+ end
21
+
22
+ def aggregate(delta)
23
+ @value += delta #accumulate
24
+ end
25
+
26
+ def to_s
27
+ if @sample_rate == 1 then r = "" else r = "|@#{@sample_rate}" end
28
+ if @message == ""
29
+ m = ""
30
+ else
31
+ if r == ""
32
+ m = "||#{@message}"
33
+ else
34
+ m = "|#{@message}"
35
+ end
36
+ end
37
+ "#{name}:#{@value}|c#{r}#{m}"
38
+ end
39
+ end
40
+
41
+ class GMetric < Metric
42
+ # Guage
43
+ def initialize(name, value, msg="")
44
+ @name = name
45
+ @value = value
46
+ @message = msg
47
+ @count = 1
48
+ end
49
+
50
+ def aggregate(value)
51
+ @value += value #average
52
+ @count += 1
53
+ end
54
+
55
+ def to_s
56
+ avg = @value / @count
57
+ if @message == "" then m = "" else m = "|#{@message}" end
58
+ "#{name}:#{avg}|g#{m}"
59
+ end
60
+
61
+ end
62
+
63
+ class TMetric < Metric
64
+ # Timing
65
+ def initialize(name, value, rate=1, msg="")
66
+ @name = name
67
+ @value = value
68
+ @sample_rate = rate
69
+ @message = msg
70
+ @count = 1
71
+ end
72
+
73
+ def aggregate(value)
74
+ @value += value #average
75
+ @count += 1
76
+ end
77
+
78
+ def to_s
79
+ avg = @value / @count
80
+ if @message == "" then m = "" else m = "|#{@message}" end
81
+ "#{name}:#{avg}|ms#{m}"
82
+ end
83
+
84
+ end
85
+
86
+ class SMetric < Metric
87
+ # Set (per the etsy standard)
88
+ def initialize(name, value, msg="")
89
+ @name = name
90
+ @value = value
91
+ @message = msg
92
+ end
93
+
94
+ def aggregate(value)
95
+ @value = value #overwrite
96
+ end
97
+
98
+ def to_s
99
+ if @message == "" then m = "" else m = "|#{@message}" end
100
+ "#{name}:#{@value}|s#{m}"
101
+ end
102
+
103
+ end
104
+
105
+ end #module StatsdMetrics
106
+
data/lib/afstatsd.rb ADDED
@@ -0,0 +1,281 @@
1
+ require 'socket'
2
+ require 'forwardable'
3
+ require 'rubygems'
4
+ require 'posix_mq'
5
+ require 'afstatsd/statsd_metrics'
6
+ require 'afstatsd/statsd_aggregator'
7
+ require 'monitor'
8
+ require 'fcntl'
9
+
10
+ # = Statsd: A Statsd client (https://github.com/etsy/statsd)
11
+ #
12
+ # @example Set up a global Statsd client for a server on localhost:9125,
13
+ # aggregate 20 seconds worth of metrics
14
+ # $statsd = Statsd.new 'localhost', 8125, 20
15
+ # @example Send some stats
16
+ # $statsd.increment 'garets'
17
+ # $statsd.timing 'glork', 320
18
+ # $statsd.gauge 'bork', 100
19
+ # @example Use {#time} to time the execution of a block
20
+ # $statsd.time('account.activate') { @account.activate! }
21
+ # @example Create a namespaced statsd client and increment 'account.activate'
22
+ # statsd = Statsd.new('localhost').tap{|sd| sd.namespace = 'account'}
23
+ # statsd.increment 'activate'
24
+ #
25
+ # Statsd instances are thread safe for general usage, by using a thread local
26
+ # UDPSocket and carrying no state. The attributes are stateful, and are not
27
+ # mutexed, it is expected that users will not change these at runtime in
28
+ # threaded environments. If users require such use cases, it is recommend that
29
+ # users either mutex around their Statsd object, or create separate objects for
30
+ # each namespace / host+port combination.
31
+ class Statsd
32
+
33
+ # A namespace to prepend to all statsd calls.
34
+ attr_reader :namespace
35
+
36
+ # StatsD host. Defaults to 127.0.0.1. Only used with UDP transport
37
+ attr_reader :host
38
+
39
+ # StatsD port. Defaults to 8125. Only used with UDP transport
40
+ attr_reader :port
41
+
42
+ # StatsD namespace prefix, generated from #namespace
43
+ attr_reader :prefix
44
+
45
+ # a postfix to append to all metrics
46
+ attr_reader :postfix
47
+
48
+ # count of messages that were dropped due to transmit error
49
+ attr_reader :dropped
50
+
51
+ class << self
52
+ # Set to a standard logger instance to enable debug logging.
53
+ attr_accessor :logger
54
+ end
55
+
56
+ # @param [String] host your statsd host
57
+ # @param [Integer] port your statsd port
58
+ # @param [Integer] interval for aggregatore
59
+ def initialize(host = '127.0.0.1', port = 8125, interval = 20)
60
+ self.host, self.port = host, port
61
+ @prefix = nil
62
+ @postfix = nil
63
+ @aggregator = StatsdAggregator.new(interval)
64
+ set_transport :mq_transport
65
+ self.aggregating = true unless interval == 0
66
+ @dropped = 0
67
+ end
68
+
69
+ # @param [method] The ruby symbol for the method that gets called to send
70
+ # one metric to the server. eg: set_transport :udp_transport
71
+ def set_transport(transport)
72
+ @transport = method(transport)
73
+ @aggregator.transport = @transport # aggregator needs to know
74
+ end
75
+
76
+ # @param [Boolean] Turn aggregation on or off
77
+ def aggregating= (should_aggregate)
78
+ if should_aggregate
79
+ @aggregator.start(@transport)
80
+ else
81
+ @aggregator.stop
82
+ end
83
+ end
84
+
85
+ # is the aggregator running?
86
+ def aggregating
87
+ @aggregator.running
88
+ end
89
+
90
+ # @attribute [w] namespace
91
+ # Writes are not thread safe.
92
+ def namespace=(namespace)
93
+ @namespace = namespace
94
+ @prefix = "#{namespace}."
95
+ end
96
+
97
+ # @attribute [w] postfix
98
+ # A value to be appended to the stat name after a '.'. If the value is
99
+ # blank then the postfix will be reset to nil (rather than to '.').
100
+ def postfix=(pf)
101
+ case pf
102
+ when nil, false, '' then @postfix = nil
103
+ else @postfix = ".#{pf}"
104
+ end
105
+ end
106
+
107
+ # @attribute [w] host
108
+ # Writes are not thread safe.
109
+ def host=(host)
110
+ @host = host || '127.0.0.1'
111
+ end
112
+
113
+ # @attribute [w] port
114
+ # Writes are not thread safe.
115
+ def port=(port)
116
+ @port = port || 8125
117
+ end
118
+
119
+ # Sends an increment (count = 1) for the given stat to the statsd server.
120
+ #
121
+ # @param [String] stat stat name
122
+ # @param [Numeric] sample_rate sample rate, 1 for always
123
+ # @param [String] optional note (AppFirst extension to StatsD)
124
+ # @see #count
125
+ def increment(stat, sample_rate=1, note="")
126
+ count stat, 1, sample_rate, note
127
+ end
128
+
129
+ # Sends a decrement (count = -1) for the given stat to the statsd server.
130
+ #
131
+ # @param [String] stat stat name
132
+ # @param [Numeric] sample_rate sample rate, 1 for always
133
+ # @param [String] optional note (AppFirst extension to StatsD)
134
+ # @see #count
135
+ def decrement(stat, sample_rate=1, note="")
136
+ count stat, -1, sample_rate, note
137
+ end
138
+
139
+ # Sends an arbitrary count for the given stat to the statsd server.
140
+ #
141
+ # @param [String] stat stat name
142
+ # @param [Integer] count count
143
+ # @param [Numeric] sample_rate sample rate, 1 for always
144
+ # @param [String] optional note (AppFirst extension to StatsD)
145
+ def count(stat, count, sample_rate=1, note="")
146
+ if sample_rate == 1 or rand < sample_rate
147
+ send_metric StatsdMetrics::CMetric.new(expand_name(stat), count, sample_rate, note)
148
+ end
149
+ end
150
+
151
+ # Sends an arbitary gauge value for the given stat to the statsd server.
152
+ #
153
+ # This is useful for recording things like available disk space,
154
+ # memory usage, and the like, which have different semantics than
155
+ # counters.
156
+ #
157
+ # @param [String] stat stat name.
158
+ # @param [Numeric] value gauge value.
159
+ # @param [String] optional note (AppFirst extension to StatsD)
160
+ # @example Report the current user count:
161
+ # $statsd.gauge('user.count', User.count)
162
+ def gauge(stat, value, note="")
163
+ send_metric StatsdMetrics::GMetric.new(expand_name(stat), value, note)
164
+ end
165
+
166
+ # Sends an arbitary set value for the given stat to the statsd server.
167
+ #
168
+ # This is for recording counts of unique events, which are useful to
169
+ # see on graphs to correlate to other values. For example, a deployment
170
+ # might get recorded as a set, and be drawn as annotations on a CPU history
171
+ # graph.
172
+ #
173
+ # @param [String] stat stat name.
174
+ # @param [Numeric] value event value.
175
+ # @param [String] optional note (AppFirst extension to StatsD)
176
+ # @example Report a deployment happening:
177
+ # $statsd.set('deployment', DEPLOYMENT_EVENT_CODE)
178
+ def set(stat, value, note="")
179
+ send_metric StatsdMetrics::SMetric.new(expand_name(stat), value, note)
180
+ end
181
+
182
+ # Sends a timing (in ms) for the given stat to the statsd server. The
183
+ # sample_rate determines what percentage of the time this report is sent. The
184
+ # statsd server then uses the sample_rate to correctly track the average
185
+ # timing for the stat.
186
+ #
187
+ # @param [String] stat stat name
188
+ # @param [Integer] ms timing in milliseconds
189
+ # @param [Numeric] sample_rate sample rate, 1 for always
190
+ # @param [String] optional note (AppFirst extension to StatsD)
191
+ def timing(stat, ms, sample_rate=1, note="")
192
+ if sample_rate == 1 or rand < sample_rate
193
+ send_metric StatsdMetrics::TMetric.new(expand_name(stat), ms, sample_rate, note)
194
+ end
195
+ end
196
+
197
+ # Reports execution time of the provided block using {#timing}.
198
+ #
199
+ # @param [String] stat stat name
200
+ # @param [Numeric] sample_rate sample rate, 1 for always
201
+ # @param [String] optional note (AppFirst extension to StatsD)
202
+ # @yield The operation to be timed
203
+ # @see #timing
204
+ # @example Report the time (in ms) taken to activate an account
205
+ # $statsd.time('account.activate') { @account.activate! }
206
+ def time(stat, sample_rate=1, note="")
207
+ start = Time.now
208
+ result = yield
209
+ timing(stat, ((Time.now - start) * 1000).round, sample_rate, note)
210
+ result
211
+ end
212
+
213
+ protected
214
+
215
+ def send_metric(metric)
216
+ # All the metric types above funnel to here. We will send or aggregate.
217
+ if aggregating
218
+ @aggregator.add metric
219
+ else
220
+ @transport.call(metric)
221
+ end
222
+ end
223
+
224
+ def expand_name(name)
225
+ # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
226
+ name = name.to_s.gsub('::', '.').tr(':|@', '_')
227
+ "#{prefix}#{name}#{postfix}"
228
+ end
229
+
230
+ def udp_transport(metric)
231
+ #puts "socket < #{metric}\n"
232
+ self.class.logger.debug { "Statsd: #{metric}" } if self.class.logger
233
+ socket.send(metric.to_s, 0, @host, @port)
234
+ rescue => boom
235
+ #puts "socket send error"
236
+ @dropped +=1
237
+ self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
238
+ nil
239
+ end
240
+
241
+ STATSD_SEVERITY = 3
242
+ def mq_transport(metric)
243
+ #puts "MQ < #{metric}\n" #debug
244
+ self.class.logger.debug { "Statsd: #{metric}" } if self.class.logger
245
+ if not @mq
246
+ begin
247
+ @mq = POSIX_MQ.new("/afcollectorapi", Fcntl::O_WRONLY | Fcntl::O_NONBLOCK)
248
+ rescue => boom
249
+ self.class.logger.debug { "Statsd: MQ open error #{boom.class} #{boom}" } if self.class.logger
250
+ # failed to open MQ. Fall back to UPD transport. Note: Current message will be lost.
251
+ @dropped += 1
252
+ # puts "fallback to udp"
253
+ set_transport :udp_transport
254
+ return nil
255
+ end
256
+ end
257
+ begin
258
+ @mq.send(metric.to_s, STATSD_SEVERITY)
259
+ rescue => boom
260
+ # just drop it on the floor
261
+ @dropped += 1
262
+ #puts "MQ send error: #{boom.class} #{boom}"
263
+ self.class.logger.error { "Statsd: MQ Send Error#{boom.class} #{boom}" } if self.class.logger
264
+ nil
265
+ end
266
+ end
267
+
268
+ def both_transport(metric)
269
+ mq_transport(metric)
270
+ udp_transport(metric)
271
+ end
272
+
273
+ private
274
+
275
+ def socket
276
+ Thread.current[:statsd_socket] ||= UDPSocket.new
277
+ end
278
+
279
+ end # class Statsd
280
+
281
+
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: afstatsd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Clark Bremer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: posix_mq
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.0.0
30
+ description: A StatsD library with AppFirst Extensions
31
+ email: clark@appfirst.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/afstatsd.rb
37
+ - lib/afstatsd/statsd_aggregator.rb
38
+ - lib/afstatsd/statsd_metrics.rb
39
+ - example/example.rb
40
+ homepage: http://appfirst.com
41
+ licenses: []
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 1.8.24
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: AppFirst StatsD Library
64
+ test_files: []
65
+ has_rdoc: