class Focuslight::RRD

Public Class Methods

new(args={}) click to toggle source
# File lib/focuslight/rrd.rb, line 14
def initialize(args={})
  @datadir = Focuslight::Config.get(:datadir)
  # rrdcached
end

Public Instance Methods

_escape(str) click to toggle source
# File lib/focuslight/rrd.rb, line 391
def _escape(str)
  str.gsub(':', '\:')
end
calc_period(span, from, to) click to toggle source
# File lib/focuslight/rrd.rb, line 83
def calc_period(span, from, to)
  span ||= 'd'

  period_title = nil
  period = nil
  period_end = 'now'
  xgrid = nil

  case span
  when 'c', 'sc'
    from_time = Time.parse(from)             # from default: 8 days ago by '%Y/%m/%d %T'
    to_time = to ? Time.parse(to) : Time.now # to   default: now by '%Y/%m/%d %T'
    raise ArgumentError, "from(#{from}) is recent date than to(#{to})" if from_time > to_time
    period_title = "#{from} to #{to}"
    period = from_time.to_i
    period_end = to_time.to_i
    diff = to_time - from_time
    if diff < 3 * 60 * 60
      xgrid = 'MINUTE:10:MINUTE:20:MINUTE:10:0:%M'
    elsif diff < 4 * 24 * 60 * 60
      xgrid = 'HOUR:6:DAY:1:HOUR:6:0:%H'
    elsif diff < 14 * 24 * 60 * 60
      xgrid = 'DAY:1:DAY:1:DAY:2:86400:%m/%d'
    elsif diff < 45 * 24 * 60 * 60
      xgrid = 'DAY:1:WEEK:1:WEEK:1:0:%F'
    else
      xgrid = 'WEEK:1:MONTH:1:MONTH:1:2592000:%b'
    end
  when 'h', 'sh'
    period_title = (span == 'h' ? 'Hour (5min avg)' : 'Hour (1min avg)')
    period = -1 * 60 * 60 * 2
    xgrid = 'MINUTE:10:MINUTE:20:MINUTE:10:0:%M'
  when 'n', 'sn'
    period_title = (span == 'n' ? 'Half Day (5min avg)' : 'Half Day (1min avg)')
    period = -1 * 60 * 60 * 14
    xgrid = 'MINUTE:60:MINUTE:120:MINUTE:120:0:%H %M'
  when 'w'
    period_title = 'Week (30min avg)'
    period = -1 * 60 * 60 * 24 * 8
    xgrid = 'DAY:1:DAY:1:DAY:1:86400:%a'
  when 'm'
    period_title = 'Month (2hour avg)'
    period = -1 * 60 * 60 * 24 * 35
    xgrid = 'DAY:1:WEEK:1:WEEK:1:604800:Week %W'
  when 'y'
    period_title = 'Year (1day avg)'
    period = -1 * 60 * 60 * 24 * 400
    xgrid = 'WEEK:1:MONTH:1:MONTH:1:2592000:%b'
  when '3d', 's3d'
    period_title = (span == '3d' ? '3 Days (5min avg)' : '3 Days (1min avg)')
    period = -1 * 60 * 60 * 24 * 3
    xgrid = 'HOUR:6:DAY:1:HOUR:6:0:%H'
  when '8h', 's8h'
    period_title = (span == '8h' ? '8 Hours (5min avg)' : '8 Hours (1min avg)')
    period = -1 * 8 * 60 * 60
    xgrid = 'MINUTE:30:HOUR:1:HOUR:1:0:%H:%M'
  when '4h', 's4h'
    period_title = (span == '4h' ? '4 Hours (5min avg)' : '4 Hours (1min avg)')
    period = -1 * 4 * 60 * 60
    xgrid = 'MINUTE:30:HOUR:1:MINUTE:30:0:%H:%M'
  else # 'd' or 'sd' ?
    period_title = (span == 'sd' ? 'Day (1min avg)' : 'Day (5min avg)')
    period = -1 * 60 * 60 * 33 # 33 hours
    xgrid = 'HOUR:1:HOUR:2:HOUR:2:0:%H'
  end

  return period_title, period, period_end, xgrid
