focuslight 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.env +13 -0
  3. data/.gitignore +21 -0
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +21 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/Procfile +3 -0
  9. data/Procfile-gem +3 -0
  10. data/README.md +162 -0
  11. data/Rakefile +37 -0
  12. data/bin/focuslight +7 -0
  13. data/config.ru +6 -0
  14. data/focuslight.gemspec +41 -0
  15. data/lib/focuslight.rb +6 -0
  16. data/lib/focuslight/cli.rb +56 -0
  17. data/lib/focuslight/config.rb +27 -0
  18. data/lib/focuslight/data.rb +258 -0
  19. data/lib/focuslight/graph.rb +240 -0
  20. data/lib/focuslight/init.rb +13 -0
  21. data/lib/focuslight/logger.rb +89 -0
  22. data/lib/focuslight/rrd.rb +393 -0
  23. data/lib/focuslight/validator.rb +220 -0
  24. data/lib/focuslight/version.rb +3 -0
  25. data/lib/focuslight/web.rb +614 -0
  26. data/lib/focuslight/worker.rb +97 -0
  27. data/public/css/bootstrap.min.css +7 -0
  28. data/public/favicon.ico +0 -0
  29. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  30. data/public/fonts/glyphicons-halflings-regular.svg +229 -0
  31. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  32. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  33. data/public/js/bootstrap.min.js +7 -0
  34. data/public/js/jquery-1.10.2.min.js +6 -0
  35. data/public/js/jquery-1.10.2.min.map +0 -0
  36. data/public/js/jquery.storageapi.min.js +2 -0
  37. data/public/js/site.js +214 -0
  38. data/spec/spec_helper.rb +3 -0
  39. data/spec/syntax_spec.rb +9 -0
  40. data/spec/validator_predefined_rules_spec.rb +177 -0
  41. data/spec/validator_result_spec.rb +27 -0
  42. data/spec/validator_rule_spec.rb +68 -0
  43. data/spec/validator_spec.rb +121 -0
  44. data/view/add_complex.erb +143 -0
  45. data/view/base.erb +200 -0
  46. data/view/docs.erb +125 -0
  47. data/view/edit.erb +102 -0
  48. data/view/edit_complex.erb +158 -0
  49. data/view/index.erb +19 -0
  50. data/view/list.erb +22 -0
  51. data/view/view.erb +42 -0
  52. data/view/view_graph.erb +16 -0
  53. 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