afstatsd 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/example/example.rb CHANGED
@@ -14,19 +14,18 @@ $statsd.gauge 'gauge1', 1024
14
14
  $statsd.gauge 'gauge1', 1025
15
15
  $statsd.gauge 'gauge1', 1026
16
16
  $statsd.gauge 'gauge1', 1027
17
- $statsd.gauge 'gauge1', 1028 # gauges get averaged when aggregated
17
+ $statsd.gauge 'gauge1', 1028 # gauges get overwritten when aggregated
18
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
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
23
 
24
24
 
25
25
  =begin
26
26
 
27
27
  100.times do
28
- #$statsd.increment 'sampled', 0.1, 'sampled'
29
- $statsd.increment 'sampled'
28
+ $statsd.increment 'sampled', 0.1, 'sampled'
30
29
  end
31
30
 
32
31
  $statsd.set 'set1', 1099, "ez"
@@ -39,7 +38,6 @@ end
39
38
  $statsd.increment 'fast' # don't do this if aggregation is off
40
39
  end
41
40
 
42
- # In this test program, this will give the aggregator time to run.
43
41
  15.times do
44
42
  sleep 2
45
43
  $statsd.increment 'slow'
@@ -1,132 +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
-
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.0
40
+ swap_buffers
41
+ sleep @interval/2.0
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
132
  end # class StatsdAggregator
