focuslight 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.env +13 -0
- data/.gitignore +21 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Procfile +3 -0
- data/Procfile-gem +3 -0
- data/README.md +162 -0
- data/Rakefile +37 -0
- data/bin/focuslight +7 -0
- data/config.ru +6 -0
- data/focuslight.gemspec +41 -0
- data/lib/focuslight.rb +6 -0
- data/lib/focuslight/cli.rb +56 -0
- data/lib/focuslight/config.rb +27 -0
- data/lib/focuslight/data.rb +258 -0
- data/lib/focuslight/graph.rb +240 -0
- data/lib/focuslight/init.rb +13 -0
- data/lib/focuslight/logger.rb +89 -0
- data/lib/focuslight/rrd.rb +393 -0
- data/lib/focuslight/validator.rb +220 -0
- data/lib/focuslight/version.rb +3 -0
- data/lib/focuslight/web.rb +614 -0
- data/lib/focuslight/worker.rb +97 -0
- data/public/css/bootstrap.min.css +7 -0
- data/public/favicon.ico +0 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/js/bootstrap.min.js +7 -0
- data/public/js/jquery-1.10.2.min.js +6 -0
- data/public/js/jquery-1.10.2.min.map +0 -0
- data/public/js/jquery.storageapi.min.js +2 -0
- data/public/js/site.js +214 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/syntax_spec.rb +9 -0
- data/spec/validator_predefined_rules_spec.rb +177 -0
- data/spec/validator_result_spec.rb +27 -0
- data/spec/validator_rule_spec.rb +68 -0
- data/spec/validator_spec.rb +121 -0
- data/view/add_complex.erb +143 -0
- data/view/base.erb +200 -0
- data/view/docs.erb +125 -0
- data/view/edit.erb +102 -0
- data/view/edit_complex.erb +158 -0
- data/view/index.erb +19 -0
- data/view/list.erb +22 -0
- data/view/view.erb +42 -0
- data/view/view_graph.erb +16 -0
- metadata +345 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "focuslight"
|
3
|
+
require "focuslight/config"
|
4
|
+
require "focuslight/data"
|
5
|
+
|
6
|
+
module Focuslight::Init
|
7
|
+
def self.run
|
8
|
+
datadir = Focuslight::Config.get(:datadir)
|
9
|
+
FileUtils.mkdir_p(datadir)
|
10
|
+
data = Focuslight::Data.new
|
11
|
+
data.create_tables
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require "focuslight/config"
|
3
|
+
|
4
|
+
module Focuslight
|
5
|
+
module Logger
|
6
|
+
def self.included(klass)
|
7
|
+
# To define logger *class* method
|
8
|
+
klass.extend(self)
|
9
|
+
end
|
10
|
+
|
11
|
+
# for test
|
12
|
+
def logger=(logger)
|
13
|
+
Focuslight.logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
Focuslight.logger
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# for test
|
22
|
+
def self.logger=(logger)
|
23
|
+
@logger = logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logger
|
27
|
+
return @logger if @logger
|
28
|
+
|
29
|
+
log_path = Focuslight::Logger::Config.log_path
|
30
|
+
log_level = Focuslight::Logger::Config.log_level
|
31
|
+
# NOTE: Please note that ruby 2.0.0's Logger has a problem on log rotation.
|
32
|
+
# Update to ruby 2.1.0. See https://github.com/ruby/ruby/pull/428 for details.
|
33
|
+
log_shift_age = Focuslight::Logger::Config.log_shift_age
|
34
|
+
log_shift_size = Focuslight::Logger::Config.log_shift_size
|
35
|
+
@logger = ::Logger.new(log_path, log_shift_age, log_shift_size)
|
36
|
+
@logger.level = log_level
|
37
|
+
@logger
|
38
|
+
end
|
39
|
+
|
40
|
+
class Logger::Config
|
41
|
+
def self.log_path(log_path = Focuslight::Config.get(:log_path))
|
42
|
+
case log_path
|
43
|
+
when 'STDOUT'
|
44
|
+
$stdout
|
45
|
+
when 'STDERR'
|
46
|
+
$stderr
|
47
|
+
else
|
48
|
+
log_path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.log_level(log_level = Focuslight::Config.get(:log_level))
|
53
|
+
case log_level
|
54
|
+
when 'debug'
|
55
|
+
::Logger::DEBUG
|
56
|
+
when 'info'
|
57
|
+
::Logger::INFO
|
58
|
+
when 'warn'
|
59
|
+
::Logger::WARN
|
60
|
+
when 'error'
|
61
|
+
::Logger::ERROR
|
62
|
+
when 'fatal'
|
63
|
+
::Logger::FATAL
|
64
|
+
else
|
65
|
+
raise ArgumentError, "invalid log_level #{log_level}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.log_shift_age(log_shift_age = Focuslight::Config.get(:log_shift_age))
|
70
|
+
case log_shift_age
|
71
|
+
when /\d+/
|
72
|
+
log_shift_age.to_i
|
73
|
+
when 'daily'
|
74
|
+
log_shift_age
|
75
|
+
when 'weekly'
|
76
|
+
log_shift_age
|
77
|
+
when 'monthly'
|
78
|
+
log_shift_age
|
79
|
+
else
|
80
|
+
raise ArgumentError, "invalid log_shift_age #{log_shift_age}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.log_shift_size(log_shift_size = Focuslight::Config.get(:log_shift_size))
|
85
|
+
log_shift_size.to_i
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,393 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "focuslight"
|
3
|
+
require "focuslight/config"
|
4
|
+
require "focuslight/logger"
|
5
|
+
require "focuslight/graph"
|
6
|
+
|
7
|
+
require "time"
|
8
|
+
require "tempfile"
|
9
|
+
|
10
|
+
require "rrd"
|
11
|
+
|
12
|
+
class Focuslight::RRD
|
13
|
+
include Focuslight::Logger
|
14
|
+
|
15
|
+
def initialize(args={})
|
16
|
+
@datadir = Focuslight::Config.get(:datadir)
|
17
|
+
# rrdcached
|
18
|
+
end
|
19
|
+
|
20
|
+
def rrd_create_options_long(dst)
|
21
|
+
[
|
22
|
+
'--step', '300',
|
23
|
+
"DS:num:#{dst}:600:U:U",
|
24
|
+
'RRA:AVERAGE:0.5:1:1440', # 5mins, 5days
|
25
|
+
'RRA:AVERAGE:0.5:6:1008', # 30mins, 21days
|
26
|
+
'RRA:AVERAGE:0.5:24:1344', # 2hours, 112days
|
27
|
+
'RRA:AVERAGE:0.5:288:2500', # 24hours, 500days
|
28
|
+
'RRA:MAX:0.5:1:1440', # 5mins, 5days
|
29
|
+
'RRA:MAX:0.5:6:1008', # 30mins, 21days
|
30
|
+
'RRA:MAX:0.5:24:1344', # 2hours, 112days
|
31
|
+
'RRA:MAX:0.5:288:2500', # 24hours, 500days
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
def rrd_create_options_short(dst)
|
36
|
+
[
|
37
|
+
'--step', '60',
|
38
|
+
"DS:num:#{dst}:120:U:U",
|
39
|
+
'RRA:AVERAGE:0.5:1:4800', # 1min, 3days(80hours)
|
40
|
+
'RRA:MAX:0.5:1:4800', # 1min, 3days(80hours)
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
def path(graph, target=:normal)
|
45
|
+
dst = (graph.mode == 'derive' ? 'DERIVE' : 'GAUGE')
|
46
|
+
filepath = nil
|
47
|
+
rrdoptions = nil
|
48
|
+
if target == :short
|
49
|
+
filepath = File.join(@datadir, graph.md5 + '_s.rrd')
|
50
|
+
rrdoptions = rrd_create_options_short(dst)
|
51
|
+
else # :long
|
52
|
+
filepath = File.join(@datadir, graph.md5 + '.rrd')
|
53
|
+
rrdoptions = rrd_create_options_long(dst)
|
54
|
+
end
|
55
|
+
unless File.exists?(filepath)
|
56
|
+
ret = RRD::Wrapper.create(filepath, *rrdoptions.map(&:to_s))
|
57
|
+
unless ret
|
58
|
+
# TODO: error logging / handling
|
59
|
+
raise "RRDtool returns error to create #{filepath}, error: #{RRD::Wrapper.error}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
filepath
|
63
|
+
end
|
64
|
+
|
65
|
+
def update(graph, target=:normal)
|
66
|
+
file = path(graph, target)
|
67
|
+
options = [
|
68
|
+
file,
|
69
|
+
'-t', 'num',
|
70
|
+
'--', ['N', graph.number].join(':')
|
71
|
+
]
|
72
|
+
## TODO: rrdcached
|
73
|
+
# if ( $self->{rrdcached} ) {
|
74
|
+
# # The caching daemon cannot be used together with templates (-t) yet.
|
75
|
+
# splice(@argv, 1, 2); # delete -t option
|
76
|
+
# unshift(@argv, '-d', $self->{rrdcached});
|
77
|
+
# }
|
78
|
+
ret = RRD::Wrapper.update(*options.map(&:to_s))
|
79
|
+
unless ret
|
80
|
+
raise "RRDtool returns error to update #{file}, error: #{RRD::Wrapper.error}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def calc_period(span, from, to)
|
85
|
+
span ||= 'd'
|
86
|
+
|
87
|
+
period_title = nil
|
88
|
+
period = nil
|
89
|
+
period_end = 'now'
|
90
|
+
xgrid = nil
|
91
|
+
|
92
|
+
case span
|
93
|
+
when 'c', 'sc'
|
94
|
+
from_time = Time.parse(from) # from default: 8 days ago by '%Y/%m/%d %T'
|
95
|
+
to_time = to ? Time.parse(to) : Time.now # to default: now by '%Y/%m/%d %T'
|
96
|
+
raise ArgumentError, "from(#{from}) is recent date than to(#{to})" if from_time > to_time
|
97
|
+
period_title = "#{from} to #{to}"
|
98
|
+
period = from_time.to_i
|
99
|
+
period_end = to_time.to_i
|
100
|
+
diff = to_time - from_time
|
101
|
+
if diff < 3 * 60 * 60
|
102
|
+
xgrid = 'MINUTE:10:MINUTE:20:MINUTE:10:0:%M'
|
103
|
+
elsif diff < 4 * 24 * 60 * 60
|
104
|
+
xgrid = 'HOUR:6:DAY:1:HOUR:6:0:%H'
|
105
|
+
elsif diff < 14 * 24 * 60 * 60
|
106
|
+
xgrid = 'DAY:1:DAY:1:DAY:2:86400:%m/%d'
|
107
|
+
elsif diff < 45 * 24 * 60 * 60
|
108
|
+
xgrid = 'DAY:1:WEEK:1:WEEK:1:0:%F'
|
109
|
+
else
|
110
|
+
xgrid = 'WEEK:1:MONTH:1:MONTH:1:2592000:%b'
|
111
|
+
end
|
112
|
+
when 'h', 'sh'
|
113
|
+
period_title = (span == 'h' ? 'Hour (5min avg)' : 'Hour (1min avg)')
|
114
|
+
period = -1 * 60 * 60 * 2
|
115
|
+
xgrid = 'MINUTE:10:MINUTE:20:MINUTE:10:0:%M'
|
116
|
+
when 'n', 'sn'
|
117
|
+
period_title = (span == 'n' ? 'Half Day (5min avg)' : 'Half Day (1min avg)')
|
118
|
+
period = -1 * 60 * 60 * 14
|
119
|
+
xgrid = 'MINUTE:60:MINUTE:120:MINUTE:120:0:%H %M'
|
120
|
+
when 'w'
|
121
|
+
period_title = 'Week (30min avg)'
|
122
|
+
period = -1 * 60 * 60 * 24 * 8
|
123
|
+
xgrid = 'DAY:1:DAY:1:DAY:1:86400:%a'
|
124
|
+
when 'm'
|
125
|
+
period_title = 'Month (2hour avg)'
|
126
|
+
period = -1 * 60 * 60 * 24 * 35
|
127
|
+
xgrid = 'DAY:1:WEEK:1:WEEK:1:604800:Week %W'
|
128
|
+
when 'y'
|
129
|
+
period_title = 'Year (1day avg)'
|
130
|
+
period = -1 * 60 * 60 * 24 * 400
|
131
|
+
xgrid = 'WEEK:1:MONTH:1:MONTH:1:2592000:%b'
|
132
|
+
when '3d', 's3d'
|
133
|
+
period_title = (span == '3d' ? '3 Days (5min avg)' : '3 Days (1min avg)')
|
134
|
+
period = -1 * 60 * 60 * 24 * 3
|
135
|
+
xgrid = 'HOUR:6:DAY:1:HOUR:6:0:%H'
|
136
|
+
when '8h', 's8h'
|
137
|
+
period_title = (span == '8h' ? '8 Hours (5min avg)' : '8 Hours (1min avg)')
|
138
|
+
period = -1 * 8 * 60 * 60
|
139
|
+
xgrid = 'MINUTE:30:HOUR:1:HOUR:1:0:%H:%M'
|
140
|
+
when '4h', 's4h'
|
141
|
+
period_title = (span == '4h' ? '4 Hours (5min avg)' : '4 Hours (1min avg)')
|
142
|
+
period = -1 * 4 * 60 * 60
|
143
|
+
xgrid = 'MINUTE:30:HOUR:1:MINUTE:30:0:%H:%M'
|
144
|
+
else # 'd' or 'sd' ?
|
145
|
+
period_title = (span == 'sd' ? 'Day (1min avg)' : 'Day (5min avg)')
|
146
|
+
period = -1 * 60 * 60 * 33 # 33 hours
|
147
|
+
xgrid = 'HOUR:1:HOUR:2:HOUR:2:0:%H'
|
148
|
+
end
|
149
|
+
|
150
|
+
return period_title, period, period_end, xgrid
|
151
|
+
end
|
152
|
+
|
153
|
+
def graph(datas, args)
|
154
|
+
datas = [datas] unless datas.is_a?(Array)
|
155
|
+
span = args.fetch(:t, :d)
|
156
|
+
from = args[:from]
|
157
|
+
to = args[:to]
|
158
|
+
width = args.fetch(:width, 390)
|
159
|
+
height = args.fetch(:height, 110)
|
160
|
+
|
161
|
+
period_title, period, period_end, xgrid = calc_period(span, from, to)
|
162
|
+
|
163
|
+
tmpfile = Tempfile.new(["", ".png"]) # [basename_prefix, suffix]
|
164
|
+
rrdoptions = [
|
165
|
+
tmpfile.path,
|
166
|
+
'-w', width,
|
167
|
+
'-h', height,
|
168
|
+
'-a', 'PNG',
|
169
|
+
'-l', 0, #minimum
|
170
|
+
'-u', 2, #maximum
|
171
|
+
'-x', (args[:xgrid].empty? ? xgrid : args[:xgrid]),
|
172
|
+
'-s', period,
|
173
|
+
'-e', period_end,
|
174
|
+
'--slope-mode',
|
175
|
+
'--disable-rrdtool-tag',
|
176
|
+
'--color', 'BACK#' + args[:background_color].to_s.upcase,
|
177
|
+
'--color', 'CANVAS#' + args[:canvas_color].to_s.upcase,
|
178
|
+
'--color', 'FONT#' + args[:font_color].to_s.upcase,
|
179
|
+
'--color', 'FRAME#' + args[:frame_color].to_s.upcase,
|
180
|
+
'--color', 'AXIS#' + args[:axis_color].to_s.upcase,
|
181
|
+
'--color', 'SHADEA#' + args[:shadea_color].to_s.upcase,
|
182
|
+
'--color', 'SHADEB#' + args[:shadeb_color].to_s.upcase,
|
183
|
+
'--border', args[:border].to_s.upcase
|
184
|
+
]
|
185
|
+
rrdoptions.push('-y', args[:ygrid]) unless args[:ygrid].empty?
|
186
|
+
rrdoptions.push('-t', period_title.to_s.dup) unless args[:notitle]
|
187
|
+
rrdoptions.push('--no-legend') unless args[:legend]
|
188
|
+
rrdoptions.push('--only-graph') if args[:graphonly]
|
189
|
+
rrdoptions.push('--logarithmic') if args[:logarithmic]
|
190
|
+
|
191
|
+
rrdoptions.push('--font', "AXIS:8:")
|
192
|
+
rrdoptions.push('--font', "LEGEND:8:")
|
193
|
+
|
194
|
+
rrdoptions.push('-u', args[:upper_limit]) if args[:upper_limit]
|
195
|
+
rrdoptions.push('-l', args[:lower_limit]) if args[:lower_limit]
|
196
|
+
rrdoptions.push('-r') if args[:rigid]
|
197
|
+
|
198
|
+
defs = []
|
199
|
+
datas.each_with_index do |data, i|
|
200
|
+
type = data.c_type ? data.c_type : data.type
|
201
|
+
gdata = 'num'
|
202
|
+
llimit = data.llimit
|
203
|
+
ulimit = data.ulimit
|
204
|
+
stack = (data.stack && i > 0 ? ':STACK' : '')
|
205
|
+
file = (span =~ /^s/ ? path(data, :short) : path(data, :long))
|
206
|
+
unit = (data.unit || '').gsub('%', '%%')
|
207
|
+
|
208
|
+
rrdoptions.push(
|
209
|
+
'DEF:%s%dt=%s:%s:AVERAGE' % [gdata, i, file, gdata],
|
210
|
+
'CDEF:%s%d=%s%dt,%s,%s,LIMIT,%d,%s' % [gdata, i, gdata, i, llimit, ulimit, data.adjustval, data.adjust],
|
211
|
+
'%s:%s%d%s:%s %s' % [type, gdata, i, data.color, _escape(data.graph), stack],
|
212
|
+
'GPRINT:%s%d:LAST:Cur\: %%4.1lf%%s%s' % [gdata, i, unit],
|
213
|
+
'GPRINT:%s%d:AVERAGE:Avg\: %%4.1lf%%s%s' % [gdata, i, unit],
|
214
|
+
'GPRINT:%s%d:MAX:Max\: %%4.1lf%%s%s' % [gdata, i, unit],
|
215
|
+
'GPRINT:%s%d:MIN:Min\: %%4.1lf%%s%s\l' % [gdata, i, unit],
|
216
|
+
'VDEF:%s%dcur=%s%d,LAST' % [gdata, i, gdata, i],
|
217
|
+
'PRINT:%s%dcur:%%.8lf' % [gdata, i],
|
218
|
+
'VDEF:%s%davg=%s%d,AVERAGE' % [gdata, i, gdata, i],
|
219
|
+
'PRINT:%s%davg:%%.8lf' % [gdata, i],
|
220
|
+
'VDEF:%s%dmax=%s%d,MAXIMUM' % [gdata, i, gdata, i],
|
221
|
+
'PRINT:%s%dmax:%%.8lf' % [gdata, i],
|
222
|
+
'VDEF:%s%dmin=%s%d,MINIMUM' % [gdata, i, gdata, i],
|
223
|
+
'PRINT:%s%dmin:%%.8lf' % [gdata, i],
|
224
|
+
)
|
225
|
+
defs << ('%s%d' % [gdata, i])
|
226
|
+
end
|
227
|
+
|
228
|
+
if args[:sumup]
|
229
|
+
sumup = [ defs.shift ]
|
230
|
+
unit = datas.first.unit.gsub('%', '%%')
|
231
|
+
defs.each do |d|
|
232
|
+
sumup.push(d, '+')
|
233
|
+
end
|
234
|
+
rrdoptions.push(
|
235
|
+
'CDEF:sumup=%s' % [ sumup.join(',') ],
|
236
|
+
'LINE0:sumup#cccccc:total',
|
237
|
+
'GPRINT:sumup:LAST:Cur\: %%4.1lf%%s%s' % [unit],
|
238
|
+
'GPRINT:sumup:AVERAGE:Avg\: %%4.1lf%%s%s' % [unit],
|
239
|
+
'GPRINT:sumup:MAX:Max\: %%4.1lf%%s%s' % [unit],
|
240
|
+
'GPRINT:sumup:MIN:Min\: %%4.1lf%%s%s\l' % [unit],
|
241
|
+
'VDEF:sumupcur=sumup,LAST',
|
242
|
+
'PRINT:sumupcur:%.8lf',
|
243
|
+
'VDEF:sumupavg=sumup,AVERAGE',
|
244
|
+
'PRINT:sumupavg:%.8lf',
|
245
|
+
'VDEF:sumupmax=sumup,MAXIMUM',
|
246
|
+
'PRINT:sumupmax:%.8lf',
|
247
|
+
'VDEF:sumupmin=sumup,MINIMUM',
|
248
|
+
'PRINT:sumupmin:%.8lf',
|
249
|
+
)
|
250
|
+
end
|
251
|
+
|
252
|
+
ret = RRD::Wrapper.graph(*rrdoptions.map(&:to_s))
|
253
|
+
unless ret
|
254
|
+
tmpfile.close!
|
255
|
+
raise "RRDtool returns error to draw graph, error: #{RRD::Wrapper.error}"
|
256
|
+
end
|
257
|
+
|
258
|
+
# Cannot get last PRINT return value, set of [current,avg,max,min] of each data source
|
259
|
+
# This makes 'summary' API not supported
|
260
|
+
|
261
|
+
graph_img = IO.binread(tmpfile.path); # read as binary
|
262
|
+
tmpfile.delete
|
263
|
+
|
264
|
+
[
|
265
|
+
"/var/folders/tl/xtb7dnc132nggd6hs83y58h40000gq/T/20140117-86285-1igjvvh.png",
|
266
|
+
"-w", 390,
|
267
|
+
"-h", 110,
|
268
|
+
"-a", "PNG",
|
269
|
+
"-l", 0,
|
270
|
+
"-u", 2,
|
271
|
+
"-x", "HOUR:1:HOUR:2:HOUR:2:0:%H",
|
272
|
+
"-s", -118800,
|
273
|
+
"-e", "now",
|
274
|
+
"--slope-mode",
|
275
|
+
"--disable-rrdtool-tag",
|
276
|
+
"--color", "BACK#F3F3F3", "--color", "CANVAS#FFFFFF", "--color", "FONT#000000",
|
277
|
+
"--color", "FRAME#000000", "--color", "AXIS#000000", "--color", "SHADEA#CFCFCF",
|
278
|
+
"--color", "SHADEB#9E9E9E",
|
279
|
+
"--border", "3",
|
280
|
+
"-t", "Day (1min avg)",
|
281
|
+
"--no-legend",
|
282
|
+
"--font", "AXIS:8:",
|
283
|
+
"--font", "LEGEND:8:",
|
284
|
+
"DEF:num0t=./data/c4ca4238a0b923820dcc509a6f75849b.rrd:num:AVERAGE",
|
285
|
+
"CDEF:num0=num0t,-1000000000.0,1.0e+15,LIMIT,1,*",
|
286
|
+
"AREA:num0:one",
|
287
|
+
"GPRINT:num0:LAST:Cur\\: %4.1lf%s",
|
288
|
+
"GPRINT:num0:AVERAGE:Avg\\: %4.1lf%s",
|
289
|
+
"GPRINT:num0:MAX:Max\\: %4.1lf%s",
|
290
|
+
"GPRINT:num0:MIN:Min\\: %4.1lf%s\\l",
|
291
|
+
"VDEF:num0cur=num0,LAST",
|
292
|
+
"PRINT:num0cur:%.8lf",
|
293
|
+
"VDEF:num0avg=num0,AVERAGE",
|
294
|
+
"PRINT:num0avg:%.8lf",
|
295
|
+
"VDEF:num0max=num0,MAXIMUM",
|
296
|
+
"PRINT:num0max:%.8lf",
|
297
|
+
"VDEF:num0min=num0,MINIMUM",
|
298
|
+
"PRINT:num0min:%.8lf"
|
299
|
+
]
|
300
|
+
|
301
|
+
graph_img
|
302
|
+
end
|
303
|
+
|
304
|
+
def export(datas, args)
|
305
|
+
datas = [datas] unless datas.is_a?(Array)
|
306
|
+
span = args.fetch(:t, 'd')
|
307
|
+
from = args[:from]
|
308
|
+
to = args[:to]
|
309
|
+
width = args.fetch(:width, 390)
|
310
|
+
cf = args[:cf]
|
311
|
+
|
312
|
+
period_title, period, period_end, xgrid = calc_period(span, from, to)
|
313
|
+
|
314
|
+
rrdoptions = [
|
315
|
+
'-m', width,
|
316
|
+
'-s', period,
|
317
|
+
'-e', period_end
|
318
|
+
]
|
319
|
+
|
320
|
+
rrdoptions.push('--step', args[:step]) if args[:step]
|
321
|
+
|
322
|
+
defs = []
|
323
|
+
datas.each_with_index do |data, i|
|
324
|
+
type = data.c_type ? data.c_type : data.type
|
325
|
+
gdata = 'num'
|
326
|
+
llimit = data.llimit
|
327
|
+
ulimit = data.ulimit
|
328
|
+
stack = (data.stack && i > 0 ? ':STACK' : '')
|
329
|
+
file = (span =~ /^s/ ? path(data, :short) : path(data, :long))
|
330
|
+
|
331
|
+
rrdoptions.push(
|
332
|
+
'DEF:%s%dt=%s:%s:%s' % [gdata, i, file, gdata, cf],
|
333
|
+
'CDEF:%s%d=%s%dt,%s,%s,LIMIT,%d,%s' % [gdata, i, gdata, i, llimit, ulimit, data.dadjustval, data.adjust],
|
334
|
+
'XPORT:%s%d:%s' % [gdata, i, _escape(data.graph)]
|
335
|
+
)
|
336
|
+
defs << ('%s%d' % [gdata, i])
|
337
|
+
end
|
338
|
+
|
339
|
+
if args[:sumup]
|
340
|
+
sumup = [ defs.shift ]
|
341
|
+
defs.each do |d|
|
342
|
+
sumup.push(d, '+')
|
343
|
+
end
|
344
|
+
rrdoptions.push(
|
345
|
+
'CDEF:sumup=%s' % [sumup.join(',')],
|
346
|
+
'XPORT:sumup:total'
|
347
|
+
)
|
348
|
+
end
|
349
|
+
|
350
|
+
ret = RRD::Wrapper.xport(*rrdoptions.map(&:to_s))
|
351
|
+
unless ret
|
352
|
+
raise "RRDtool returns error to xport, error: #{RRD::Wrapper.error}"
|
353
|
+
end
|
354
|
+
### copied from RRD::Wrapper spec
|
355
|
+
# values = RRD::Wrapper.xport("--start", "1266933600", "--end", "1266944400", "DEF:xx=#{RRD_FILE}:cpu0:AVERAGE", "XPORT:xx:Legend 0")
|
356
|
+
# values[0..-2].should == [["time", "Legend 0"], [1266933600, 0.0008], [1266937200, 0.0008], [1266940800, 0.0008]]
|
357
|
+
cols_row = ret.shift
|
358
|
+
|
359
|
+
column_names = cols_row[1..-1] # cols_row[0] == 'time'
|
360
|
+
columns = column_names.length
|
361
|
+
start_timestamp = ret.first.first
|
362
|
+
end_timestamp = ret.last.first
|
363
|
+
step = ret[1].first - ret[0].first
|
364
|
+
|
365
|
+
rows = []
|
366
|
+
ret.each do |values|
|
367
|
+
rows << values[1..-1]
|
368
|
+
end
|
369
|
+
|
370
|
+
{
|
371
|
+
'start_timestamp' => start_timestamp,
|
372
|
+
'end_timestamp' => end_timestamp,
|
373
|
+
'step' => step,
|
374
|
+
'columns' => columns,
|
375
|
+
'column_names' => column_names,
|
376
|
+
'rows' => rows,
|
377
|
+
}
|
378
|
+
end
|
379
|
+
|
380
|
+
def remove(graph)
|
381
|
+
[File.join(@datadir, graph.md5 + '.rrd'), File.join(@datadir, graph.md5 + '_s.rrd')].each do |file|
|
382
|
+
begin
|
383
|
+
File.delete(file)
|
384
|
+
rescue => e
|
385
|
+
# ignore NOSUCHFILE or others
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def _escape(str)
|
391
|
+
str.gsub(':', '\:')
|
392
|
+
end
|
393
|
+
end
|