end
export(datas, args) click to toggle source
# File lib/focuslight/rrd.rb, line 305
def export(datas, args)
  datas = [datas] unless datas.is_a?(Array)
  span = args.fetch(:t, 'd')
  from = args[:from]
  to = args[:to]
  width = args.fetch(:width, 390)
  cf = args[:cf]

  period_title, period, period_end, xgrid = calc_period(span, from, to)

  rrdoptions = [
    '-m', width,
    '-s', period,
    '-e', period_end
  ]

  rrdoptions.push('--step', args[:step]) if args[:step]

  defs = []
  datas.each_with_index do |data, i|
    type  = data.c_type ? data.c_type : data.type
    gdata =  'num'
    llimit = data.llimit
    ulimit = data.ulimit
    stack = (data.stack && i > 0 ? ':STACK' : '')
    file = (span =~ /^s/ ? path(data, :short) : path(data, :long))

    rrdoptions.push(
      'DEF:%s%dt=%s:%s:%s' % [gdata, i, file, gdata, cf],
      'CDEF:%s%d=%s%dt,%s,%s,LIMIT,%d,%s' % [gdata, i, gdata, i, llimit, ulimit, data.adjustval, data.adjust],
      'XPORT:%s%d:%s' % [gdata, i, _escape(data.graph)]
    )
    defs << ('%s%d' % [gdata, i])
  end

  if args[:sumup]
    sumup = [ defs.shift ]
    defs.each do |d|
      sumup.push(d, '+')
    end
    rrdoptions.push(
      'CDEF:sumup=%s' % [sumup.join(',')],
      'XPORT:sumup:total'
    )
  end

  ret = RRD::Wrapper.xport(*rrdoptions.map(&:to_s))
  unless ret
    raise "RRDtool returns error to xport, error: #{RRD::Wrapper.error}"
  end
  ### copied from RRD::Wrapper spec
  # values = RRD::Wrapper.xport("--start", "1266933600", "--end", "1266944400", "DEF:xx=#{RRD_FILE}:cpu0:AVERAGE", "XPORT:xx:Legend 0")
  # values[0..-2].should == [["time", "Legend 0"], [1266933600, 0.0008], [1266937200, 0.0008], [1266940800, 0.0008]]
  cols_row = ret.shift

  column_names = cols_row[1..-1] # cols_row[0] == 'time'
  columns = column_names.length
  start_timestamp = ret.first.first
  end_timestamp = ret.last.first
  step = ret[1].first - ret[0].first
  rows = []
  ret.each do |values|
    # GrowthForecast compatibility NaN to nil
    rows << values[1..-1].map {|v| v.nan? ? nil : v}
  end

  {
    'start_timestamp' => start_timestamp,
    'end_timestamp' => end_timestamp,
    'step' => step,
    'columns' => columns,
    'column_names' => column_names,
    'rows' => rows,
  }