@@ -1,103 +1,103 @@
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
- end
48
-
49
- def aggregate(value)
50
- @value = value #overwrite
51
- end
52
-
53
- def to_s
54
- if @message == "" then m = "" else m = "|#{@message}" end
55
- "#{name}:#{@value}|g#{m}"
56
- end
57
-
58
- end
59
-
60
- class TMetric < Metric
61
- # Timing
62
- def initialize(name, value, rate=1, msg="")
63
- @name = name
64
- @value = value
65
- @sample_rate = rate
66
- @message = msg
67
- @count = 1
68
- end
69
-
70
- def aggregate(value)
71
- @value += value #average
72
- @count += 1
73
- end
74
-
75
- def to_s
76
- avg = @value / @count
77
- if @message == "" then m = "" else m = "|#{@message}" end
78
- "#{name}:#{avg}|ms#{m}"
79
- end
80
-
81
- end
82
-
83
- class SMetric < Metric
84
- # Set (per the etsy standard)
85
- def initialize(name, value, msg="")
86
- @name = name
87
- @value = value
88
- @message = msg
89
- end
90
-
91
- def aggregate(value)
92
- @value = value #overwrite
93
- end
94
-
95
- def to_s
96
- if @message == "" then m = "" else m = "|#{@message}" end
97
- "#{name}:#{@value}|s#{m}"
98
- end
99
-
100
- end
101
-
102
- end #module StatsdMetrics
103
-
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
+ end
48
+
49
+ def aggregate(value)
50
+ @value = value #overwrite
51
+ end
52
+
53
+ def to_s
54
+ if @message == "" then m = "" else m = "|#{@message}" end
55
+ "#{name}:#{@value}|g#{m}"
56
+ end
57
+
58
+ end
59
+
60
+ class TMetric < Metric
61
+ # Timing
62
+ def initialize(name, value, rate=1, msg="")
63
+ @name = name
64
+ @value = value
65
+ @sample_rate = rate
66
+ @message = msg
67
+ @count = 1
68
+ end
69
+
70
+ def aggregate(value)
71
+ @value += value #average
72
+ @count += 1
73
+ end
74
+
75
+ def to_s
76
+ avg = @value / @count
77
+ if @message == "" then m = "" else m = "|#{@message}" end
78
+ "#{name}:#{avg}|ms#{m}"
79
+ end
80
+
81
+ end
82
+
83
+ class SMetric < Metric
84
+ # Set (per the etsy standard)
85
+ def initialize(name, value, msg="")
86
+ @name = name
87
+ @value = value
88
+ @message = msg
89
+ end
90
+
91
+ def aggregate(value)
92
+ @value = value #overwrite
93
+ end
94
+
95
+ def to_s
96
+ if @message == "" then m = "" else m = "|#{@message}" end
97
+ "#{name}:#{@value}|s#{m}"
98
+ end
99
+
100
+ end
101
+
102
+ end #module StatsdMetrics
103
+
data/lib/afstatsd.rb CHANGED
@@ -1,281 +1,295 @@
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
-
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
+ # Turn on debug messages
52
+ attr_accessor :debugging
53
+
54
+ class << self
55
+ # Set to a standard logger instance to enable debug logging.
56
+ attr_accessor :logger
57
+ end
58
+
59
+ # @param [String] host your statsd host
60
+ # @param [Integer] port your statsd port
61
+ # @param [Integer] interval for aggregatore
62
+ def initialize(host = '127.0.0.1', port = 8125, interval = 20)
63
+ self.host, self.port = host, port
64
+ @prefix = nil
65
+ @postfix = nil
66
+ @aggregator = StatsdAggregator.new(interval)
67
+ set_transport :mq_transport
68
+ self.aggregating = true unless interval == 0
69
+ @dropped = 0
70
+ @debugging = false
71
+ end
72
+
73
+ # @param [method] The ruby symbol for the method that gets called to send
74
+ # one metric to the server. eg: set_transport :udp_transport
75
+ def set_transport(transport)
76
+ @transport = method(transport)
77
+ @aggregator.transport = @transport # aggregator needs to know
78
+ end
79
+
80
+ # @attribute [Boolean] Turn aggregation on or off
81
+ def aggregating= (should_aggregate)
82
+ if should_aggregate
83
+ @aggregator.start(@transport)
84
+ else
85
+ @aggregator.stop
86
+ end
87
+ end
88
+
89
+ # is the aggregator running?
90
+ def aggregating
91
+ @aggregator.running
92
+ end
93
+
94
+ # @attribute [w] namespace
95
+ # Writes are not thread safe.
96
+ def namespace=(namespace)
97
+ @namespace = namespace
98
+ @prefix = "#{namespace}."
99
+ end
100
+
101
+ # @attribute [w] postfix
102
+ # A value to be appended to the stat name after a '.'. If the value is
103
+ # blank then the postfix will be reset to nil (rather than to '.').
104
+ def postfix=(pf)
105
+ case pf
106
+ when nil, false, '' then @postfix = nil
107
+ else @postfix = ".#{pf}"
108
+ end
109
+ end
110
+
111
+ # @attribute [Numeric] interval
112
+ # Set aggregation interval
113
+ def interval=(iv)
114
+ @aggregator.set_interval(iv)
115
+ end
116
+
117
+ # @attribute [w] host
118
+ # Writes are not thread safe.
119
+ def host=(host)
120
+ @host = host || '127.0.0.1'
121
+ end
122
+
123
+ # @attribute [w] port
124
+ # Writes are not thread safe.
125
+ def port=(port)
126
+ @port = port || 8125
127
+ end
128
+
129
+ # Sends an increment (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 increment(stat, sample_rate=1, note="")
136
+ count stat, 1, sample_rate, note
137
+ end
138
+
139
+ # Sends a decrement (count = -1) for the given stat to the statsd server.
140
+ #
141
+ # @param [String] stat stat name
142
+ # @param [Numeric] sample_rate sample rate, 1 for always
143
+ # @param [String] optional note (AppFirst extension to StatsD)
144
+ # @see #count
145
+ def decrement(stat, sample_rate=1, note="")
146
+ count stat, -1, sample_rate, note
147
+ end
148
+
149
+ # Sends an arbitrary count for the given stat to the statsd server.
150
+ #
151
+ # @param [String] stat stat name
152
+ # @param [Integer] count count
153
+ # @param [Numeric] sample_rate sample rate, 1 for always
154
+ # @param [String] optional note (AppFirst extension to StatsD)
155
+ def count(stat, count, sample_rate=1, note="")
156
+ if sample_rate == 1 or rand < sample_rate
157
+ send_metric StatsdMetrics::CMetric.new(expand_name(stat), count, sample_rate, note)
158
+ end
159
+ end
160
+
161
+ # Sends an arbitary gauge value for the given stat to the statsd server.
162
+ #
163
+ # This is useful for recording things like available disk space,
164
+ # memory usage, and the like, which have different semantics than
165
+ # counters.
166
+ #
167
+ # @param [String] stat stat name.
168
+ # @param [Numeric] value gauge value.
169
+ # @param [String] optional note (AppFirst extension to StatsD)
170
+ # @example Report the current user count:
171
+ # $statsd.gauge('user.count', User.count)
172
+ def gauge(stat, value, note="")
173
+ send_metric StatsdMetrics::GMetric.new(expand_name(stat), value, note)
174
+ end
175
+
176
+ # Sends an arbitary set value for the given stat to the statsd server.
177
+ #
178
+ # This is for recording counts of unique events, which are useful to
179
+ # see on graphs to correlate to other values. For example, a deployment
180
+ # might get recorded as a set, and be drawn as annotations on a CPU history
181
+ # graph.
182
+ #
183
+ # @param [String] stat stat name.
184
+ # @param [Numeric] value event value.
185
+ # @param [String] optional note (AppFirst extension to StatsD)
186
+ # @example Report a deployment happening:
187
+ # $statsd.set('deployment', DEPLOYMENT_EVENT_CODE)
188
+ def set(stat, value, note="")
189
+ send_metric StatsdMetrics::SMetric.new(expand_name(stat), value, note)
190
+ end
191
+
192
+ # Sends a timing (in ms) for the given stat to the statsd server. The
193
+ # sample_rate determines what percentage of the time this report is sent. The
194
+ # statsd server then uses the sample_rate to correctly track the average
195
+ # timing for the stat.
196
+ #
197
+ # @param [String] stat stat name
198
+ # @param [Integer] ms timing in milliseconds
199
+ # @param [Numeric] sample_rate sample rate, 1 for always
200
+ # @param [String] optional note (AppFirst extension to StatsD)
201
+ def timing(stat, ms, sample_rate=1, note="")
202
+ if sample_rate == 1 or rand < sample_rate
203
+ send_metric StatsdMetrics::TMetric.new(expand_name(stat), ms, sample_rate, note)
204
+ end
205
+ end
206
+
207
+ # Reports execution time of the provided block using {#timing}.
208
+ #
209
+ # @param [String] stat stat name
210
+ # @param [Numeric] sample_rate sample rate, 1 for always
211
+ # @param [String] optional note (AppFirst extension to StatsD)
212
+ # @yield The operation to be timed
213
+ # @see #timing
214
+ # @example Report the time (in ms) taken to activate an account
215
+ # $statsd.time('account.activate') { @account.activate! }
216
+ def time(stat, sample_rate=1, note="")
217
+ start = Time.now
218
+ result = yield
219
+ timing(stat, ((Time.now - start) * 1000).round, sample_rate, note)
220
+ result
221
+ end
222
+
223
+ protected
224
+
225
+ def send_metric(metric)
226
+ # All the metric types above funnel to here. We will send or aggregate.
227
+ if aggregating
228
+ @aggregator.add metric
229
+ else
230
+ @transport.call(metric)
231
+ end
232
+ end
233
+
234
+ def expand_name(name)
235
+ # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
236
+ name = name.to_s.gsub('::', '.').tr(':|@', '_')
237
+ "#{prefix}#{name}#{postfix}"
238
+ end
239
+
240
+ def udp_transport(metric)
241
+ if @debugging
242
+ puts "socket < #{metric}\n" #debug
243
+ end
244
+ self.class.logger.debug { "Statsd: #{metric}" } if self.class.logger
245
+ socket.send(metric.to_s, 0, @host, @port)
246
+ rescue => boom
247
+ #puts "socket send error"
248
+ @dropped +=1
249
+ self.class.logger.debug { "Statsd: #{boom.class} #{boom}" } if self.class.logger
250
+ nil
251
+ end
252
+
253
+ STATSD_SEVERITY = 3
254
+ def mq_transport(metric)
255
+ if @debugging
256
+ puts "MQ < #{metric}\n" #debug
257
+ end
258
+ self.class.logger.debug { "Statsd: #{metric}" } if self.class.logger
259
+ if not @mq
260
+ begin
261
+ @mq = POSIX_MQ.new("/afcollectorapi", Fcntl::O_WRONLY | Fcntl::O_NONBLOCK)
262
+ rescue => boom
263
+ self.class.logger.debug { "Statsd: MQ open error #{boom.class} #{boom}" } if self.class.logger
264
+ # failed to open MQ. Fall back to UPD transport. Note: Current message will be lost.
265
+ @dropped += 1
266
+ # puts "fallback to udp"
267
+ set_transport :udp_transport
268
+ return nil
269
+ end
270
+ end
271
+ begin
272
+ @mq.send(metric.to_s, STATSD_SEVERITY)
273
+ rescue => boom
274
+ # just drop it on the floor
275
+ @dropped += 1
276
+ #puts "MQ send error: #{boom.class} #{boom}"
277
+ self.class.logger.debug { "Statsd: MQ Send Error#{boom.class} #{boom}" } if self.class.logger
278
+ nil
279
+ end
280
+ end
281
+
282
+ def both_transport(metric)
283
+ mq_transport(metric)
284
+ udp_transport(metric)
285
+ end
286
+
287
+ private
288
+
289
+ def socket
290
+ Thread.current[:statsd_socket] ||= UDPSocket.new
291
+ end
292
+
293
+ end # class Statsd
294
+
295
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: afstatsd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-06 00:00:00.000000000 Z
12
+ date: 2013-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: posix_mq