rredis 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +23 -0
- data/.gemtest +0 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +68 -0
- data/History.txt +6 -0
- data/Manifest.txt +40 -0
- data/README.md +143 -0
- data/Rakefile +34 -0
- data/benchmark/default.rb +62 -0
- data/benchmark/pipeline.rb +57 -0
- data/bin/rredis_server +7 -0
- data/config.ru +5 -0
- data/lib/lua/get.lua +74 -0
- data/lib/lua/store.lua +170 -0
- data/lib/rredis.rb +61 -0
- data/lib/rredis/server.rb +63 -0
- data/lib/rredis/web/assets/javascripts/application.js +3 -0
- data/lib/rredis/web/assets/javascripts/main.js +97 -0
- data/lib/rredis/web/assets/javascripts/vendor/bootstrap.js +1726 -0
- data/lib/rredis/web/assets/javascripts/vendor/bootstrap.min.js +6 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/highcharts.js +202 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/modules/canvas-tools.js +133 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/modules/exporting.js +23 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/themes/dark-blue.js +263 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/themes/dark-green.js +263 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/themes/gray.js +262 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/themes/grid.js +95 -0
- data/lib/rredis/web/assets/javascripts/vendor/highcharts/themes/skies.js +89 -0
- data/lib/rredis/web/assets/javascripts/vendor/jquery.js +4 -0
- data/lib/rredis/web/assets/stylesheets/application.css +10 -0
- data/lib/rredis/web/assets/stylesheets/vendor/bootstrap-responsive.css +686 -0
- data/lib/rredis/web/assets/stylesheets/vendor/bootstrap.css +3990 -0
- data/lib/rredis/web/images/glyphicons-halflings-white.png +0 -0
- data/lib/rredis/web/images/glyphicons-halflings.png +0 -0
- data/lib/rredis/web/views/index.slim +7 -0
- data/lib/rredis/web/views/layout.slim +26 -0
- data/spec/rredis/get_spec.rb +47 -0
- data/spec/rredis/store_spec.rb +159 -0
- data/spec/spec_helper.rb +21 -0
- metadata +124 -0
data/bin/rredis_server
ADDED
data/config.ru
ADDED
data/lib/lua/get.lua
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
local key = "rrd_" .. KEYS[1]
|
2
|
+
local function get_value(value)
|
3
|
+
local n = string.find(value, '_');
|
4
|
+
if n then
|
5
|
+
return string.sub(value, n+1, -1)
|
6
|
+
else
|
7
|
+
return tonumber(value)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
local function get_data(data, offset)
|
11
|
+
local values = {}
|
12
|
+
local timestamps = {}
|
13
|
+
for i, d in ipairs(data) do
|
14
|
+
if i % 2 == 0 then
|
15
|
+
table.insert(timestamps, get_value(d)-offset)
|
16
|
+
else
|
17
|
+
table.insert(values, get_value(d))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
return {timestamps, values}
|
21
|
+
end
|
22
|
+
|
23
|
+
-- Load the config
|
24
|
+
local config = cjson.decode(redis.call("get", key .. "_config"))
|
25
|
+
-- If we do not have a config we can assume that we also have no data to return
|
26
|
+
if not config then
|
27
|
+
return {{}, {}}
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
local start = tonumber(ARGV[1])
|
32
|
+
local stop = tonumber(ARGV[2])
|
33
|
+
local timespan = stop-start
|
34
|
+
|
35
|
+
local method
|
36
|
+
if ARGV[3] == "" then
|
37
|
+
method = 'average'
|
38
|
+
else
|
39
|
+
method = ARGV[3]
|
40
|
+
end
|
41
|
+
|
42
|
+
local higher_key = key..'_'..config["steps"]
|
43
|
+
|
44
|
+
local oldest = redis.call("ZRANGE", higher_key, 0, 0, 'WITHSCORES')
|
45
|
+
if not oldest then
|
46
|
+
return {{}, {}}
|
47
|
+
end
|
48
|
+
|
49
|
+
local oldest = tonumber(oldest[2])
|
50
|
+
|
51
|
+
if timespan <= config.steps*config.rows and timespan/config.steps < 650 then
|
52
|
+
local data = redis.call("ZRANGEBYSCORE", higher_key, start, stop, 'WITHSCORES' )
|
53
|
+
return get_data(data, 0)
|
54
|
+
end
|
55
|
+
|
56
|
+
if config["rra"] then
|
57
|
+
local higher = config
|
58
|
+
local rra_count = table.getn(config.rra)
|
59
|
+
local rra_key, oldest
|
60
|
+
for i, rra in ipairs(config["rra"]) do
|
61
|
+
-- Get all entries from the higher precision bucket
|
62
|
+
rra_key = key..'_'..rra["steps"]..'_'..method
|
63
|
+
oldest = redis.call("ZRANGE", rra_key, 0, 0, 'WITHSCORES')
|
64
|
+
if oldest == {} then
|
65
|
+
return {{}, {}}
|
66
|
+
end
|
67
|
+
|
68
|
+
oldest = tonumber(oldest[2])
|
69
|
+
if (timespan <= rra.steps*rra.rows and timespan/rra.steps < 650) or i == rra_count then
|
70
|
+
local data = redis.call("ZRANGEBYSCORE", rra_key, start, stop, 'WITHSCORES' )
|
71
|
+
return get_data(data, rra.steps/2)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/lua/store.lua
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
local config, oldest
|
2
|
+
local key = "rrd_" .. KEYS[1]
|
3
|
+
-- Check if there is a config for this metric
|
4
|
+
if redis.call("exists",key .. "_config") == 0 then
|
5
|
+
-- Create a config based on the default one
|
6
|
+
config = redis.call("get", "rrd_default_config")
|
7
|
+
redis.call("set", key .. "_config", config)
|
8
|
+
redis.call("sadd", "rrd_metrics_set", KEYS[1])
|
9
|
+
end
|
10
|
+
|
11
|
+
-- Load the config
|
12
|
+
config = cjson.decode(redis.call("get", key .. "_config"))
|
13
|
+
|
14
|
+
local timestamp = tonumber(ARGV[2])
|
15
|
+
|
16
|
+
-- If steps are defined for the native resolution, we will round the timestamp
|
17
|
+
local higher_key = key..'_'..config["steps"]
|
18
|
+
if (timestamp % config["steps"]) / config["steps"] <= 0.5 then
|
19
|
+
timestamp = math.floor(timestamp - (timestamp % config["steps"]))
|
20
|
+
else
|
21
|
+
timestamp = math.floor(timestamp - (timestamp % config["steps"])) + config["steps"]
|
22
|
+
end
|
23
|
+
|
24
|
+
-- Get the amount of entries in this bucket
|
25
|
+
local count = redis.call("ZCARD", higher_key)
|
26
|
+
|
27
|
+
-- We need to make sure that to old entries are not added to the bucked
|
28
|
+
if count+1 == config["rows"] then
|
29
|
+
oldest = tonumber(redis.call("ZRANGE", higher_key, 0, 0, 'WITHSCORES')[2])
|
30
|
+
if timestamp < oldest then
|
31
|
+
-- We cannot add older entries
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
-- We may be updating an old entry, which we want to delete first
|
37
|
+
redis.call("ZREMRANGEBYSCORE", higher_key, timestamp, timestamp)
|
38
|
+
|
39
|
+
-- Add the new entry to the bucked
|
40
|
+
redis.call("ZADD", higher_key, timestamp, timestamp..'_'..ARGV[1])
|
41
|
+
|
42
|
+
if count+1 > config["rows"] then
|
43
|
+
-- We have too many entries in this bucket - remove the oldest
|
44
|
+
redis.call("ZREMRANGEBYRANK", higher_key, 0, (count-config.rows))
|
45
|
+
end
|
46
|
+
|
47
|
+
local get_value = function(value) return tonumber(string.sub(value, string.find(value, '_')+1, -1)) end
|
48
|
+
|
49
|
+
if config["rra"] then
|
50
|
+
local higher = config
|
51
|
+
local lower_start, higher_ts, value, rra_key, last_rra, n
|
52
|
+
|
53
|
+
for j, rra in ipairs(config["rra"]) do
|
54
|
+
-- Calculate the timestamp for the aggregation
|
55
|
+
local steps = rra.steps
|
56
|
+
local rest = (timestamp % rra.steps)
|
57
|
+
if rest == 0 then
|
58
|
+
lower_start = timestamp - rra.steps + higher["steps"]
|
59
|
+
higher_ts = timestamp
|
60
|
+
else
|
61
|
+
lower_start = timestamp - rest + higher["steps"]
|
62
|
+
higher_ts = timestamp - rest + rra.steps
|
63
|
+
end
|
64
|
+
|
65
|
+
if config["aggregations"] then
|
66
|
+
local a = {}
|
67
|
+
-- Shortcut, if we are in the first rra we can calculate the aggregations faster
|
68
|
+
if higher.steps == config.steps then
|
69
|
+
-- Get all entries from the higher precision bucket
|
70
|
+
local data = redis.call( "ZRANGEBYSCORE", higher_key, lower_start, lower_start+rra.steps)
|
71
|
+
-- If steps are defined for the native resolution, only proceed if we have enough entries
|
72
|
+
if not (table.getn(data) > (rra["steps"]/higher["steps"]*rra["xff"])) then
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
|
76
|
+
a.sum = 0
|
77
|
+
a.min = 4503599627370496
|
78
|
+
a.max = -4503599627370496
|
79
|
+
for i, value in ipairs(data) do
|
80
|
+
value = get_value(value)
|
81
|
+
a.sum = a.sum + value
|
82
|
+
if value < a.min then
|
83
|
+
a.min = value
|
84
|
+
end
|
85
|
+
if value > a.max then
|
86
|
+
a.max = value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
a.avg = a.sum / table.getn(data)
|
90
|
+
else
|
91
|
+
-- For every other rra we need to fetch the matching data
|
92
|
+
for i, method in ipairs(config["aggregations"]) do
|
93
|
+
-- Get all entries from the higher precision bucket
|
94
|
+
local data = redis.call( "ZRANGEBYSCORE", higher_key..'_'..method, lower_start, lower_start+rra.steps)
|
95
|
+
-- If steps are defined for the native resolution, only proceed if we have enough entries
|
96
|
+
if not (table.getn(data) > (rra["steps"]/higher["steps"]*rra["xff"])) then
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
if method == "average" then
|
101
|
+
a.sum = 0
|
102
|
+
|
103
|
+
for i, value in ipairs(data) do
|
104
|
+
a.sum = a.sum + get_value(value)
|
105
|
+
end
|
106
|
+
a.avg = a.sum / table.getn(data)
|
107
|
+
elseif method == "sum" then
|
108
|
+
a.sum = 0
|
109
|
+
|
110
|
+
for i, v in ipairs(data) do
|
111
|
+
a.sum = a.sum + get_value(v)
|
112
|
+
end
|
113
|
+
elseif method == "min" then
|
114
|
+
a.min = 4503599627370496
|
115
|
+
for i, value in ipairs(data) do
|
116
|
+
n = get_value(value)
|
117
|
+
if n < a.min then
|
118
|
+
a.min = n
|
119
|
+
end
|
120
|
+
end
|
121
|
+
elseif method == "max" then
|
122
|
+
a.max = -4503599627370496
|
123
|
+
for i, value in ipairs(data) do
|
124
|
+
n = get_value(value)
|
125
|
+
if n > a.max then
|
126
|
+
a.max = n
|
127
|
+
end
|
128
|
+
end
|
129
|
+
else
|
130
|
+
redis.log(redis.LOG_ERROR, "Not implemented")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
-- Update the buckets
|
136
|
+
for i, method in ipairs(config["aggregations"]) do
|
137
|
+
rra_key = key..'_'..rra["steps"]..'_'..method
|
138
|
+
-- We may be updating an old entry, which we want to delete first
|
139
|
+
redis.call("ZREMRANGEBYSCORE", rra_key, higher_ts, higher_ts)
|
140
|
+
-- Get the amount of entries in this bucket
|
141
|
+
count = redis.call("ZCARD", rra_key)
|
142
|
+
|
143
|
+
if count > higher["rows"] then
|
144
|
+
-- We have too many entries in this bucket - remove the oldest
|
145
|
+
oldest = redis.call("ZRANGE", rra_key, 0, 0)
|
146
|
+
redis.call("ZREM", rra_key, oldest[1])
|
147
|
+
end
|
148
|
+
|
149
|
+
-- Add the new entry to the bucked
|
150
|
+
if method == "average" then
|
151
|
+
redis.call("ZADD", rra_key, higher_ts, higher_ts..'_'..a.avg)
|
152
|
+
elseif method == "sum" then
|
153
|
+
redis.call("ZADD", rra_key, higher_ts, higher_ts..'_'..a.sum)
|
154
|
+
elseif method == "min" then
|
155
|
+
redis.call("ZADD", rra_key, higher_ts, higher_ts..'_'..a.min)
|
156
|
+
elseif method == "max" then
|
157
|
+
redis.call("ZADD", rra_key, higher_ts, higher_ts..'_'..a.max)
|
158
|
+
end
|
159
|
+
|
160
|
+
last_rra = rra
|
161
|
+
end
|
162
|
+
|
163
|
+
-- Set the higher precision bucket to the current rra
|
164
|
+
higher = last_rra
|
165
|
+
higher_key = key..'_'..higher["steps"]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
return true
|
data/lib/rredis.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
class RReDis
|
5
|
+
VERSION = '0.1.0'
|
6
|
+
attr_reader :default_config
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@r = Redis.new
|
10
|
+
@default_config = {:steps=>10, :rows=>8640,
|
11
|
+
:aggregations=>["average", "min", "max"],
|
12
|
+
:rra => [ {:steps=>60, :rows=>10080, :xff=>0.5},
|
13
|
+
{:steps=>900, :rows=>2976, :xff=>0.5},
|
14
|
+
{:steps=>3600, :rows=>8760, :xff=>0.5}]}
|
15
|
+
self.default_config = @default_config
|
16
|
+
|
17
|
+
|
18
|
+
@script_cache = {}
|
19
|
+
@sha_cache = {}
|
20
|
+
Dir.glob(File.join(File.dirname(__FILE__), '/lua/*.lua')).each do |file|
|
21
|
+
name = File.basename(file, File.extname(file))
|
22
|
+
@script_cache[name.to_sym] = File.open(file).read
|
23
|
+
@sha_cache[name.to_sym] = @r.script(:load, @script_cache[name.to_sym])
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_config=(config)
|
29
|
+
self.config('default', config, false)
|
30
|
+
end
|
31
|
+
|
32
|
+
def config(metric, config, set_metric=true)
|
33
|
+
config[:rra] = config[:rra].sort {|a,b| a[:steps] <=> b[:steps] } if config[:rra]
|
34
|
+
@r.set("rrd_#{metric}_config", JSON.dump(config))
|
35
|
+
@r.sadd "rrd_metrics_set", metric if set_metric
|
36
|
+
end
|
37
|
+
|
38
|
+
def store(metric, timestamp, value=nil)
|
39
|
+
if value.nil?
|
40
|
+
value = timestamp
|
41
|
+
timestamp = Time.now
|
42
|
+
end
|
43
|
+
@r.evalsha(@sha_cache[:store], :keys => [metric], :argv => [value, timestamp.to_f])
|
44
|
+
end
|
45
|
+
|
46
|
+
def get(metric, start, stop, method = nil)
|
47
|
+
resp = @r.evalsha(@sha_cache[:get], :keys => [metric], :argv => [start.to_i, stop.to_i, method])
|
48
|
+
if resp
|
49
|
+
resp[1].collect! { |x| x.to_f}
|
50
|
+
resp
|
51
|
+
else
|
52
|
+
[[],[]]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def pipelined(&block)
|
57
|
+
@r.pipelined do
|
58
|
+
yield self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'slim'
|
3
|
+
require 'sprockets'
|
4
|
+
|
5
|
+
module RReDisServer
|
6
|
+
class SprocketsMiddleware
|
7
|
+
def initialize(app, options={})
|
8
|
+
@app = app
|
9
|
+
@root = options[:root]
|
10
|
+
path = options[:path] || 'assets'
|
11
|
+
@matcher = /^\/#{path}\/*/
|
12
|
+
@environment = ::Sprockets::Environment.new(@root)
|
13
|
+
@environment.append_path 'assets/javascripts'
|
14
|
+
@environment.append_path 'assets/javascripts/vendor'
|
15
|
+
@environment.append_path 'assets/stylesheets'
|
16
|
+
@environment.append_path 'assets/stylesheets/vendor'
|
17
|
+
@environment.append_path 'assets/images'
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
return [301, { 'Location' => "#{env['SCRIPT_NAME']}/" }, []] if env['SCRIPT_NAME'] == env['REQUEST_PATH']
|
22
|
+
|
23
|
+
return @app.call(env) unless @matcher =~ env["PATH_INFO"]
|
24
|
+
env['PATH_INFO'].sub!(@matcher,'')
|
25
|
+
@environment.call(env)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Web < Sinatra::Base
|
30
|
+
dir = File.expand_path(File.dirname(__FILE__) + "/web")
|
31
|
+
set :views, "#{dir}/views"
|
32
|
+
set :root, "#{dir}/assets"
|
33
|
+
set :slim, :pretty => true
|
34
|
+
use SprocketsMiddleware, :root => dir
|
35
|
+
|
36
|
+
|
37
|
+
helpers do
|
38
|
+
def root_path
|
39
|
+
"#{env['SCRIPT_NAME']}/"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
super
|
45
|
+
@r = Redis.new
|
46
|
+
@rrd = RReDis.new
|
47
|
+
end
|
48
|
+
|
49
|
+
get "/" do
|
50
|
+
@metrics = @r.smembers("rrd_metrics_set")
|
51
|
+
slim :index
|
52
|
+
end
|
53
|
+
|
54
|
+
get "/get" do
|
55
|
+
data = {}
|
56
|
+
params['aggregations'].split(',').each do |method|
|
57
|
+
data[method] = @rrd.get(params['metric'], Time.now-params['timespan'].to_i, Time.now, method)
|
58
|
+
end
|
59
|
+
JSON.dump(data)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
function RReDis() {
|
2
|
+
that = this;
|
3
|
+
this.charts = {};
|
4
|
+
this.config = [{name: "10 minutes", steps: 60*10}, {name: "1 hour", steps: 3600}, {name: "12 hours", steps: 3600*12}, {name: "1 day", steps: 3600*24},{name: "30 days", steps: 3600*24*31}];
|
5
|
+
$("#metrics").change(function(value) {
|
6
|
+
that.metric = $(this).val();
|
7
|
+
that.createGraphs(that.metric);
|
8
|
+
});
|
9
|
+
this.createGraphs = function(metric) {
|
10
|
+
var now = new Date().getTime()/1000;
|
11
|
+
$("#charts").html("")
|
12
|
+
var d = new Date();
|
13
|
+
for(var i = 0; i < this.config.length; i++) {
|
14
|
+
var config = this.config[i];
|
15
|
+
$("#charts").append('<div id="'+metric+config.steps+'" class="chart"></div>');
|
16
|
+
this.charts[metric+config.steps] = new Highcharts.Chart({
|
17
|
+
chart: {
|
18
|
+
renderTo: metric+config.steps,
|
19
|
+
defaultSeriesType: 'line',
|
20
|
+
events: {
|
21
|
+
load: this.requestData(metric, config)
|
22
|
+
},
|
23
|
+
zoomType: 'xy'
|
24
|
+
|
25
|
+
},
|
26
|
+
tooltip: {
|
27
|
+
xDateFormat: '%Y-%m-%d %H:%M:%S',
|
28
|
+
shared: true
|
29
|
+
},
|
30
|
+
|
31
|
+
title: {
|
32
|
+
text: config.name
|
33
|
+
},
|
34
|
+
xAxis: {
|
35
|
+
type: 'datetime',
|
36
|
+
tickPixelInterval : 50,
|
37
|
+
labels: {
|
38
|
+
rotation: -45,
|
39
|
+
align: 'right',
|
40
|
+
},
|
41
|
+
maxZoom: 20 * 1000,
|
42
|
+
max: d.getTime(),
|
43
|
+
min: d.getTime()-(config.steps*1000)
|
44
|
+
},
|
45
|
+
yAxis: {
|
46
|
+
minPadding: 0.2,
|
47
|
+
maxPadding: 0.2,
|
48
|
+
title: {
|
49
|
+
text: 'Value',
|
50
|
+
margin: 80
|
51
|
+
}
|
52
|
+
},
|
53
|
+
plotOptions: {
|
54
|
+
line: {
|
55
|
+
lineWidth: 1,
|
56
|
+
marker: {
|
57
|
+
enabled: false,
|
58
|
+
states: {
|
59
|
+
hover: {
|
60
|
+
enabled: true,
|
61
|
+
radius: 5
|
62
|
+
}
|
63
|
+
}
|
64
|
+
},
|
65
|
+
}
|
66
|
+
},
|
67
|
+
series: [{
|
68
|
+
name: metric,
|
69
|
+
data: [ ]
|
70
|
+
},
|
71
|
+
{ name: metric + "min",
|
72
|
+
data: []},
|
73
|
+
{ name: metric + "max",
|
74
|
+
data: []}]
|
75
|
+
});
|
76
|
+
}
|
77
|
+
};
|
78
|
+
this.requestData = function(metric, config) {
|
79
|
+
var that = this;
|
80
|
+
$.getJSON('get', {metric: metric, timespan: config.steps, aggregations: "average,min,max"},
|
81
|
+
function(data) {
|
82
|
+
var i = 0;
|
83
|
+
|
84
|
+
for(method in data) {
|
85
|
+
var items = [];
|
86
|
+
$.each(data[method][0], function(key, val) {
|
87
|
+
items.push([val*1000, data[method][1][key]]);
|
88
|
+
});
|
89
|
+
|
90
|
+
that.charts[metric+config.steps].series[i].setData (items, true, false);
|
91
|
+
i++;
|
92
|
+
}
|
93
|
+
|
94
|
+
}
|
95
|
+
);
|
96
|
+
};
|
97
|
+
}
|