focuslight 0.1.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.
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