statsy 0.1.1

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.
Files changed (5) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +53 -0
  3. data/lib/statsy.rb +113 -0
  4. data/test/statsy_test.rb +124 -0
  5. metadata +80 -0
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 Sean Treadway, SoundCloud Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Statsy
2
+
3
+ [Cal made simple stat aggregation][cal]. And it was good.
4
+
5
+ [Etsy also made simple stat aggregation][etsy]. And it was also good.
6
+
7
+ This is a simple client. It does 2 things, increment and measure. Oh 3 things if you count batching too.
8
+
9
+ Usage: Default to UDP to a host in the current search domain called 'stats' on port 8125.
10
+
11
+ client = Statsy::Client.new
12
+
13
+ Usage: Use a custom transport or change the host/port pair for UDP.
14
+
15
+ client = Statsy::Client.new(Statsy::Transport::UDP.new("graphite.acme.com", 8125))
16
+ client = Statsy::Client.new(Acme::Transport::Statsd) # <- you made that
17
+ client = Statsy::Client.new(Statsy::Transport::Queue.new) # <- if you want to test stuff
18
+
19
+ Usage: Increment by 1, arbitrary integer, or arbitrary integer at a uniform random distribution
20
+
21
+ client.increment("coffee.single-espresso")
22
+ client.increment("coffee.single-espresso", 1)
23
+ client.increment("coffee.single-espresso", 1, 0.5) # 50% of the time
24
+
25
+ Usage: Measure a timing stat that will calculate the mean, min, max, upper\_90 and count
26
+
27
+ client.measure("acme.backend-runtime", response.headers["X-Runtime"].to_i)
28
+
29
+ Bonus points: Batch up many things into a fewer packets like in a shell script
30
+
31
+ loop do
32
+ batch_lines = 1000
33
+ client.batch do |batch|
34
+ $stdin.each do |log_line|
35
+ metric, timing = parse(log_line) # <- you made that
36
+ client.measure metric, timing
37
+ break if (batch_lines -= 1) <= 0
38
+ end
39
+ end
40
+ end
41
+
42
+ These stats end up in your graphite interface under the top level keys. Look for them in this folders:
43
+
44
+ stats_counts
45
+ stats/timings
46
+ stats
47
+
48
+ Fork it out of love. Enjoy.
49
+
50
+ [cal]:http://code.flickr.com/blog/2008/10/27/counting-timing/
51
+ [etsy]:http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/
52
+
53
+
data/lib/statsy.rb ADDED
@@ -0,0 +1,113 @@
1
+ # Client to access statsd service authored by etsy. Yay etsy!
2
+ # https://github.com/etsy/statsd
3
+ module Statsy
4
+ VERSION="0.1.1"
5
+
6
+ module Transport
7
+ require 'socket'
8
+
9
+ # Atomically send a Statsd encoded message to the service
10
+ # only call once per packet
11
+ module Interface
12
+ def write(stat); end
13
+ end
14
+
15
+ # UDP transport class that writes a stat per packet
16
+ # connects on construction, doesn't handle exceptions
17
+ class UDP < UDPSocket
18
+ include Interface
19
+
20
+ def initialize(host, port)
21
+ super()
22
+ connect(host, port)
23
+ end
24
+
25
+ def write(stat)
26
+ send(stat, 0)
27
+ end
28
+ end
29
+
30
+ # Queue transport writes for tests and batch operations
31
+ class Queue < Array
32
+ include Interface
33
+
34
+ def write(stat)
35
+ self.push(stat)
36
+ end
37
+ end
38
+ end
39
+
40
+ class Client
41
+ # Construct a client with a given transport that implements
42
+ # Transport::Interface
43
+ #
44
+ # Usage:
45
+ # client = Statsy::Client.new
46
+ # client = Statsy::Client.new(Statsy::Transport::UDP.new("custom", 8888))
47
+ #
48
+ def initialize(transport=Transport::UDP.new("stats", 8125))
49
+ @transport = transport
50
+ end
51
+
52
+ # Increment a count optionally at a random sample rate
53
+ #
54
+ # Usage:
55
+ # client.increment("coffee.single-espresso")
56
+ # client.increment("coffee.single-espresso", 1)
57
+ # client.increment("coffee.single-espresso", 1, 0.5) # 50% of the time
58
+ #
59
+ def increment(stat, count=1, sampling=1)
60
+ if sampling < 1
61
+ if Kernel.rand < sampling
62
+ @transport.write("%s:%d|c@%f" % [ stat, count, sampling ])
63
+ end
64
+ else
65
+ @transport.write("%s:%d|c" % [ stat, count ])
66
+ end
67
+ self
68
+ end
69
+
70
+ # Sample a timing
71
+ #
72
+ # Usage:
73
+ # client.measure("foo.backendtime", response.headers["X-Runtime"].to_i)
74
+ #
75
+ def measure(stat, time, sampling=1)
76
+ if sampling >= 1 || rand < sampling
77
+ @transport.write("%s:%d|ms" % [ stat, time ])
78
+ end
79
+ self
80
+ end
81
+
82
+ # Batch multiple transport operations, that will group any counts together
83
+ # and send the fewest number of packets with the counts/timers optimized at
84
+ # the end of the batch block.
85
+ #
86
+ # Note: this does not attempt to fit the packet size within the MTU.
87
+ #
88
+ # Usage:
89
+ # client.batch do |batch|
90
+ # batch.increment("foo.bar", 10)
91
+ # batch.measure("bat.baz", 101)
92
+ # batch.measure("foo.bar", 101)
93
+ # end
94
+ #
95
+ # => write "foo.bar:10|c:333|ms"
96
+ # => write "bat.baz:101|ms"
97
+ #
98
+ def batch
99
+ yield self.class.new(batch = Transport::Queue.new)
100
+
101
+ batch.inject(Hash.new { |h,k| h[k]=[] }) do |stats, stat|
102
+ # [ "foo.bar:10|c", "foo.bar:101|ms" ]
103
+ key, value = stat.split(':', 2)
104
+ stats[key] << value
105
+ stats
106
+ end.sort.each do |pairs|
107
+ # [ "foo.bar", [ "10|c", "101|ms" ] ]
108
+ @transport.write(pairs.flatten.join(":"))
109
+ end
110
+ self
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,124 @@
1
+ require 'test/unit'
2
+ require File.expand_path('../../lib/statsy', __FILE__)
3
+
4
+ class Unit < Test::Unit::TestCase
5
+ def setup
6
+ @transport = Statsy::Transport::Queue.new
7
+ @client = Statsy::Client.new(@transport)
8
+ end
9
+
10
+ def test_increment_should_return_self
11
+ assert_equal @client, @client.increment("foo.stat")
12
+ end
13
+
14
+ def test_increment_should_form_single_count
15
+ @client.increment("foo.stat")
16
+ assert_equal "foo.stat:1|c", @transport.shift
17
+ end
18
+
19
+ def test_increment_should_count_by_more_than_one
20
+ @client.increment("foo.stat", 101)
21
+ assert_equal "foo.stat:101|c", @transport.shift
22
+ end
23
+
24
+ def test_increment_should_sample
25
+ @client.increment("foo.stat", 1, 0.999999)
26
+ assert_equal "foo.stat:1|c@0.999999", @transport.shift
27
+ end
28
+
29
+ def test_measure_should_return_self
30
+ assert_equal @client, @client.measure("foo.stat", 100)
31
+ end
32
+
33
+ def test_measure_should_form_ms_rate
34
+ @client.measure("foo.timing", 1000)
35
+ assert_equal "foo.timing:1000|ms", @transport.shift
36
+ end
37
+
38
+ def test_measure_should_sample
39
+ @client.measure("foo.sampled.timing", 100, 0.0000001)
40
+ assert_equal nil, @transport.shift
41
+ end
42
+
43
+ def test_increment_twice_should_write_twice
44
+ @client.increment("foo.inc", 1)
45
+ @client.increment("foo.inc", 2)
46
+ assert_equal 2, @transport.size
47
+ assert_equal "foo.inc:1|c", @transport.shift
48
+ assert_equal "foo.inc:2|c", @transport.shift
49
+ end
50
+
51
+ def test_batch_should_return_self
52
+ assert_equal @client, @client.batch { }
53
+ end
54
+
55
+ def test_batch_should_write_same_as_increment
56
+ @client.increment("foo.inc")
57
+
58
+ @client.batch do |c|
59
+ c.increment("foo.inc")
60
+ end
61
+
62
+ assert_equal 2, @transport.size
63
+ assert_equal "foo.inc:1|c", @transport.shift
64
+ assert_equal "foo.inc:1|c", @transport.shift
65
+ end
66
+
67
+ def test_batch_should_only_write_once_per_key
68
+ @client.batch do |c|
69
+ c.increment("foo.inc", 2)
70
+ c.increment("foo.inc", 5)
71
+ end
72
+
73
+ assert_equal 1, @transport.size
74
+ assert_equal "foo.inc:2|c:5|c", @transport.shift
75
+ end
76
+
77
+ def test_batch_should_group_per_key
78
+ @client.batch do |c|
79
+ c.increment("foo.inc", 2)
80
+ c.increment("bar.inc", 3)
81
+ c.increment("foo.inc", 5)
82
+ c.increment("bar.inc", 7)
83
+ end
84
+
85
+ assert_equal 2, @transport.size
86
+ assert_equal "bar.inc:3|c:7|c", @transport.shift
87
+ assert_equal "foo.inc:2|c:5|c", @transport.shift
88
+ end
89
+
90
+ def test_batch_should_mix_increment_with_measure_per_key_in_sorted_order
91
+ @client.batch do |c|
92
+ c.increment("foo.inc", 2)
93
+ c.increment("bar.inc", 3)
94
+ c.measure("foo.inc", 500)
95
+ c.measure("bar.inc", 700)
96
+ end
97
+
98
+ assert_equal 2, @transport.size
99
+ assert_equal "bar.inc:3|c:700|ms", @transport.shift
100
+ assert_equal "foo.inc:2|c:500|ms", @transport.shift
101
+ end
102
+
103
+ def test_sampling_should_not_send_when_not_sampled
104
+ @client.increment("foo.sampled", 1, 0.000001)
105
+ assert_equal 0, @transport.size
106
+ end
107
+
108
+ def test_batch_should_be_nestable
109
+ @client.batch do |c1|
110
+ c1.increment("foo.inc", 2)
111
+ c1.measure("bar.inc", 700)
112
+ c1.batch do |c2|
113
+ c2.increment("foo.inc", 9)
114
+ c2.measure("bar.inc", 900)
115
+ end
116
+ c1.measure("foo.inc", 500)
117
+ c1.increment("bar.inc", 3)
118
+ end
119
+
120
+ assert_equal 2, @transport.size
121
+ assert_equal "bar.inc:700|ms:900|ms:3|c", @transport.shift
122
+ assert_equal "foo.inc:2|c:9|c:500|ms", @transport.shift
123
+ end
124
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statsy
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - Sean Treadway
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-09-08 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: test-unit
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :development
32
+ version_requirements: *id001
33
+ description: Simple way to increment counts and measure variance in timings of everything from requests per second to single espressos
34
+ email:
35
+ - treadway@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ files:
43
+ - lib/statsy.rb
44
+ - LICENSE
45
+ - README.md
46
+ - test/statsy_test.rb
47
+ has_rdoc: true
48
+ homepage: http://github.com/streadway/statsy
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options: []
53
+
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project: statsy
75
+ rubygems_version: 1.3.7
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Client network library to Statsd
79
+ test_files:
80
+ - test/statsy_test.rb