* luci/statistics: ffluci -> luci conversation
[project/luci.git] / applications / luci-statistics / src / statistics / rrdtool.lua
1 module("luci.statistics.rrdtool", package.seeall)
2
3 require("luci.statistics.datatree")
4 require("luci.statistics.rrdtool.colors")
5 require("luci.statistics.rrdtool.definitions")
6 require("luci.util")
7 require("luci.bits")
8 require("luci.fs")
9
10
11 Graph = luci.util.class()
12
13 function Graph.__init__( self, timespan, opts )
14
15         opts = opts or { }
16         opts.width = opts.width or "400"
17
18         self.colors = luci.statistics.rrdtool.colors.Instance()
19         self.defs   = luci.statistics.rrdtool.definitions.Instance()
20         self.tree   = luci.statistics.datatree.Instance()
21
22         -- rrdtool defalt args
23         self.args   = {
24                 "-a", "PNG",
25                 "-s", "NOW-" .. ( timespan or 900 ),
26                 "-w", opts.width
27         }
28 end
29
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
34         end
35         t = t .. "/" .. dtype
36         if type(dtype_instance) == "string" and dtype_instance:len() > 0 then
37                 t = t .. "-" .. dtype_instance
38         end
39         return t
40 end
41
42 function Graph.mkrrdpath( self, ... )
43         return string.format( "/tmp/%s.rrd", self:mktitle( ... ) )
44 end
45
46 function Graph.mkpngpath( self, ... )
47         return string.format( "/tmp/rrdimg/%s.png", self:mktitle( ... ) )
48 end
49
50 function Graph._push( self, elem )
51
52         if type(elem) == "string" then
53                 table.insert( self.args, elem )
54         else
55                 for i, item in ipairs(elem) do
56                         table.insert( self.args, item )
57                 end
58         end
59
60         return( self.args )
61 end
62
63 function Graph._clearargs( self )
64         for i = #self.args, 7, -1 do
65                 table.remove( self.args, i )
66         end
67 end
68
69 function Graph._forcelol( self, list )
70         if type(list[1]) ~= "table" then
71                 return( { list } )
72         end
73         return( list )
74 end
75
76 function Graph._rrdtool( self, png, rrd )
77
78         -- prepare directory
79         local dir = png:gsub("/[^/]+$","")
80         luci.fs.mkdir( dir, true )
81
82         -- construct commandline
83         local cmdline = "rrdtool graph " .. png
84
85         for i, opt in ipairs(self.args) do
86
87                 opt = opt .. ""    -- force string
88                 
89                 if rrd then
90                         opt = opt:gsub( "{file}", rrd )
91                 end
92
93                 if opt:match("[^%w]") then
94                         cmdline = cmdline .. " '" .. opt .. "'"
95                 else
96                         cmdline = cmdline .. " " .. opt
97                 end
98         end
99
100         -- execute rrdtool
101         local rrdtool = io.popen( cmdline )
102         rrdtool:close()
103 end
104
105 function Graph._generic( self, opts )
106
107         local images = { }
108
109         -- remember images
110         table.insert( images, opts.image )
111
112         -- insert provided addition rrd options
113         self:_push( { "-t", opts.title or "Unknown title" } )
114         self:_push( opts.rrd )
115
116         -- construct an array of safe instance names
117         local inst_names = { }
118         for i, source in ipairs(opts.sources) do
119                 inst_names[i] = i .. source.name:gsub("[^A-Za-z0-9%-_]","_")
120         end
121
122         -- create DEF statements for each instance, find longest instance name
123         local longest_name = 0
124         for i, source in ipairs(opts.sources) do
125                 if source.name:len() > longest_name then
126                         longest_name = source.name:len()
127                 end
128
129                 local ds = source.ds or "value"
130
131                 self:_push( "DEF:" .. inst_names[i] .. "_min=" ..source.rrd .. ":" .. ds .. ":MIN" )
132                 self:_push( "DEF:" .. inst_names[i] .. "_avg=" ..source.rrd .. ":" .. ds .. ":AVERAGE" )
133                 self:_push( "DEF:" .. inst_names[i] .. "_max=" ..source.rrd .. ":" .. ds .. ":MAX" )
134                 self:_push( "CDEF:" .. inst_names[i] .. "_nnl=" .. inst_names[i] .. "_avg,UN,0," .. inst_names[i] .. "_avg,IF" )
135         end
136
137         -- create CDEF statement for last instance name
138         self:_push( "CDEF:" .. inst_names[#inst_names] .. "_stk=" .. inst_names[#inst_names] .. "_nnl" )
139
140         -- create CDEF statements for each instance
141         for i, source in ipairs(inst_names) do
142                 if i > 1 then
143                         self:_push(
144                                 "CDEF:" ..
145                                 inst_names[1 + #inst_names - i] .. "_stk=" ..
146                                 inst_names[1 + #inst_names - i] .. "_nnl," ..
147                                 inst_names[2 + #inst_names - i] .. "_stk,+"
148                         )
149                 end
150         end
151
152         -- create LINE and GPRINT statements for each instance
153         for i, source in ipairs(opts.sources) do
154
155                 local legend = string.format(
156                         "%-" .. longest_name .. "s",
157                         source.name
158                 )
159
160                 local numfmt = opts.number_format or "%6.1lf"
161
162                 local line_color
163                 local area_color
164
165                 -- find color: try source, then opts.colors; fall back to random color
166                 if type(source.color) == "string" then
167                         line_color = source.color
168                         area_color = self.colors:from_string( line_color )
169                 elseif type(opts.colors[source.name:gsub("[^%w]","_")]) == "string" then
170                         line_color = opts.colors[source.name:gsub("[^%w]","_")]
171                         area_color = self.colors:from_string( line_color )
172                 else
173                         area_color = self.colors:random()
174                         line_color = self.colors:to_string( area_color )
175                 end
176
177                 -- derive area background color from line color
178                 area_color = self.colors:to_string( self.colors:faded( area_color ) )
179
180
181                 self:_push( "AREA:"   .. inst_names[i] .. "_stk#" .. area_color )
182                 self:_push( "LINE1:"  .. inst_names[i] .. "_stk#" .. line_color .. ":" .. legend )
183                 self:_push( "GPRINT:" .. inst_names[i] .. "_min:MIN:" .. numfmt .. " Min" )
184                 self:_push( "GPRINT:" .. inst_names[i] .. "_avg:AVERAGE:" .. numfmt .. " Avg" )
185                 self:_push( "GPRINT:" .. inst_names[i] .. "_max:MAX:" .. numfmt .. " Max" )
186                 self:_push( "GPRINT:" .. inst_names[i] .. "_avg:LAST:" .. numfmt .. " Last\\l" )
187         end
188
189         return images
190 end
191
192 function Graph.render( self, host, plugin, plugin_instance )
193
194         dtype_instances = dtype_instances or { "" }
195         local pngs = { }
196
197         -- check for a whole graph handler
198         local plugin_def = "luci.statistics.rrdtool.definitions." .. plugin
199         local stat, def = pcall( require, plugin_def )
200
201         if stat and def and type(def.rrdargs) == "function" then
202                 for i, opts in ipairs( self:_forcelol( def.rrdargs( self, host, plugin, plugin_instance, dtype ) ) ) do
203                         for i, png in ipairs( self:_generic( opts ) ) do
204                                 table.insert( pngs, png )
205
206                                 -- exec
207                                 self:_rrdtool( png )
208
209                                 -- clear args
210                                 self:_clearargs()
211                         end
212                 end
213         else
214
215                 -- no graph handler, iterate over data types
216                 for i, dtype in ipairs( self.tree:data_types( plugin, plugin_instance ) ) do
217
218                         -- check for data type handler
219                         local dtype_def = plugin_def .. "." .. dtype
220                         local stat, def = pcall( require, dtype_def )
221
222                         if stat and def and type(def.rrdargs) == "function" then
223                                 for i, opts in ipairs( self:_forcelol( def.rrdargs( self, host, plugin, plugin_instance, dtype ) ) ) do
224                                         for i, png in ipairs( self:_generic( opts ) ) do
225                                                 table.insert( pngs, png )
226
227                                                 -- exec
228                                                 self:_rrdtool( png )
229
230                                                 -- clear args
231                                                 self:_clearargs()
232                                         end
233                                 end
234                         else
235
236                                 -- no data type handler, fall back to builtin definition
237                                 if type(self.defs.definitions[dtype]) == "table" then
238
239                                         -- iterate over data type instances
240                                         for i, inst in ipairs( self.tree:data_instances( plugin, plugin_instance, dtype ) ) do
241
242                                                 local title = self:mktitle( host, plugin, plugin_instance, dtype, inst )
243                                                 local png   = self:mkpngpath( host, plugin, plugin_instance, dtype, inst )
244                                                 local rrd   = self:mkrrdpath( host, plugin, plugin_instance, dtype, inst )
245
246                                                 self:_push( { "-t", title } )
247                                                 self:_push( self.defs.definitions[dtype] )
248
249                                                 table.insert( pngs, png )
250
251                                                 -- exec
252                                                 self:_rrdtool( png, rrd )
253
254                                                 -- clear args
255                                                 self:_clearargs()
256                                         end
257                                 end
258                         end
259                 end
260         end
261
262         return pngs
263 end
264