rstatsd 0.2

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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.2-p290@rstatsd
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rstatsd.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Dan Herrera
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # rstatsd
2
+
3
+ rstatsd is a ruby based daemon for capturing data from statsd clients.
4
+
5
+ rstatsd is inspired by the work at etsy to measure everything, measure
6
+ anything. They use a combination of node.js and graphite to capture and
7
+ graph this data.
8
+
9
+ The goal of this project was to replicate this light-weight approach and
10
+ reduce the number of dependencies to do this to two: redis and ruby.
11
+
12
+ ## Installation
13
+
14
+ $ gem install rstatsd
15
+
16
+ ## Usage
17
+
18
+ Start redis
19
+
20
+ $ brew install redis
21
+ $ redis-server /usr/local/etc/redis.conf
22
+
23
+
24
+ Start the collection daemon and server
25
+
26
+ $ rstatsd
27
+
28
+ Add some data (you'll need a statsd compatible client like statsd-ruby)
29
+
30
+ $ irb
31
+ irb> require 'statsd'
32
+ => true
33
+ irb> s = Statsd.new('localhost')
34
+ => #<Statsd:0x007fee419866d8 @host="localhost", @port=8125>
35
+ irb(main):004:0> s.increment('grebulons')
36
+ => 10
37
+ irb> s.increment('grebulons')
38
+ => 10
39
+ irb> s.increment('grebulons')
40
+ => 10
41
+ irb> s.increment('grebulons')
42
+ => 10
43
+
44
+
45
+ Then view the result in a web browser
46
+
47
+ irb> `open http://localhost:8126/?target=grebulons`
48
+
49
+ Bask in the something of something-something.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/rstatsd ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rstatsd/autorun'
data/lib/rstatsd.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'eventmachine'
2
+
3
+ require_relative './rstatsd/collector'
4
+ require_relative './rstatsd/server'
5
+ require_relative './rstatsd/helpers'
6
+ require_relative './rstatsd/chart'
7
+ require_relative './rstatsd/config'
8
+
9
+ module Rstatsd
10
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'server'
2
+ require_relative 'collector'
3
+
4
+ EventMachine::run {
5
+ EventMachine::open_datagram_socket("127.0.0.1", 8125, Rstatsd::Collector)
6
+ EventMachine::start_server("127.0.0.1", 8126, Rstatsd::Server)
7
+ puts "http server running at http://localhost:8126/"
8
+ puts "go there for more instructions"
9
+ }
@@ -0,0 +1,51 @@
1
+ require 'json'
2
+
3
+ module Rstatsd
4
+ class Chart
5
+ include Rstatsd::Helpers
6
+
7
+ attr_accessor :data
8
+
9
+ def initialize(query_string)
10
+ @query_string = query_string
11
+ @data = {}
12
+ end
13
+
14
+ def targets
15
+ @query_string.split("&").map do |term|
16
+ param_name, value = term.split("=")
17
+ value if param_name == 'target'
18
+ end
19
+ end
20
+
21
+ def column_types
22
+ targets.inject([['datetime', 'Timestamp']]) do |memo, target|
23
+ memo << ['number', target.capitalize]
24
+ memo
25
+ end
26
+ end
27
+
28
+ def title
29
+ @query_string.split("&").detect do |term|
30
+ param_name, value = term.split("=")
31
+ if param_name == 'title'
32
+ return URI.unescape(value)
33
+ end
34
+ end
35
+ targets.map(&:capitalize).join(', ')
36
+ end
37
+
38
+ def draw_chart
39
+ @data = JSON.dump(fetch_counters(targets))
40
+ yield self
41
+ end
42
+
43
+ def width
44
+ 800
45
+ end
46
+
47
+ def height
48
+ 480
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ require 'em-hiredis'
2
+ require_relative './helpers'
3
+
4
+ module Rstatsd
5
+ class Collector < EventMachine::Connection
6
+ include Rstatsd::Helpers
7
+
8
+ def initialize
9
+ super
10
+ @redis = EM::Hiredis.connect
11
+ end
12
+
13
+ def post_init
14
+ end
15
+
16
+ def receive_data(data)
17
+ bits = data.split(':')
18
+ key = format_key(bits.first)
19
+
20
+ fields = bits.last.split("|")
21
+ case fields[1]
22
+ when 'c'
23
+ if fields[0] == '1'
24
+ @redis.incr(key).callback do |value|
25
+ @redis.rpush(counter_key_name(key), "#{value}:#{Time.now.to_i}")
26
+ end
27
+ elsif fields[0] == '-1'
28
+ @redis.decr(key).callback do |value|
29
+ @redis.rpush(counter_key_name(key), "#{value}:#{Time.now.to_i}")
30
+ end
31
+ end
32
+ @redis.ltrim(counter_key_name(key), 10000)
33
+ when 'ms'
34
+ #update timer
35
+ @redis.rpush(timer_key_name(key), "#{fields[0]}:#{Time.now.to_i}")
36
+ @redis.ltrim(timer_key_name(key), 10000)
37
+ else
38
+ # invalid update
39
+ end
40
+ end
41
+
42
+ def unbind
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ module Rstatsd
2
+ attr_accessor :configuration
3
+
4
+ def self.configure
5
+ configuration ||= Configuration.new()
6
+ yield(configuration)
7
+ @configuration = configuration
8
+ end
9
+
10
+ class Configuration
11
+ attr_accessor :redis_db, :redis_host
12
+
13
+ def initialize
14
+ @redis_db = 1
15
+ @redis_host = '127.0.0.1:6379'
16
+ end
17
+
18
+ def [](option)
19
+ send(option)
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,45 @@
1
+ require 'redis'
2
+ require 'hiredis'
3
+
4
+ module Rstatsd
5
+ module Helpers
6
+ def format_key(key)
7
+ key.strip
8
+ .gsub(/\s+/, '_')
9
+ .gsub(/\//, '-')
10
+ .gsub(/[^a-zA-Z_\-0-9\.]/, '')
11
+ end
12
+
13
+ def counter_key_name(key)
14
+ "counter:#{key}"
15
+ end
16
+
17
+ def timer_key_name(key)
18
+ "timer:#{key}"
19
+ end
20
+
21
+ def redis
22
+ @redis ||= Redis.new
23
+ end
24
+
25
+ def fetch_counters(counters)
26
+ finished_data = {}
27
+ counters.each_with_index do |counter, index|
28
+ data = redis_data_for(counter)
29
+ data.keys.each do |key|
30
+ finished_data[key] ||= Array.new(counters.length, 0)
31
+ finished_data[key][index] = data[key]
32
+ end
33
+ end
34
+ finished_data
35
+ end
36
+
37
+ def redis_data_for(key)
38
+ redis.lrange(counter_key_name(key), 0, -1).inject({}) do |memo, point|
39
+ val, time = point.split(":")
40
+ memo[time] = val.to_i
41
+ memo
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ require 'em-hiredis'
2
+ require 'eventmachine_httpserver'
3
+ require 'evma_httpserver/response'
4
+ require 'erb'
5
+
6
+ require_relative './helpers'
7
+ require_relative './chart'
8
+
9
+ module Rstatsd
10
+ class Server < EventMachine::Connection
11
+ include EventMachine::HttpServer
12
+ include Rstatsd::Helpers
13
+
14
+ def post_init
15
+ super
16
+ end
17
+
18
+ def process_http_request
19
+ response = EM::DelegatedHttpResponse.new(self)
20
+
21
+ case @http_request_uri
22
+ when '/'
23
+ response.content_type 'text/html'
24
+ response.content = File.open('templates/index.html').read
25
+ response.send_response
26
+ when '/stats'
27
+ Rstatsd::Chart.new(@http_query_string).draw_chart do |chart|
28
+ @chart = chart
29
+ google_chart = ERB.new(File.open('templates/google_chart.erb').read).result(binding)
30
+
31
+ response.content_type 'text/html'
32
+ response.content = google_chart
33
+ response.send_response
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Rstatsd
2
+ VERSION = 0.2
3
+ end
data/rstatsd.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/rstatsd/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Whoahbot!"]
6
+ gem.email = ["whoahbot@gmail.com"]
7
+ gem.description = %q{a stats daemon that stores data in redis}
8
+ gem.summary = %q{rstatsd is a simpler ruby implementaiton of statsd, storing the data in redis}
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.bindir = 'bin'
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ gem.name = "rstatsd"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = Rstatsd::VERSION
18
+
19
+ gem.add_dependency "redis"
20
+ gem.add_dependency "hiredis"
21
+ gem.add_dependency "eventmachine"
22
+ gem.add_dependency "eventmachine_httpserver"
23
+
24
+ gem.add_development_dependency "rspec"
25
+ gem.add_development_dependency "timecop"
26
+ gem.add_development_dependency "statsd-ruby"
27
+ end
data/spec/benchmark.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'statsd'
2
+ require 'benchmark'
3
+
4
+ # user system total real
5
+ # 0.920000 1.900000 2.820000 ( 3.579831)
6
+
7
+ s = Statsd.new('localhost')
8
+
9
+ Benchmark.bm do |x|
10
+ x.report { 10000.times { s.increment('foobar') }}
11
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rstatsd::Chart do
4
+ let(:new_chart) {
5
+ Rstatsd::Chart.new('target=crumdinglers')
6
+ }
7
+
8
+ describe "query string parsing" do
9
+ context "for one chart target" do
10
+ it "should find the data requested from the target parameters" do
11
+ Rstatsd::Chart.new("target=crumdinglers").targets.should == ['crumdinglers']
12
+ end
13
+ end
14
+
15
+ context "for multiple chart targets" do
16
+ it "should find the data requested from the target parameters" do
17
+ Rstatsd::Chart.new("target=crumdinglers&target=snozzblers").targets.should == ['crumdinglers', 'snozzblers']
18
+ end
19
+ end
20
+ end
21
+
22
+ describe "#title" do
23
+ let(:new_chart) {
24
+ Rstatsd::Chart.new('target=crumdinglers')
25
+ }
26
+
27
+ context "when no title is provided" do
28
+ it "should set the default title to be the target when none is given" do
29
+ new_chart.title.should == "Crumdinglers"
30
+ end
31
+ end
32
+
33
+ context "when a title is provided in the options hash" do
34
+ it "should return the title" do
35
+ Rstatsd::Chart.new('target=crumdinglers&title=Number%20of%20Crumdinglers').title.should ==
36
+ "Number of Crumdinglers"
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "#width" do
42
+ context "when no width is specified" do
43
+ it "should default to 800" do
44
+ new_chart.width.should == 800
45
+ end
46
+ end
47
+ end
48
+
49
+ describe "#height" do
50
+ context "when no height is specified" do
51
+ it "should default to 480" do
52
+ new_chart.height.should == 480
53
+ end
54
+ end
55
+ end
56
+
57
+ describe "#column_types" do
58
+ context "with one target" do
59
+ it "should return an array of column types and labels that describe the data" do
60
+ new_chart.column_types.should ==
61
+ [['datetime', 'Timestamp'],
62
+ ['number', 'Crumdinglers']]
63
+ end
64
+ end
65
+
66
+ context "with multiple targets" do
67
+ it "should only include one timestamp column" do
68
+ Rstatsd::Chart.new('target=crumdinglers&target=crambizzlers').
69
+ column_types.should == [
70
+ ['datetime', 'Timestamp'],
71
+ ['number', 'Crumdinglers'],
72
+ ['number', 'Crambizzlers']
73
+ ]
74
+ end
75
+ end
76
+ end
77
+ end