simple_metrics 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.3@simple_metrics
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in simple-metrics-server.gemspec
4
+ gemspec
data/README.markdown ADDED
@@ -0,0 +1,127 @@
1
+ SimpleMetrics
2
+ =============
3
+
4
+ SimpleMetrics makes it easy to collect and aggregate data (specifically counters, timers and events).
5
+
6
+ It is heavily inspired by Statsd (https://github.com/etsy/statsd) from Etsy. Read the "Measure Anything, measure Everything" blog post (http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/) which did it for me.
7
+
8
+ Technically speaking it provides a simple UDP interface to send data to an Eventmachine based UDP Server. The data is stored in MongoDB using a Round Robin Database (RRD) scheme.
9
+
10
+ SimpleMetrics is written in Ruby and packaged as a gem.
11
+
12
+ The current version is considered ALPHA.
13
+
14
+ SimpleMetrics Client
15
+ --------------------
16
+
17
+ Commandline client:
18
+
19
+ Send a count of 5 for data point "module.test1":
20
+
21
+ simple_metrics_client module.test1 -counter 5
22
+
23
+ Send a timing of 100ms:
24
+
25
+ simple_metrics_client module.test1 -timing 100
26
+
27
+ doing the same, but since we expect a lot of calls we sample the data (10%):
28
+
29
+ simple_metrics_client module.test1 -timing 100 --sample_rate 0.1
30
+
31
+ more info:
32
+
33
+ simple_metrics_client --help
34
+
35
+ Ruby client API
36
+ ---------------
37
+
38
+ Initialize client:
39
+
40
+ client = SimpleMetrics::Client.new("localhost")
41
+
42
+ sends "com.example.test1:1|c" via UDP:
43
+
44
+ client.increment("com.example.test1")
45
+
46
+ sends "com.example.test1:-1|c":
47
+
48
+ client.decrement("com.example.test1")
49
+
50
+ sends "com.example.test1:5|c" (a counter with a relative value of 5):
51
+
52
+ client.count("com.example.test1", 5)
53
+
54
+ sends "com.example.test1:5|c|@0.1" with a sample rate of 10%:
55
+
56
+ client.count("com.example.test1", 5, 0.1)
57
+
58
+ sends "com.example.test1:5|g" (meaning gauge, an absolute value of 5):
59
+
60
+ client.count("com.example.test1", 5)
61
+
62
+ sends "com.example.test1:100|ms":
63
+
64
+ client.timing("com.example.test1")
65
+
66
+ More examples in the examples/ directory.
67
+
68
+ SimpleMetrics Server
69
+ --------------------
70
+
71
+ We provide a simple commandline wrapper using daemons gem (http://daemons.rubyforge.org/).
72
+
73
+ Start Server as background daemond:
74
+
75
+ simple_metrics_server start
76
+
77
+ Start in foreground:
78
+
79
+ simple_metrics_server start -t
80
+
81
+ Show Help:
82
+
83
+ simple_metrics_server --help
84
+
85
+ Round Robin Database Principles in MongoDB
86
+ ------------------------------------------
87
+
88
+ We use 4 collections in MongoDB each with more coarse timestamp buckets:
89
+ * 10 sec
90
+ * 1 min
91
+ * 10 min
92
+ * 1 day
93
+
94
+ The 10s and 1m collections are capped collections and have a fixed size. The other will store the data as long as we have sufficient disc space.
95
+
96
+ How can we map these times to graphs?
97
+
98
+ * 10 sec, near real-time graph (ttl: several hours)
99
+ * 1 min, last hour (ttl: several days)
100
+ * 10 min, whole day view (ttl: forever)
101
+ * 1 day , week view (ttl: forever)
102
+
103
+ License
104
+ -------
105
+
106
+ (The MIT License)
107
+
108
+ Copyright (c) 2012 Frederik Dietz <fdietz@gmail.com>
109
+
110
+ Permission is hereby granted, free of charge, to any person obtaining
111
+ a copy of this software and associated documentation files (the
112
+ 'Software'), to deal in the Software without restriction, including
113
+ without limitation the rights to use, copy, modify, merge, publish,
114
+ distribute, sublicense, and/or sell copies of the Software, and to
115
+ permit persons to whom the Software is furnished to do so, subject to
116
+ the following conditions:
117
+
118
+ The above copyright notice and this permission notice shall be
119
+ included in all copies or substantial portions of the Software.
120
+
121
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
122
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
123
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
124
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
125
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
126
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
127
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new
6
+ task :default => :spec
7
+ task :test => :spec
8
+
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+
6
+ require 'optparse'
7
+ require "simple_metrics"
8
+
9
+ options = {
10
+ :host => 'localhost',
11
+ :port => 8125,
12
+ :sample_rate => 1
13
+ }
14
+
15
+ parser ||= OptionParser.new do |opts|
16
+ opts.banner = "Usage Example: simple_metrics_send com.test.mymetric -c5"
17
+
18
+ opts.separator ""
19
+ opts.separator "Client options:"
20
+
21
+ opts.on("-c", "--counter VALUE", "Counter, a relative value") do |value|
22
+ options[:type] = 'c'
23
+ options[:stat] = value.to_i
24
+ end
25
+ opts.on("-g", "--gauge VALUE", "Gauge, an absolute value ") do |value|
26
+ options[:type] = 'g'
27
+ options[:stat] = value.to_i
28
+ end
29
+ opts.on("-t", "--timing VALUE", "A timing in ms") do |value|
30
+ options[:type] = 'ms'
31
+ options[:stat] = value.to_i
32
+ end
33
+ opts.on("-s", "--sample_rate VALUE", "An optional sample rate between 0 and 1 (example: 0.2)") do |value|
34
+ options[:sample_rate] = value.to_f || 1
35
+ end
36
+
37
+ opts.separator ""
38
+
39
+ opts.on("-a", "--address HOST", "bind to HOST address (default: #{options[:host]})") do |host|
40
+ options[:host] = host
41
+ end
42
+
43
+ opts.on("-p", "--port PORT", "use PORT (default: #{options[:port]})") do |port|
44
+ options[:port] = port.to_i
45
+ end
46
+
47
+ opts.separator ""
48
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
49
+ opts.on_tail('-v', '--version', "Show version") { puts SimpleMetrics::VERSION; exit }
50
+
51
+ end.parse!(ARGV)
52
+
53
+ command = ARGV.shift
54
+ arguments = ARGV
55
+ client = SimpleMetrics::Client.new(options[:host])
56
+
57
+ case options[:type]
58
+ when'c'
59
+ client.count(command, options[:stat], options[:sample_rate])
60
+ when 'g'
61
+ client.gauge(command, options[:stat], options[:sample_rate])
62
+ when 'ms'
63
+ client.timing(command, options[:stat], options[:sample_rate])
64
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ require "simple_metrics"
6
+ require "daemons"
7
+
8
+ options = {
9
+ :backtrace => true,
10
+ :log_output => true,
11
+ :dir_mode => :script
12
+ }
13
+
14
+ Daemons.run_proc("simple_metrics", options) do
15
+ SimpleMetrics::Server.new.start
16
+ end
17
+
18
+
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+ require "simple_metrics"
5
+
6
+ client = SimpleMetrics::Client.new("localhost")
7
+ # com.example.test1:1|c
8
+ client.increment("com.example.test1")
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+ require "logger"
3
+
4
+ require "simple_metrics/version"
5
+ require "simple_metrics/client"
6
+ require "simple_metrics/server"
7
+ require "simple_metrics/stats"
8
+ require "simple_metrics/bucket"
9
+ require "simple_metrics/mongo"
10
+
11
+ module SimpleMetrics
12
+ extend self
13
+
14
+ def logger
15
+ @@logger ||= Logger.new(STDOUT)
16
+ end
17
+
18
+ def logger=(logger)
19
+ @@logger = logger
20
+ end
21
+
22
+ CONFIG_DEFAULTS = {
23
+ :host => 'localhost',
24
+ :port => 8125,
25
+ :flush_interval => 10
26
+ }.freeze
27
+
28
+ def config
29
+ @@config ||= CONFIG_DEFAULTS
30
+ end
31
+
32
+ def config=(options)
33
+ @@config = CONFIG_DEFAULTS.merge(options)
34
+ end
35
+
36
+ BUCKETS_DEFAULTS = [
37
+ {
38
+ :name => 'stats_per_10s',
39
+ :seconds => 10,
40
+ :capped => true,
41
+ :size => 100_100_100
42
+ },
43
+ {
44
+ :name => 'stats_per_1min',
45
+ :seconds => 60,
46
+ :capped => true,
47
+ :size => 1_100_100_100
48
+ },
49
+ {
50
+ :name => 'stats_per_10min',
51
+ :seconds => 600,
52
+ :size => 0 ,
53
+ :capped => false
54
+ },
55
+ {
56
+ :name => 'stats_per_day',
57
+ :seconds => 86400,
58
+ :size => 0,
59
+ :capped => false
60
+ }
61
+ ].freeze
62
+
63
+ def buckets_config
64
+ @@buckets ||= BUCKETS_DEFAULTS
65
+ end
66
+
67
+ def buckets_config=(buckets)
68
+ @@buckets = buckets
69
+ end
70
+
71
+ MONGODB_DEFAULTS = {
72
+ :pool_size => 5,
73
+ :timeout => 5,
74
+ :strict => true
75
+ }.freeze
76
+
77
+ DB_CONFIG_DEFAULTS = {
78
+ :host => 'localhost',
79
+ :prefix => 'development'
80
+ }.freeze
81
+
82
+ def db_config=(options)
83
+ @@db_config = {
84
+ :host => options.delete(:host),
85
+ :db_name => "simple_metrics_#{options.delete(:prefix)}",
86
+ :options => MONGODB_DEFAULTS.merge(options)
87
+ }
88
+ end
89
+
90
+ def db_config
91
+ @@db_config ||= DB_CONFIG_DEFAULTS.merge(
92
+ :db_name => "simple_metrics_#{DB_CONFIG_DEFAULTS[:prefix]}",
93
+ :options => MONGODB_DEFAULTS
94
+ )
95
+ end
96
+
97
+ end
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+ class Bucket
4
+
5
+ class << self
6
+
7
+ def all
8
+ @@all ||= SimpleMetrics.buckets_config.map { |r| Bucket.new(r) }
9
+ end
10
+
11
+ def first
12
+ all.first
13
+ end
14
+ alias :finest :first
15
+
16
+ def [](index)
17
+ all[index]
18
+ end
19
+
20
+ def coarse_buckets
21
+ Bucket.all.sort_by! { |r| r.seconds }[1..-1]
22
+ end
23
+
24
+ def flush_stats(stats)
25
+ return if stats.empty?
26
+ SimpleMetrics.logger.info "#{Time.now} Flushing #{stats.count} counters to MongoDB"
27
+
28
+ ts = Time.now.utc.to_i
29
+ bucket = Bucket.first
30
+ stats.each { |data| bucket.save(data, ts) }
31
+
32
+ self.aggregate_all(ts)
33
+ end
34
+
35
+ def aggregate_all(ts)
36
+ ts_bucket = self.first.ts_bucket(ts)
37
+
38
+ coarse_buckets.each do |bucket|
39
+ current_ts = bucket.ts_bucket(ts_bucket)
40
+ previous_ts = bucket.previous_ts_bucket(ts_bucket)
41
+ SimpleMetrics.logger.debug "Aggregating #{bucket.name} #{previous_ts}....#{current_ts} (#{humanized_timestamp(previous_ts)}..#{humanized_timestamp(current_ts)})"
42
+
43
+ unless bucket.stats_exist_in_previous_ts?(previous_ts)
44
+ stats_coll = self.first.find_all_in_ts_range(previous_ts, current_ts)
45
+ stats_coll.group_by { |stats| stats.name }.each_pair do |name,stats_array|
46
+ stats = Stats.aggregate(stats_array)
47
+ bucket.save(stats, previous_ts)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def humanized_timestamp(ts)
56
+ Time.at(ts).utc
57
+ end
58
+ end
59
+
60
+ attr_reader :name, :capped
61
+
62
+ def initialize(attributes)
63
+ @name = attributes[:name]
64
+ @seconds = attributes[:seconds]
65
+ @capped = attributes[:capped]
66
+ @size = attributes[:size]
67
+ end
68
+
69
+ def seconds
70
+ @seconds.to_i
71
+ end
72
+
73
+ def size
74
+ @size.to_i
75
+ end
76
+
77
+ def ts_bucket(ts)
78
+ ts / seconds * seconds
79
+ end
80
+
81
+ def next_ts_bucket(ts)
82
+ ts_bucket(ts) + seconds
83
+ end
84
+
85
+ def previous_ts_bucket(ts)
86
+ ts_bucket(ts) - seconds
87
+ end
88
+
89
+ def find(id)
90
+ mongo_result = mongo_coll.find_one({ :_id => id })
91
+ Stats.create_from_db(mongo_result)
92
+ end
93
+
94
+ def find_all_by_name(name)
95
+ mongo_result = mongo_coll.find({ :name => name })
96
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
97
+ end
98
+
99
+ def find_all_in_ts(ts)
100
+ mongo_result = mongo_coll.find({ :ts => ts_bucket(ts) })
101
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
102
+ end
103
+
104
+ def find_all_in_ts_by_name(ts, name)
105
+ mongo_result = mongo_coll.find({ :ts => ts_bucket(ts), :name => name })
106
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
107
+ end
108
+
109
+ def find_all_in_ts_range(previous_ts, current_ts)
110
+ mongo_result = mongo_coll.find({ :ts => { "$gte" => previous_ts, "$lt" => current_ts }}).to_a
111
+ mongo_result.inject([]) { |result, a| result << Stats.create_from_db(a) }
112
+ end
113
+
114
+ def stats_exist_in_previous_ts?(ts)
115
+ mongo_coll.find({ :ts => ts }).count > 0
116
+ end
117
+
118
+ def save(stats, ts)
119
+ stats.ts = ts_bucket(ts)
120
+ result = mongo_coll.insert(stats.attributes)
121
+ SimpleMetrics.logger.debug "SERVER: MongoDB - insert in #{name}: #{stats.inspect}, result: #{result}"
122
+ end
123
+
124
+ def mongo_coll
125
+ Mongo.collection(name)
126
+ end
127
+
128
+ def capped?
129
+ @capped == true
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,83 @@
1
+ # encoding: utf-8
2
+ require "socket"
3
+
4
+ module SimpleMetrics
5
+
6
+ class Client
7
+ VERSION = "0.0.1"
8
+
9
+ def initialize(host, port = 8125)
10
+ @host, @port = host, port
11
+ end
12
+
13
+ # send relative value
14
+ def increment(stat, sample_rate = 1)
15
+ count(stat, 1, sample_rate)
16
+ end
17
+
18
+ # send relative value
19
+ def decrement(stat, sample_rate = 1)
20
+ count(stat, -1, sample_rate)
21
+ end
22
+
23
+ # send relative value
24
+ def count(stat, count, sample_rate = 1)
25
+ send_data( stat, count, 'c', sample_rate)
26
+ end
27
+
28
+ # send absolute value
29
+ # TODO: check if this is actually supported by Statsd server
30
+ def gauge(stat, value)
31
+ send_data(stat, value, 'g')
32
+ end
33
+
34
+ # Sends a timing (in ms) (glork)
35
+ def timing(stat, ms, sample_rate = 1)
36
+ send_data(stat, ms, 'ms', sample_rate)
37
+ end
38
+
39
+ # Sends a timing (in ms) block based
40
+ def time(stat, sample_rate = 1, &block)
41
+ start = Time.now
42
+ result = block.call
43
+ timing(stat, ((Time.now - start) * 1000).round, sample_rate)
44
+ result
45
+ end
46
+
47
+ private
48
+
49
+ def sampled(sample_rate, &block)
50
+ if sample_rate < 1
51
+ block.call if rand <= sample_rate
52
+ else
53
+ block.call
54
+ end
55
+ end
56
+
57
+ def send_data(stat, delta, type, sample_rate = 1)
58
+ sampled(sample_rate) do
59
+ data = "#{stat}:#{delta}|#{type}" # TODO: check stat is valid
60
+ data << "|@#{sample_rate}" if sample_rate < 1
61
+ send_to_socket(data)
62
+ end
63
+ end
64
+
65
+ def send_to_socket(data)
66
+ logger.debug "SimpleMetrics Client send: #{data}"
67
+ socket.send(data, 0, @host, @port)
68
+ rescue Exception => e
69
+ puts e.backtrace
70
+ logger.error "SimpleMetrics Client error: #{e}"
71
+ end
72
+
73
+ def socket
74
+ @socket ||= UDPSocket.new
75
+ end
76
+
77
+ def logger
78
+ @logger ||= SimpleMetrics.logger
79
+ end
80
+
81
+ end
82
+
83
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ require "mongo"
3
+
4
+ module SimpleMetrics
5
+ module Mongo
6
+ extend self
7
+
8
+ def ensure_collections_exist
9
+ SimpleMetrics.logger.debug "SERVER: MongoDB - found following collections: #{db.collection_names.inspect}"
10
+ Bucket.all.each do |bucket|
11
+ unless db.collection_names.include?(bucket.name)
12
+ db.create_collection(bucket.name, :capped => bucket.capped, :size => bucket.size)
13
+ SimpleMetrics.logger.debug "SERVER: MongoDB - created collection #{bucket.name}, capped: #{bucket.capped}, size: #{bucket.size}"
14
+ end
15
+ db.collection(bucket.name).ensure_index([['ts', ::Mongo::ASCENDING]])
16
+ SimpleMetrics.logger.debug "SERVER: MongoDB - ensure index on column ts for collection #{bucket.name}"
17
+ end
18
+ end
19
+
20
+ def truncate_collections
21
+ Bucket.all.each do |bucket|
22
+ if db.collection_names.include?(bucket.name)
23
+ if bucket.capped?
24
+ collection(bucket.name).drop # capped collections can't remove elements, drop it instead
25
+ else
26
+ collection(bucket.name).remove
27
+ end
28
+ SimpleMetrics.logger.debug "SERVER: MongoDB - truncated collection #{bucket.name}"
29
+ end
30
+ end
31
+ end
32
+
33
+ @@collection = {}
34
+ def collection(name)
35
+ @@collection[name] ||= db.collection(name)
36
+ end
37
+
38
+ def connection
39
+ @@connection ||= ::Mongo::Connection.new(SimpleMetrics.db_config[:host])
40
+ end
41
+
42
+ def db
43
+ @@db ||= connection.db(SimpleMetrics.db_config[:db_name], SimpleMetrics.db_config[:options])
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+ require "eventmachine"
3
+
4
+ module SimpleMetrics
5
+
6
+ module ClientHandler
7
+
8
+ @@stats = []
9
+
10
+ class << self
11
+ def get_and_clear_stats
12
+ stats = @@stats.dup
13
+ @@stats = []
14
+ stats
15
+ end
16
+ end
17
+
18
+ def stats
19
+ @@stats
20
+ end
21
+
22
+ def post_init
23
+ SimpleMetrics.logger.info "ClientHandler entering post_init"
24
+ end
25
+
26
+ def receive_data(data)
27
+ SimpleMetrics.logger.debug "received_data: #{data.inspect}"
28
+
29
+ @@stats ||= []
30
+ @@stats << Stats.parse(data)
31
+ rescue Stats::ParserError => e
32
+ SimpleMetrics.logger.debug "Invalid Data skipped: #{data}"
33
+ end
34
+ end
35
+
36
+ class Server
37
+
38
+ attr_reader :db, :connection
39
+
40
+ def start
41
+ SimpleMetrics.logger.info "SERVER: starting up on #{SimpleMetrics.config[:host]}:#{SimpleMetrics.config[:port]}..."
42
+
43
+ Mongo.ensure_collections_exist
44
+
45
+ EM.run do
46
+ EM.open_datagram_socket(SimpleMetrics.config[:host], SimpleMetrics.config[:port], SimpleMetrics::ClientHandler) do |con|
47
+ EventMachine::add_periodic_timer(SimpleMetrics.config[:flush_interval]) do
48
+ SimpleMetrics.logger.debug "SERVER: period timer triggered after #{SimpleMetrics.config[:flush_interval]} seconds"
49
+
50
+ EM.defer { Bucket.flush_stats(ClientHandler.get_and_clear_stats) }
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def stop
57
+ SimpleMetrics.logger.info "EventMachine stop"
58
+ EM.stop
59
+ end
60
+
61
+ def to_s
62
+ "#{SimpleMetrics.config[:host]}:#{SimpleMetrics.config[:port]}"
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+
4
+ class Stats
5
+
6
+ class NonMatchingTypesError < Exception; end
7
+ class ParserError < Exception; end
8
+
9
+ # examples:
10
+ # com.example.test1:1|c
11
+ # com.example.test2:-1|c
12
+ # com.example.test2:50|g
13
+ # com.example.test3:5|c|@0.1
14
+ # com.example.test4:44|ms
15
+ REGEXP = /^([\d\w_.]*):(-?[\d]*)\|(c|g|ms){1}(\|@([.\d]+))?$/i
16
+
17
+ class << self
18
+
19
+ def parse(str)
20
+ if str =~ REGEXP
21
+ name, value, type, sample_rate = $1, $2, $3, $5
22
+ if type == "ms"
23
+ # TODO: implement sample_rate handling
24
+ create_timing(:name => name, :value => value)
25
+ elsif type == "g"
26
+ create_gauge(:name => name, :value => (value.to_i || 1) * (1.0 / (sample_rate || 1).to_f) )
27
+ elsif type == "c"
28
+ create_counter(:name => name, :value => (value.to_i || 1) * (1.0 / (sample_rate || 1).to_f) )
29
+ end
30
+ else
31
+ raise ParserError, "Parser Error - Invalid Stat: #{str}"
32
+ end
33
+ end
34
+
35
+ def create_counter(attributes)
36
+ Stats.new(attributes.merge(:type => 'c'))
37
+ end
38
+
39
+ def create_gauge(attributes)
40
+ Stats.new(attributes.merge(:type => 'g'))
41
+ end
42
+
43
+ def create_timing(attributes)
44
+ Stats.new(attributes.merge(:type => 'ms'))
45
+ end
46
+
47
+ def aggregate(stats_array)
48
+ raise NonMatchingTypesError unless stats_array.group_by { |stats| stats.type }.size == 1
49
+
50
+ result_stat = stats_array.first.dup
51
+ result_stat.value = stats_array.map { |stats| stats.value }.inject(0) { |result, value| result += value }
52
+ result_stat
53
+ end
54
+
55
+ def create_from_db(attributes)
56
+ Stats.new(:name => attributes["name"], :value => attributes["value"], :ts => attributes["ts"], :type => attributes["type"])
57
+ end
58
+ end
59
+
60
+ attr_accessor :name, :ts, :type, :value
61
+
62
+ def initialize(attributes)
63
+ @name = attributes[:name]
64
+ @value = attributes[:value]
65
+ @ts = attributes[:ts]
66
+ @type = attributes[:type]
67
+ end
68
+
69
+ def counter?
70
+ type == 'c'
71
+ end
72
+
73
+ def gauge?
74
+ type == 'g'
75
+ end
76
+
77
+ def timing?
78
+ type == 'ms'
79
+ end
80
+
81
+ def timestamp
82
+ ts
83
+ end
84
+
85
+ def value
86
+ @value.to_i
87
+ end
88
+
89
+ def attributes
90
+ {
91
+ :name => name,
92
+ :value => value,
93
+ :ts => ts,
94
+ :type => type
95
+ }
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ module SimpleMetrics
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "simple_metrics/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "simple_metrics"
7
+ s.version = SimpleMetrics::VERSION
8
+ s.authors = ["Frederik Dietz"]
9
+ s.email = ["fdietz@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{SimpleMetrics}
12
+ s.description = %q{SimpleMetrics}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency "rake"
20
+ s.add_development_dependency "rspec"
21
+ s.add_development_dependency "rr"
22
+
23
+ s.add_dependency "eventmachine"
24
+ s.add_dependency "daemons"
25
+ s.add_dependency "mongo"
26
+ s.add_dependency "bson_ext"
27
+ end
@@ -0,0 +1,181 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ module SimpleMetrics
5
+
6
+ describe Bucket do
7
+
8
+ let(:bucket) do
9
+ Bucket.first
10
+ end
11
+
12
+ let(:sec) do
13
+ bucket.seconds
14
+ end
15
+
16
+ let(:ts) do
17
+ Time.now.utc.to_i
18
+ end
19
+
20
+ describe "#ts_bucket" do
21
+ it "calculates timestamp for current bucket" do
22
+ bucket.ts_bucket(ts).should == ts/sec*sec
23
+ end
24
+ end
25
+
26
+ describe "#next_ts_bucket" do
27
+ it "calculates timestamp for next bucket" do
28
+ bucket.next_ts_bucket(ts).should == ts/sec*sec+sec
29
+ end
30
+ end
31
+
32
+ describe "#previous_ts_bucket" do
33
+ it "calculates timestamp for previous bucket" do
34
+ bucket.previous_ts_bucket(ts).should == ts/sec*sec-sec
35
+ end
36
+ end
37
+
38
+ describe "#save" do
39
+ before do
40
+ Mongo.truncate_collections
41
+ Mongo.ensure_collections_exist
42
+ bucket.save(stats, ts)
43
+ end
44
+
45
+ let(:stats) do
46
+ Stats.create_counter(:name => "key1", :value => 5)
47
+ end
48
+
49
+ it "saves given data in bucket" do
50
+ results = bucket.find_all_by_name("key1")
51
+ results.should have(1).item
52
+ result = results.first
53
+ result.name.should == stats.name
54
+ result.value.should == stats.value
55
+ result.type.should == stats.type
56
+ end
57
+
58
+ it "saves data in correct timestamp" do
59
+ result = bucket.find_all_by_name("key1").first
60
+ result.ts.should == ts/sec*sec
61
+ end
62
+
63
+ end # describe "#save" do
64
+
65
+ describe "finder methods" do
66
+
67
+ before do
68
+ Mongo.truncate_collections
69
+ Mongo.ensure_collections_exist
70
+ end
71
+
72
+ describe "#find_all_by_name" do
73
+ it "returns all stats for given name" do
74
+ stats_same1 = Stats.create_counter(:name => "key1", :value => 5)
75
+ stats_same2 = Stats.create_counter(:name => "key1", :value => 3)
76
+ stats_different = Stats.create_counter(:name => "key2", :value => 3)
77
+
78
+ bucket.save(stats_same1, ts)
79
+ bucket.save(stats_same2, ts)
80
+ bucket.save(stats_different, ts)
81
+
82
+ results = bucket.find_all_by_name("key1")
83
+ results.should have(2).items
84
+ results.first.name.should == stats_same1.name
85
+ end
86
+ end
87
+
88
+ describe "#find_all_in_ts" do
89
+ it "returns all stats in given timestamp" do
90
+ stats1 = Stats.create_counter(:name => "key1", :value => 5)
91
+ stats2 = Stats.create_counter(:name => "key2", :value => 3)
92
+
93
+ bucket.save(stats1, ts)
94
+ bucket.save(stats2, bucket.next_ts_bucket(ts))
95
+
96
+ result1 = bucket.find_all_in_ts(ts).first
97
+ result1.name.should == stats1.name
98
+ result1.value.should == stats1.value
99
+
100
+ result2 = bucket.find_all_in_ts(bucket.next_ts_bucket(ts)).first
101
+ result2.name.should == stats2.name
102
+ result2.value.should == stats2.value
103
+ end
104
+ end
105
+
106
+ describe "#find_all_in_ts_by_name" do
107
+ it "returns all stats for given name and timestamp" do
108
+ stats1a = Stats.create_counter(:name => "key1", :value => 5)
109
+ stats1b = Stats.create_counter(:name => "key1", :value => 7)
110
+ stats2 = Stats.create_counter(:name => "key2", :value => 7)
111
+ stats1_different_ts = Stats.create_counter(:name => "key1", :value => 3)
112
+
113
+ bucket.save(stats1a, ts)
114
+ bucket.save(stats1b, ts)
115
+ bucket.save(stats2, ts)
116
+ bucket.save(stats1_different_ts, bucket.next_ts_bucket(ts))
117
+
118
+ results = bucket.find_all_in_ts_by_name(ts, "key1")
119
+ results.should have(2).items
120
+ results.first.name.should == "key1"
121
+ results.last.name.should == "key1"
122
+ end
123
+ end
124
+
125
+ end # describe "finder methods"
126
+
127
+ describe "#aggregate_all" do
128
+ before do
129
+ Mongo.truncate_collections
130
+ Mongo.ensure_collections_exist
131
+ end
132
+
133
+ it "aggregates all stats" do
134
+ stats1a = Stats.create_counter(:name => "key1", :value => 5)
135
+ stats1b = Stats.create_counter(:name => "key1", :value => 7)
136
+ stats2 = Stats.create_counter(:name => "key2", :value => 3)
137
+
138
+ bucket2 = Bucket[1]
139
+ ts_at_insert = bucket2.previous_ts_bucket(ts)
140
+ bucket.save(stats1a, ts_at_insert)
141
+ bucket.save(stats1b, ts_at_insert)
142
+ bucket.save(stats2, ts_at_insert)
143
+
144
+ Bucket.aggregate_all(ts)
145
+
146
+ results = bucket2.find_all_in_ts(ts_at_insert)
147
+ results.should have(2).items
148
+
149
+ key1_result = results.find {|stat| stat.name == "key1"}
150
+ key1_result.value.should == 12
151
+ key1_result.should be_counter
152
+
153
+ key2_result = results.find {|stat| stat.name == "key2"}
154
+ key2_result.value.should == 3
155
+ key2_result.should be_counter
156
+ end
157
+ end # describe "#aggregate_all"
158
+
159
+ describe "#flush_stats" do
160
+ before do
161
+ stats1 = Stats.create_counter(:name => "key1", :value => 5)
162
+ stats2 = Stats.create_counter(:name => "key1", :value => 7)
163
+ stats3 = Stats.create_counter(:name => "key2", :value => 3)
164
+ @stats = [stats1, stats2, stats3]
165
+ end
166
+
167
+ it "saves all stats in finest/first bucket" do
168
+ Bucket.flush_stats(@stats)
169
+
170
+ results = bucket.find_all_in_ts(ts)
171
+ results.should have(3).items
172
+ end
173
+
174
+ it "calls aggregate_all afterwards" do
175
+ mock(Bucket).aggregate_all(ts)
176
+ Bucket.flush_stats(@stats)
177
+ end
178
+ end # describe "#flush_stats"
179
+
180
+ end
181
+ end
@@ -0,0 +1,10 @@
1
+ require "simple_metrics"
2
+
3
+ RSpec.configure do |config|
4
+ config.mock_with :rr
5
+ end
6
+
7
+ SimpleMetrics.logger = Logger.new('/dev/null')
8
+ SimpleMetrics.db_config = { :host => 'localhost', :prefix => 'test' }
9
+
10
+
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ module SimpleMetrics
5
+
6
+ describe Bucket do
7
+
8
+ describe "#parse" do
9
+
10
+ it "parses increment counter" do
11
+ stats = Stats.parse("com.example.test1:1|c")
12
+ stats.name.should == "com.example.test1"
13
+ stats.value.should == 1
14
+ stats.should be_counter
15
+ end
16
+
17
+ it "parses decrement counter" do
18
+ stats = Stats.parse("com.example.test1:-1|c")
19
+ stats.name.should == "com.example.test1"
20
+ stats.value.should == -1
21
+ stats.should be_counter
22
+ end
23
+
24
+ it "parses counter with sample rate" do
25
+ stats = Stats.parse("com.example.test2:5|c|@0.1")
26
+ stats.name.should == "com.example.test2"
27
+ stats.value.should == 50
28
+ stats.should be_counter
29
+ end
30
+
31
+ it "parses increment gauge" do
32
+ stats = Stats.parse("com.example.test3:5|g")
33
+ stats.name.should == "com.example.test3"
34
+ stats.value.should == 5
35
+ stats.should be_gauge
36
+ end
37
+
38
+ it "parses increment gauge with sample rate" do
39
+ stats = Stats.parse("com.example.test3:5|g|@0.1")
40
+ stats.name.should == "com.example.test3"
41
+ stats.value.should == 50
42
+ stats.should be_gauge
43
+ end
44
+
45
+ it "parses increment timing" do
46
+ stats = Stats.parse("com.example.test4:44|ms")
47
+ stats.name.should == "com.example.test4"
48
+ stats.value.should == 44
49
+ stats.should be_timing
50
+ end
51
+
52
+ it "parses increment timing with sample rate" do
53
+ end
54
+ end
55
+
56
+
57
+ describe "create_counter" do
58
+ end
59
+ end
60
+
61
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_metrics
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Frederik Dietz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70244480135940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70244480135940
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70244480135460 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70244480135460
36
+ - !ruby/object:Gem::Dependency
37
+ name: rr
38
+ requirement: &70244480135020 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70244480135020
47
+ - !ruby/object:Gem::Dependency
48
+ name: eventmachine
49
+ requirement: &70244480134600 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70244480134600
58
+ - !ruby/object:Gem::Dependency
59
+ name: daemons
60
+ requirement: &70244480134180 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70244480134180
69
+ - !ruby/object:Gem::Dependency
70
+ name: mongo
71
+ requirement: &70244480133740 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *70244480133740
80
+ - !ruby/object:Gem::Dependency
81
+ name: bson_ext
82
+ requirement: &70244480133300 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *70244480133300
91
+ description: SimpleMetrics
92
+ email:
93
+ - fdietz@gmail.com
94
+ executables:
95
+ - simple_metrics_client
96
+ - simple_metrics_server
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - .gitignore
101
+ - .rvmrc
102
+ - Gemfile
103
+ - README.markdown
104
+ - Rakefile
105
+ - bin/simple_metrics_client
106
+ - bin/simple_metrics_server
107
+ - example/increment.rb
108
+ - lib/simple_metrics.rb
109
+ - lib/simple_metrics/bucket.rb
110
+ - lib/simple_metrics/client.rb
111
+ - lib/simple_metrics/mongo.rb
112
+ - lib/simple_metrics/server.rb
113
+ - lib/simple_metrics/stats.rb
114
+ - lib/simple_metrics/version.rb
115
+ - simple_metrics.gemspec
116
+ - spec/bucket_spec.rb
117
+ - spec/spec_helper.rb
118
+ - spec/stats_spec.rb
119
+ homepage: ''
120
+ licenses: []
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.15
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: SimpleMetrics
143
+ test_files:
144
+ - spec/bucket_spec.rb
145
+ - spec/spec_helper.rb
146
+ - spec/stats_spec.rb