* ffluci/statistcs: implement rrdtool stuff, extend controller to public pages - wip
[project/luci.git] / applications / luci-statistics / src / statistics / rrdtool.lua
1 module("ffluci.statistics.rrdtool", package.seeall)
2
3 require("ffluci.statistics.datatree")
4 require("ffluci.statistics.rrdtool.colors")
5 require("ffluci.statistics.rrdtool.definitions")
6 require("ffluci.util")
7 require("ffluci.bits")
8 require("ffluci.fs")
9
10
11 Graph = ffluci.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 = ffluci.statistics.rrdtool.colors.Instance()
19         self.defs   = ffluci.statistics.rrdtool.definitions.Instance()
20         self.tree   = ffluci.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._rrdtool( self, png, rrd )
70         local cmdline = "rrdtool graph " .. png
71
72         for i, opt in ipairs(self.args) do
73
74                 opt = opt .. ""    -- force string
75                 
76                 if rrd then
77                         opt = opt:gsub( "{file}", rrd )
78                 end
79
80                 if opt:match("[^%w]") then
81                         cmdline = cmdline .. " '" .. opt .. "'"
82                 else
83                         cmdline = cmdline .. " " .. opt
84                 end
85         end
86
87         local rrdtool = io.popen( cmdline )
88         rrdtool:read("*a")
89         rrdtool:close()
90 end
91
92 function Graph._generic( self, optlist )
93
94         local images = { }
95
96         if type(optlist[1]) ~= "table" then
97                 optlist = { optlist }
98         end
99
100         for i, opts in ipairs(optlist) do
101                 -- remember images
102                 table.insert( images, opts.image )
103
104                 -- insert provided addition rrd options
105                 self:_push( { "-t", opts.title or "Unknown title" } )
106                 self:_push( opts.rrd )
107
108                 -- construct an array of safe instance names
109                 local inst_names = { }
110                 for i, source in ipairs(opts.sources) do
111                         inst_names[i] = i .. source.name:gsub("[^A-Za-z0-9%-_]","_")
112                 end
113
114                 -- create DEF statements for each instance, find longest instance name
115                 local longest_name = 0
116                 for i, source in ipairs(opts.sources) do
117                         if source.name:len() > longest_name then
118                                 longest_name = source.name:len()
119                         end
120
121                         self:_push( "DEF:" .. inst_names[i] .. "_min=" ..source.rrd .. ":value:MIN" )
122                         self:_push( "DEF:" .. inst_names[i] .. "_avg=" ..source.rrd .. ":value:AVERAGE" )
123                         self:_push( "DEF:" .. inst_names[i] .. "_max=" ..source.rrd .. ":value:MAX" )
124                         self:_push( "CDEF:" .. inst_names[i] .. "_nnl=" .. inst_names[i] .. "_avg,UN,0," .. inst_names[i] .. "_avg,IF" )
125                 end
126
127                 -- create CDEF statement for last instance name
128                 self:_push( "CDEF:" .. inst_names[#inst_names] .. "_stk=" .. inst_names[#inst_names] .. "_nnl" )
129
130                 -- create CDEF statements for each instance
131                 for i, source in ipairs(inst_names) do
132                         if i > 1 then
133                                 self:_push(
134                                         "CDEF:" ..
135                                         inst_names[1 + #inst_names - i] .. "_stk=" ..
136                                         inst_names[1 + #inst_names - i] .. "_nnl," ..
137                                         inst_names[2 + #inst_names - i] .. "_stk,+"
138                                 )
139                         end
140                 end
141
142                 -- create LINE and GPRINT statements for each instance
143                 for i, source in ipairs(opts.sources) do
144
145                         local legend = string.format(
146                                 "%-" .. longest_name .. "s",
147                                 source.name
148                         )
149
150                         local numfmt = opts.number_format or "%6.1lf"
151
152                         local line_color
153                         local area_color
154
155                         if type(opts.colors[source.name]) == "string" then
156                                 line_color = opts.colors[source.name]
157                                 area_color = self.colors:from_string( line_color )
158                         else
159                                 area_color = self.colors:random()
160                                 line_color = self.colors:to_string( area_color )
161                         end
162
163                         area_color = self.colors:to_string(
164                                 self.colors:faded( area_color )
165                         )
166
167                         self:_push( "AREA:"   .. inst_names[i] .. "_stk#" .. area_color )
168                         self:_push( "LINE1:"  .. inst_names[i] .. "_stk#" .. line_color .. ":" .. legend )
169                         self:_push( "GPRINT:" .. inst_names[i] .. "_min:MIN:" .. numfmt .. " Min" )
170                         self:_push( "GPRINT:" .. inst_names[i] .. "_avg:AVERAGE:" .. numfmt .. " Avg" )
171                         self:_push( "GPRINT:" .. inst_names[i] .. "_max:MAX:" .. numfmt .. " Max" )
172                         self:_push( "GPRINT:" .. inst_names[i] .. "_avg:LAST:" .. numfmt .. " Last\\l" )
173                 end
174         end
175
176         return images
177 end
178
179 function Graph.render( self, host, plugin, plugin_instance )
180
181         dtype_instances = dtype_instances or { "" }
182         local pngs = { }
183
184         -- check for a whole graph handler
185         local plugin_def = "ffluci.statistics.rrdtool.definitions." .. plugin
186         local stat, def = pcall( require, plugin_def )
187
188         if stat and def and type(def.rrdargs) == "function" then
189                 for i, png in ipairs( self:_generic( def.rrdargs( self, host, plugin, plugin_instance, dtype ) ) ) do
190                         table.insert( pngs, png )
191
192                         -- exec
193                         self:_rrdtool( png )
194
195                         -- clear args
196                         self:_clearargs()
197                 end
198         else
199
200                 -- no graph handler, iterate over data types
201                 for i, dtype in ipairs( self.tree:data_types( plugin, plugin_instance ) ) do
202
203                         -- check for data type handler
204                         local dtype_def = plugin_def .. "." .. dtype
205                         local stat, def = pcall( require, dtype_def )
206
207                         if stat and def and type(def.rrdargs) == "function" then
208                                 for i, png in ipairs( self:_generic( def.rrdargs( self, host, plugin, plugin_instance, dtype ) ) ) do
209                                         table.insert( pngs, png )
210
211                                         -- exec
212                                         self:_rrdtool( png )
213
214                                         -- clear args
215                                         self:_clearargs()
216                                 end
217                         else
218
219                                 -- no data type handler, fall back to builtin definition
220                                 if type(self.defs.definitions[dtype]) == "table" then
221
222                                         -- iterate over data type instances
223                                         for i, inst in ipairs( self.tree:data_instances( plugin, plugin_instance, dtype ) ) do
224
225                                                 local title = self:mktitle( host, plugin, plugin_instance, dtype, inst )
226                                                 local png   = self:mkpngpath( host, plugin, plugin_instance, dtype, inst )
227                                                 local rrd   = self:mkrrdpath( host, plugin, plugin_instance, dtype, inst )
228
229                                                 self:_push( { "-t", title } )
230                                                 self:_push( self.defs.definitions[dtype] )
231
232                                                 table.insert( pngs, png )
233
234                                                 -- exec
235                                                 self:_rrdtool( png, rrd )
236
237                                                 -- clear args
238                                                 self:_clearargs()
239                                         end
240                                 end
241                         end
242                 end
243         end
244
245         return pngs
246 end
247