simple_metrics 0.0.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.
- data/.gitignore +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.markdown +127 -0
- data/Rakefile +8 -0
- data/bin/simple_metrics_client +64 -0
- data/bin/simple_metrics_server +18 -0
- data/example/increment.rb +8 -0
- data/lib/simple_metrics.rb +97 -0
- data/lib/simple_metrics/bucket.rb +133 -0
- data/lib/simple_metrics/client.rb +83 -0
- data/lib/simple_metrics/mongo.rb +47 -0
- data/lib/simple_metrics/server.rb +66 -0
- data/lib/simple_metrics/stats.rb +99 -0
- data/lib/simple_metrics/version.rb +4 -0
- data/simple_metrics.gemspec +27 -0
- data/spec/bucket_spec.rb +181 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/stats_spec.rb +61 -0
- metadata +146 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use --create 1.9.3@simple_metrics
|
data/Gemfile
ADDED
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,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,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,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
|
data/spec/bucket_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
data/spec/stats_spec.rb
ADDED
@@ -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
|