end
graph(datas, args) click to toggle source
# File lib/focuslight/rrd.rb, line 152
def graph(datas, args)
  datas = [datas] unless datas.is_a?(Array)
  span = args.fetch(:t, :d)
  from = args[:from]
  to = args[:to]
  width = args.fetch(:width, 390)
  height = args.fetch(:height, 110)

  period_title, period, period_end, xgrid = calc_period(span, from, to)

  tmpfile = Tempfile.new(["", ".png"]) # [basename_prefix, suffix]
  rrdoptions = [
    tmpfile.path,
    '-w', width,
    '-h', height,
    '-a', 'PNG',
    '-l', 0, #minimum
    '-u', 2, #maximum
    '-x', (args[:xgrid].empty? ? xgrid : args[:xgrid]),
    '-s', period,
    '-e', period_end,
    '--slope-mode',
    '--disable-rrdtool-tag',
    '--color', 'BACK#' + args[:background_color].to_s.upcase,
    '--color', 'CANVAS#' + args[:canvas_color].to_s.upcase,
    '--color', 'FONT#' + args[:font_color].to_s.upcase,
    '--color', 'FRAME#' + args[:frame_color].to_s.upcase,
    '--color', 'AXIS#' + args[:axis_color].to_s.upcase,
    '--color', 'SHADEA#' + args[:shadea_color].to_s.upcase,
    '--color', 'SHADEB#' + args[:shadeb_color].to_s.upcase,
    '--border', args[:border].to_s.upcase
  ]
  rrdoptions.push('-y', args[:ygrid]) unless args[:ygrid].empty?
  rrdoptions.push('-t', period_title.to_s.dup) unless args[:notitle]
  rrdoptions.push('-v', args[:vertical_label]) unless args[:vertical_label].empty?
  rrdoptions.push('--no-legend') unless args[:legend]
  rrdoptions.push('--only-graph') if args[:graphonly]
  rrdoptions.push('--logarithmic') if args[:logarithmic]

  rrdoptions.push('--font', "AXIS:8:")
  rrdoptions.push('--font', "LEGEND:8:")

  rrdoptions.push('-u', args[:upper_limit]) unless args[:upper_limit].empty?
  rrdoptions.push('-l', args[:lower_limit]) unless args[:lower_limit].empty?
  rrdoptions.push('-r') if args[:rigid]
  rrdoptions.push('--units-exponent', args[:units_exponent]) if args[:units_exponent]

  defs = []
  datas.each_with_index do |data, i|
    type  = data.c_type ? data.c_type : data.type
    gdata =  'num'
    llimit = data.llimit
    ulimit = data.ulimit
    stack = (data.stack && i > 0 ? ':STACK' : '')
    file = (span =~ /^s/ ? path(data, :short) : path(data, :long))
    unit = (data.unit || '').gsub('%', '%%')

    rrdoptions.push(
      'DEF:%s%dt=%s:%s:AVERAGE' % [gdata, i, file, gdata],
      'CDEF:%s%d=%s%dt,%s,%s,LIMIT,%d,%s' % [gdata, i, gdata, i, llimit, ulimit, data.adjustval, data.adjust],
      '%s:%s%d%s:%s %s' % [type, gdata, i, data.color, _escape(data.graph), stack],
      'GPRINT:%s%d:LAST:Cur\: %%4.1lf%%s%s' % [gdata, i, unit],
      'GPRINT:%s%d:AVERAGE:Avg\: %%4.1lf%%s%s' % [gdata, i, unit],
      'GPRINT:%s%d:MAX:Max\: %%4.1lf%%s%s' % [gdata, i, unit],
      'GPRINT:%s%d:MIN:Min\: %%4.1lf%%s%s\l' % [gdata, i, unit],
      'VDEF:%s%dcur=%s%d,LAST' % [gdata, i, gdata, i],
      'PRINT:%s%dcur:%%.8lf' % [gdata, i],
      'VDEF:%s%davg=%s%d,AVERAGE' % [gdata, i, gdata, i],
      'PRINT:%s%davg:%%.8lf' % [gdata, i],
      'VDEF:%s%dmax=%s%d,MAXIMUM' % [gdata, i, gdata, i],
      'PRINT:%s%dmax:%%.8lf' % [gdata, i],
      'VDEF:%s%dmin=%s%d,MINIMUM' % [gdata, i, gdata, i],
      'PRINT:%s%dmin:%%.8lf' % [gdata, i],
    )
    defs << ('%s%d' % [gdata, i])
  end

  if args[:sumup]
    sumup = [ defs.shift ]
    unit = datas.first.unit.gsub('%', '%%')
    defs.each do |d|
      sumup.push(d, '+')
    end
    rrdoptions.push(
      'CDEF:sumup=%s' % [ sumup.join(',') ],
      'LINE0:sumup#cccccc:total',
      'GPRINT:sumup:LAST:Cur\: %%4.1lf%%s%s' % [unit],
      'GPRINT:sumup:AVERAGE:Avg\: %%4.1lf%%s%s' % [unit],
      'GPRINT:sumup:MAX:Max\: %%4.1lf%%s%s' % [unit],
      'GPRINT:sumup:MIN:Min\: %%4.1lf%%s%s\l' % [unit],
      'VDEF:sumupcur=sumup,LAST',
      'PRINT:sumupcur:%.8lf',
      'VDEF:sumupavg=sumup,AVERAGE',
      'PRINT:sumupavg:%.8lf',
      'VDEF:sumupmax=sumup,MAXIMUM',
      'PRINT:sumupmax:%.8lf',
      'VDEF:sumupmin=sumup,MINIMUM',
      'PRINT:sumupmin:%.8lf',
    )
  end

  ret = RRD::Wrapper.graph(*rrdoptions.map(&:to_s))
  unless ret
    tmpfile.close!
    raise "RRDtool returns error to draw graph, error: #{RRD::Wrapper.error}"
  end

  # Cannot get last PRINT return value, set of [current,avg,max,min] of each data source
  # This makes 'summary' API not supported

  graph_img = IO.binread(tmpfile.path); # read as binary
  tmpfile.delete

  [
    "/var/folders/tl/xtb7dnc132nggd6hs83y58h40000gq/T/20140117-86285-1igjvvh.png",
    "-w", 390,
    "-h", 110,
    "-a", "PNG",
    "-l", 0,
    "-u", 2,
    "-x", "HOUR:1:HOUR:2:HOUR:2:0:%H",
    "-s", -118800,
    "-e", "now",
    "--slope-mode",
    "--disable-rrdtool-tag",
    "--color", "BACK#F3F3F3", "--color", "CANVAS#FFFFFF", "--color", "FONT#000000",
    "--color", "FRAME#000000", "--color", "AXIS#000000", "--color", "SHADEA#CFCFCF",
    "--color", "SHADEB#9E9E9E",
    "--border", "3",
    "-t", "Day (1min avg)",
    "--no-legend",
    "--font", "AXIS:8:",
    "--font", "LEGEND:8:",
    "DEF:num0t=./data/c4ca4238a0b923820dcc509a6f75849b.rrd:num:AVERAGE",
    "CDEF:num0=num0t,-1000000000.0,1.0e+15,LIMIT,1,*",
    "AREA:num0:one",
    "GPRINT:num0:LAST:Cur\\: %4.1lf%s",
    "GPRINT:num0:AVERAGE:Avg\\: %4.1lf%s",
    "GPRINT:num0:MAX:Max\\: %4.1lf%s",
    "GPRINT:num0:MIN:Min\\: %4.1lf%s\\l",
    "VDEF:num0cur=num0,LAST",
    "PRINT:num0cur:%.8lf",
    "VDEF:num0avg=num0,AVERAGE",
    "PRINT:num0avg:%.8lf",
    "VDEF:num0max=num0,MAXIMUM",
    "PRINT:num0max:%.8lf",
    "VDEF:num0min=num0,MINIMUM",
    "PRINT:num0min:%.8lf"
  ]

  graph_img
