1 module("luci.statistics.rrdtool", package.seeall)
3 require("luci.statistics.datatree")
4 require("luci.statistics.rrdtool.colors")
5 require("luci.statistics.rrdtool.definitions")
11 Graph = luci.util.class()
13 function Graph.__init__( self, timespan, opts )
16 opts.width = opts.width or "400"
18 self.colors = luci.statistics.rrdtool.colors.Instance()
19 self.defs = luci.statistics.rrdtool.definitions.Instance()
20 self.tree = luci.statistics.datatree.Instance()
22 -- rrdtool defalt args
25 "-s", "NOW-" .. ( timespan or 900 ),
30 function Graph.mktitle( self, host, plugin, plugin_instance, dtype, dtype_instance )
31 local t = host .. "/" .. plugin
32 if type(plugin_instance) == "string" and plugin_instance:len() > 0 then
33 t = t .. "-" .. plugin_instance
36 if type(dtype_instance) == "string" and dtype_instance:len() > 0 then
37 t = t .. "-" .. dtype_instance
42 function Graph.mkrrdpath( self, ... )
43 return string.format( "/tmp/%s.rrd", self:mktitle( ... ) )
46 function Graph.mkpngpath( self, ... )
47 return string.format( "/tmp/rrdimg/%s.png", self:mktitle( ... ) )
50 function Graph._push( self, elem )
52 if type(elem) == "string" then
53 table.insert( self.args, elem )
55 for i, item in ipairs(elem) do
56 table.insert( self.args, item )
63 function Graph._clearargs( self )
64 for i = #self.args, 7, -1 do
65 table.remove( self.args, i )
69 function Graph._forcelol( self, list )
70 if type(list[1]) ~= "table" then
76 function Graph._rrdtool( self, png, rrd )
79 local dir = png:gsub("/[^/]+$","")
80 luci.fs.mkdir( dir, true )
82 -- construct commandline
83 local cmdline = "rrdtool graph " .. png
85 for i, opt in ipairs(self.args) do
87 opt = opt .. "" -- force string
90 opt = opt:gsub( "{file}", rrd )
93 if opt:match("[^%w]") then
94 cmdline = cmdline .. " '" .. opt .. "'"
96 cmdline = cmdline .. " " .. opt
101 local rrdtool = io.popen( cmdline )
105 function Graph._generic( self, opts )
108 local rrasingle = false -- XXX: fixme
110 -- internal state variables
111 local _stack_neg = { }
112 local _stack_pos = { }
113 local _longest_name = 0
114 local _has_totals = false
116 -- some convenient aliases
117 local _ti = table.insert
118 local _sf = string.format
120 -- local helper: create definitions for min, max, avg and create *_nnl (not null) variable from avg
121 function __def(source)
123 local inst = source.sname
124 local rrd = source.rrd
127 if not ds or ds:len() == 0 then ds = "value" end
129 local rv = { _sf( "DEF:%s_avg=%s:%s:AVERAGE", inst, rrd, ds ) }
131 if not rrasingle then
132 _ti( rv, _sf( "DEF:%s_min=%s:%s:MIN", inst, rrd, ds ) )
133 _ti( rv, _sf( "DEF:%s_max=%s:%s:MAX", inst, rrd, ds ) )
136 _ti( rv, _sf( "CDEF:%s_nnl=%s_avg,UN,0,%s_avg,IF", inst, inst, inst ) )
141 -- local helper: create cdefs depending on source options like flip and overlay
142 function __cdef(source)
147 -- find previous source, choose stack depending on flip state
149 prev = _stack_neg[#_stack_neg]
151 prev = _stack_pos[#_stack_pos]
154 -- is first source in stack or overlay source: source_stk = source_nnl
155 if not prev or source.overlay then
156 -- create cdef statement
157 _ti( rv, _sf( "CDEF:%s_stk=%s_nnl", source.sname, source.sname ) )
159 -- is subsequent source without overlay: source_stk = source_nnl + previous_stk
161 -- create cdef statement
163 "CDEF:%s_stk=%s_nnl,%s_stk,+", source.sname, source.sname, prev
167 -- create multiply by minus one cdef if flip is enabled
170 -- create cdef statement: source_stk = source_stk * -1
171 _ti( rv, _sf( "CDEF:%s_neg=%s_stk,-1,*", source.sname, source.sname ) )
173 -- push to negative stack if overlay is disabled
174 if not source.overlay then
175 _ti( _stack_neg, source.sname )
178 -- no flipping, push to positive stack if overlay is disabled
179 elseif not source.overlay then
181 -- push to positive stack
182 _ti( _stack_pos, source.sname )
185 -- calculate total amount of data if requested
188 "CDEF:%s_avg_sample=%s_avg,UN,0,%s_avg,IF,sample_len,*",
189 source.sname, source.sname, source.sname
193 "CDEF:%s_avg_sum=PREV,UN,0,PREV,IF,%s_avg_sample,+",
194 source.sname, source.sname, source.sname
202 -- local helper: create cdefs required for calculating total values
203 function __cdef_totals()
206 _sf( "CDEF:mytime=%s_avg,TIME,TIME,IF", opts.sources[1].sname ),
207 "CDEF:sample_len_raw=mytime,PREV(mytime),-",
208 "CDEF:sample_len=sample_len_raw,UN,0,sample_len_raw,IF"
215 -- local helper: create line and area statements
216 function __area(source)
223 -- find colors: try source, then opts.colors; fall back to random color
224 if type(source.color) == "string" then
225 line_color = source.color
226 area_color = self.colors:from_string( line_color )
227 elseif type(opts.colors[source.name:gsub("[^%w]","_")]) == "string" then
228 line_color = opts.colors[source.name:gsub("[^%w]","_")]
229 area_color = self.colors:from_string( line_color )
231 area_color = self.colors:random()
232 line_color = self.colors:to_string( area_color )
235 -- derive area background color from line color
236 area_color = self.colors:to_string( self.colors:faded( area_color ) )
238 -- choose source_stk or source_neg variable depending on flip state
246 legend = _sf( "%-" .. _longest_name .. "s", source.name )
248 -- create area and line1 statement
250 _sf( "AREA:%s_%s#%s", source.sname, var, area_color ),
251 _sf( "LINE1:%s_%s#%s:%s", source.sname, var, line_color, legend )
255 -- local helper: create gprint statements
256 function __gprint(source)
259 local numfmt = opts.number_format or "%6.1lf"
260 local totfmt = opts.totals_format or "%5.1lf%s"
262 -- don't include MIN if rrasingle is enabled
263 if not rrasingle then
264 _ti( rv, _sf( "GPRINT:%s_min:MIN:%s Min", source.sname, numfmt ) )
267 -- always include AVERAGE
268 _ti( rv, _sf( "GPRINT:%s_avg:AVERAGE:%s Avg", source.sname, numfmt ) )
270 -- don't include MAX if rrasingle is enabled
271 if not rrasingle then
272 _ti( rv, _sf( "GPRINT:%s_max:MAX:%s Max", source.sname, numfmt ) )
275 -- include total count if requested else include LAST
277 _ti( rv, _sf( "GPRINT:%s_avg_sum:LAST:(ca. %s Total)", source.sname, totfmt ) )
279 _ti( rv, _sf( "GPRINT:%s_avg:LAST:%s Last", source.sname, numfmt ) )
283 rv[#rv] = rv[#rv] .. "\\l"
291 _ti( images, opts.image )
293 -- insert provided addition rrd options
294 self:_push( { "-t", opts.title or "Unknown title" } )
295 self:_push( opts.rrd )
297 -- store index and safe instance name within each source object,
298 -- find longest instance name
299 for i, source in ipairs(opts.sources) do
301 if source.name:len() > _longest_name then
302 _longest_name = source.name:len()
310 source.sname = i .. source.name:gsub("[^A-Za-z0-9%-_]","_")
313 -- create DEF statements for each instance, find longest instance name
314 for i, source in ipairs(opts.sources) do
315 self:_push( __def( source ) )
318 -- create CDEF required for calculating totals
319 self:_push( __cdef_totals() )
321 -- create CDEF statements for each instance in reversed order
322 for i, source in ipairs(opts.sources) do
323 self:_push( __cdef( opts.sources[1 + #opts.sources - i] ) )
326 -- create LINE1, AREA and GPRINT statements for each instance
327 for i, source in ipairs(opts.sources) do
328 self:_push( __area( source ) )
329 self:_push( __gprint( source ) )
335 function Graph.render( self, host, plugin, plugin_instance )
337 dtype_instances = dtype_instances or { "" }
340 -- check for a whole graph handler
341 local plugin_def = "luci.statistics.rrdtool.definitions." .. plugin
342 local stat, def = pcall( require, plugin_def )
344 if stat and def and type(def.rrdargs) == "function" then
345 for i, opts in ipairs( self:_forcelol( def.rrdargs( self, host, plugin, plugin_instance, dtype ) ) ) do
346 for i, png in ipairs( self:_generic( opts ) ) do
347 table.insert( pngs, png )
358 -- no graph handler, iterate over data types
359 for i, dtype in ipairs( self.tree:data_types( plugin, plugin_instance ) ) do
361 -- check for data type handler
362 local dtype_def = plugin_def .. "." .. dtype
363 local stat, def = pcall( require, dtype_def )
365 if stat and def and type(def.rrdargs) == "function" then
366 for i, opts in ipairs( self:_forcelol( def.rrdargs( self, host, plugin, plugin_instance, dtype ) ) ) do
367 for i, png in ipairs( self:_generic( opts ) ) do
368 table.insert( pngs, png )
379 -- no data type handler, fall back to builtin definition
380 if type(self.defs.definitions[dtype]) == "table" then
382 -- iterate over data type instances
383 for i, inst in ipairs( self.tree:data_instances( plugin, plugin_instance, dtype ) ) do
385 local title = self:mktitle( host, plugin, plugin_instance, dtype, inst )
386 local png = self:mkpngpath( host, plugin, plugin_instance, dtype, inst )
387 local rrd = self:mkrrdpath( host, plugin, plugin_instance, dtype, inst )
389 self:_push( { "-t", title } )
390 self:_push( self.defs.definitions[dtype] )
392 table.insert( pngs, png )
395 self:_rrdtool( png, rrd )