end
path(graph, target=:normal) click to toggle source
# File lib/focuslight/rrd.rb, line 43
def path(graph, target=:normal)
  dst = (graph.mode == 'derive' ? 'DERIVE' : 'GAUGE')
  filepath = nil
  rrdoptions = nil
  if target == :short
    filepath = File.join(@datadir, graph.md5 + '_s.rrd')
    rrdoptions = rrd_create_options_short(dst)
  else # :long
    filepath = File.join(@datadir, graph.md5 + '.rrd')
    rrdoptions = rrd_create_options_long(dst)
  end
  unless File.exists?(filepath)
    ret = RRD::Wrapper.create(filepath, *rrdoptions.map(&:to_s))
    unless ret
      # TODO: error logging / handling
      raise "RRDtool returns error to create #{filepath}, error: #{RRD::Wrapper.error}"
    end
  end
  filepath
end
remove(graph) click to toggle source
# File lib/focuslight/rrd.rb, line 381
def remove(graph)
  [File.join(@datadir, graph.md5 + '.rrd'), File.join(@datadir, graph.md5 + '_s.rrd')].each do |file|
    begin
      File.delete(file)
    rescue => e
      # ignore NOSUCHFILE or others
    end
  end
end
rrd_create_options_long(dst) click to toggle source
# File lib/focuslight/rrd.rb, line 19
def rrd_create_options_long(dst)
  [
    '--step', '300',
    "DS:num:#{dst}:600:U:U",
    'RRA:AVERAGE:0.5:1:1440', # 5mins, 5days
    'RRA:AVERAGE:0.5:6:1008', # 30mins, 21days
    'RRA:AVERAGE:0.5:24:1344', # 2hours, 112days
    'RRA:AVERAGE:0.5:288:2500', # 24hours, 500days
    'RRA:MAX:0.5:1:1440', # 5mins, 5days
    'RRA:MAX:0.5:6:1008', # 30mins, 21days
    'RRA:MAX:0.5:24:1344', # 2hours, 112days
    'RRA:MAX:0.5:288:2500', # 24hours, 500days
  ]
end
rrd_create_options_short(dst) click to toggle source
# File lib/focuslight/rrd.rb, line 34
def rrd_create_options_short(dst)
  [
    '--step', '60',
    "DS:num:#{dst}:120:U:U",
    'RRA:AVERAGE:0.5:1:4800', # 1min, 3days(80hours)
    'RRA:MAX:0.5:1:4800', # 1min, 3days(80hours)
  ]
end
update(graph, target=:normal) click to toggle source
# File lib/focuslight/rrd.rb, line 64
def update(graph, target=:normal)
  file = path(graph, target)
  options = [
    file,
    '-t', 'num',
    '--', ['N', graph.number].join(':')
  ]
  ## TODO: rrdcached
  # if ( $self->{rrdcached} ) {
  #   # The caching daemon cannot be used together with templates (-t) yet.
  #   splice(@argv, 1, 2); # delete -t option
  #   unshift(@argv, '-d', $self->{rrdcached});
  # }
  ret = RRD::Wrapper.update(*options.map(&:to_s))
  unless ret
    raise "RRDtool returns error to update #{file}, error: #{RRD::Wrapper.error}"
  end
end