chiark / gitweb /
wip disk space graphs
[rrd-graphs.git] / cgi
1 #!/usr/bin/speedy -w -- -t100 -M1
2
3 use strict qw(vars);
4 use CGI::SpeedyCGI qw/:standard -no_xhtml/;
5 use CGI qw/:standard -no_xhtml/;
6 use POSIX;
7
8 sub fail ($) {
9     print(header(-status=>500),
10           start_html('Error'),
11           h1('Error'),
12           escapeHTML($_[0]),
13           end_html());
14     exit 0;
15 }
16
17 our (@sections, %section_groups, %group_elems, %graphs);
18
19 #---------- initialisation code, run once - graphs setup ----------
20
21 BEGIN {
22
23 our $R= '/var/lib/collectd/rrd/chiark.greenend.org.uk';
24 our $SELF= '/home/ijackson/things/rrd-graphs';
25
26 our @timeranges= (3600, map { $_*86400 } qw(1 7 28), 13*7+1, 366);
27
28 sub graph_of_group ($$$$$) {
29     my ($section, $group, $elem, $basis, $args) = @_;
30     $basis->{Args}= $args;
31     $basis->{Slower}= 0 unless exists $basis->{Slower};
32     $basis->{TimeRanges} ||= \@timeranges;
33     $graphs{$section,$group,$elem}= $basis;
34     if (!exists $group_elems{$section,$group}) {
35         # new group then
36         if (!exists $section_groups{$section}) {
37             # new section even
38             push @sections, $section;
39         }
40         push @{ $section_groups{$section} }, $group;
41     }
42     push @{ $group_elems{$section,$group} }, $elem;
43 }
44
45 sub graph ($$$$) {
46     my ($section, $gname, $basis, $args) = @_;
47     graph_of_group($section, $gname,'', $basis, $args);
48 }
49
50 graph('General', 'Load and processes', { },
51       [
52        "DEF:load=$R/load/load.rrd:shortterm:AVERAGE",
53        (map { "DEF:$_=$R/processes/ps_state-$_.rrd:value:AVERAGE" }
54             qw(blocked running stopped paging sleeping zombies)),
55        "AREA:running#88f:running:STACK",
56        "AREA:blocked#8f8:disk wait:STACK",
57        "AREA:paging#f88:paging:STACK",
58        "LINE:load#000:load",
59        ]);
60
61 graph('General', 'Processes', { },
62       [
63        (map { "DEF:$_=$R/processes/ps_state-$_.rrd:value:AVERAGE" }
64             qw(blocked running stopped paging sleeping zombies)),
65        "CDEF:busy=0".(join '', map { ",$_,+" } qw(running blocked paging)),
66        "AREA:sleeping#ccc:sleeping:STACK",
67        "AREA:stopped#00f:stopped:STACK",
68        "AREA:zombies#ff0:zombie:STACK",
69        "AREA:busy#000:busy:STACK",
70        ]);
71
72 graph('General', 'CPU', { Units => '[%]' },
73       [
74        (map {
75            my $thing= $_;
76            (map { "DEF:$thing$_=$R/cpu-$_/cpu-$thing.rrd:value:AVERAGE" }
77                 (0..7)),
78            "CDEF:$thing=0".join('', map { ",$thing$_,+" } (0..7)).",8.0,/";
79        } qw(idle interrupt nice softirq steal system user wait)),
80        "CDEF:allintr=softirq,steal,+,interrupt,+",
81        "AREA:allintr#ff0:interrupt:STACK",
82        "AREA:system#88f:system:STACK",
83        "AREA:user#00f:user:STACK",
84        "AREA:nice#ccc:nice:STACK",
85        "AREA:wait#f00:wait:STACK",
86        ]);
87
88 graph('General', 'Memory', { },
89       [ '-b',1024,
90        (map { "DEF:swap_$_=$R/swap/swap-$_.rrd:value:AVERAGE" }
91             qw(free used cached)),
92        (map { "DEF:mem_$_=$R/memory/memory-$_.rrd:value:AVERAGE" }
93             qw(buffered free used cached)),
94        "CDEF:c_swap_used=0,swap_used,-",
95        "CDEF:c_swap_cached=0,swap_cached,-",
96        "CDEF:c_swap_free=0,swap_free,-",
97        "AREA:c_swap_used#000:used swap",
98        "AREA:c_swap_cached#888:\"cached\" swap:STACK",
99 #       "AREA:c_swap_free#88f:free swap:STACK",
100        "AREA:mem_used#ff0:used memory",
101        "AREA:mem_buffered#00f:page cache:STACK",
102        "AREA:mem_cached#008:buffer cache:STACK",
103        "AREA:mem_free#ccc:unused memory:STACK",
104        ]);
105
106 graph('General', 'Network', { Units => '[/sec; tx +ve; errs x1000]' },
107       [
108        (map {
109            ("DEF:tx_$_=$R/interface/if_$_-eth0.rrd:tx:AVERAGE",
110             "DEF:rx_$_=$R/interface/if_$_-eth0.rrd:rx:AVERAGE",
111             "CDEF:mrx_$_=0,rx_$_,-")
112            } qw(octets packets errors)),
113        (map {
114            ("CDEF:${_}_kb=${_}_octets,1024,/",
115             "CDEF:${_}_errsx=${_}_errors,1000,*")
116            } qw(mrx tx)),
117        "AREA:tx_kb#080:kby",
118        "LINE:tx_packets#0f0:pkts",
119        "LINE:tx_errsx#000:errs",
120        "AREA:mrx_kb#008:kby",
121        "LINE:mrx_packets#00f:pkts",
122        "LINE:mrx_errsx#444:errs",
123       ]);
124
125 graph('General', 'Users', {  },
126       [
127        "DEF:users=$R/users/users.rrd:users:AVERAGE",
128        "LINE:users#008:users"
129        ]);
130
131 foreach my $src (<$R/df/df-*.rrd>) {
132     my $vol= $src;
133     $vol =~ s,\.rrd$,, or next;
134     $vol =~ s,.*/,,;
135     $vol =~ s,^df-,,;
136     graph('Disk space', $vol, {
137              Slower => 1,
138           },
139           [ '-A','-l',0,'-r',
140            qw(-b 1024 -l 0),
141            (map { "DEF:$_=$src:$_:AVERAGE" } qw(free used)),
142            "AREA:used#000:used:STACK",
143            "AREA:free#88f:free:STACK",
144            ]);
145 }
146
147 our %news_name_map;
148
149 if (!open NM, '<', "$SELF/data/news/name-map") {
150     die unless $!==&ENOENT;
151 } else {
152     while (<NM>) {
153         s/^\s*//; s/\s+$//;
154         next unless m/^[^\#]/;
155         m/^(\S+)\s+(in|out|\*)\s+(\S+)$/ or die;
156         if ($2 eq '*') {
157             $news_name_map{$1,$_}= $3 foreach qw(in out);
158         } else {
159             $news_name_map{$1,$2}= $3;
160         }
161     }
162 }
163
164 our @news_graphs;
165
166 foreach my $src (<$SELF/data/news/*.rrd>) {
167     my $site= $src;
168     $site =~ s,\.rrd$,, or next;
169     $site =~ s,.*/,,;
170     $site =~ s,_(in|out)$,,;
171     my $inout= $1;
172     $site =~ s/^([-.0-9a-z]+)_//;
173     my $us= $1; # all very well but we ignore it
174     my $newsite= $news_name_map{$site,$inout};
175     $site= $newsite if defined $newsite;
176     next if $site eq '-';
177     #my $sk= join '.', reverse split /\./, $site;
178     my $sk= $site;
179     $sk .= " $&" if $sk =~ s/^[^.]*(?:news|nntp|peer)[^.]*\.//;
180     $sk .= " $inout";
181     push @news_graphs, [ $sk, $site, $inout, $src ];
182 }
183
184 foreach my $siteinfo (sort { $a->[0] cmp $b->[0] } @news_graphs) {
185     my ($sortkey, $site, $inout, $src)= @$siteinfo;
186     graph_of_group("News", $site, $inout,
187           {
188                 Units => '[art/s]',
189                 TimeRanges => [ map { $_*86400 } qw(1 7 31), 366, 366*3 ]
190             }, $inout eq 'out' ?
191           [
192            (map { "DEF:$_=$src:$_:AVERAGE" }
193                 qw(missing deferred unwanted accepted rejected body_missing)),
194            "AREA:accepted#00f:ok",
195            "AREA:body_missing#ff0:miss:STACK",
196            "AREA:rejected#f00:rej:STACK",
197            "AREA:unwanted#aaa:unw:STACK",
198            "AREA:deferred#ddd:defer:STACK",
199            ] :
200           [
201            (map { "DEF:$_=$src:$_:AVERAGE" }
202                 qw(accepted refused rejected duplicate)),
203            (map { ("DEF:bytes_$_=$src:${_}_size:AVERAGE",
204                    "CDEF:kb_$_=bytes_$_,1024,/")
205               } qw(accepted duplicate)),
206            "AREA:accepted#00f:ok:STACK",
207            "AREA:rejected#f00:rej:STACK",
208            "AREA:duplicate#000:dupe:STACK",
209            "AREA:refused#aaa:unw:STACK",
210            "CDEF:kb_accepted_smooth=kb_accepted,<interval/60>,TREND",
211            "LINE:kb_duplicate#ff0:kb dupe",
212            "LINE:kb_accepted_smooth#008:~kb",
213            ]);
214 }
215
216 our %disk_rdev2rrd;
217
218 foreach my $physdiskrrd (<$R/disk-*/disk_octets.rrd>) {
219     $physdiskrrd =~ s,octets\.rrd$,, or die;
220     $physdiskrrd =~ m,-([^/]+)/disk_$, or die;
221     my $physdev= "/dev/$1";
222     if (!stat $physdev) {
223         die "$physdev $!" unless $!==&ENOENT;
224         next;
225     }
226     die "$physdev ?" unless S_ISBLK((stat _)[2]);
227     $disk_rdev2rrd{(stat _)[6]}= $physdiskrrd;
228 }
229
230 our @disk_vgs;
231
232 sub lvgraphs ($$) {
233     my ($vg, $factor) = @_;
234     my @lvs;
235     my $varname= $vg;
236     $varname =~ s/[^0-9a-zA-Y]/ sprintf "Z%02x", ord($&) /ge;
237     my $vginfo= {
238         Name => $vg,
239         Varname => $varname
240     };
241     foreach my $bo (qw(octets ops)) {
242         foreach my $rw (qw(read write)) {
243             $vginfo->{VarDefs}{$bo}{$rw}= [];
244             $vginfo->{Sumdef}{$bo}{$rw}= '0';
245         }
246     }
247     my $ix=0;
248     foreach my $lvpath (</dev/$vg/*>) {
249         my $lv= $lvpath; $lv =~ s,.*/,,;
250         if (!stat $lvpath) {
251             die "$lvpath $!" unless $!==&ENOENT;
252             next;
253         }
254         die "$lvpath ?" unless S_ISBLK((stat _)[2]);
255         my $rrd= $disk_rdev2rrd{(stat _)[6]};
256         next unless defined $rrd;
257
258         foreach my $bo (qw(octets ops)) {
259 0 and
260             graph_of_group('IO', "$vg $lv", $bo, { Units => '[/s]' },
261               [
262                (map { ("DEF:$_=${rrd}${bo}.rrd:$_:AVERAGE") } qw(read write)),
263                "CDEF:mwrite=0,write,-",
264                "AREA:read#00f:read",
265                "AREA:mwrite#f00:write"
266                ]);
267             foreach my $rw (qw(read write)) {
268                 $ix++;
269                 my $tvar= "lv_${rw}_${bo}_${varname}_${ix}";
270                 push @{ $vginfo->{VarDefs}{$bo}{$rw} },
271                     "DEF:$tvar=${rrd}${bo}.rrd:$rw:AVERAGE";
272                 $vginfo->{Sumdef}{$bo}{$rw} .= ",$tvar,+";
273             }
274         }
275     }
276     foreach my $bo (qw(octets ops)) {
277         my $defs= [];
278         foreach my $rw (qw(read write)) {
279             push @$defs, @{ $vginfo->{VarDefs}{$bo}{$rw} };
280             push @$defs, "CDEF:${rw}_vg_${varname}=".
281                 $vginfo->{Sumdef}{$bo}{$rw}.
282                 sprintf(",%f,*", $rw eq 'write' ? -$factor : $factor);
283         }
284         $vginfo->{Defs}{$bo}= $defs;
285     }
286     push @disk_vgs, $vginfo;
287 }
288
289 lvgraphs('vg-main', 1);
290 lvgraphs('vg-chiark-stripe', 0.5);
291
292 foreach my $bo (qw(octets ops)) {
293     my @a= ();
294     foreach my $rw (qw(read write)) {
295         my $stack= '';
296         foreach my $vginfo (@disk_vgs) {
297             push @a, @{ $vginfo->{Defs}{$bo} };
298             push @a, "AREA:${rw}_vg_$vginfo->{Varname}#0ff:$vginfo->{Name}"
299                 .$stack;
300             $stack= ':STACK';
301         }
302     }
303     graph_of_group('IO', 'T', $bo, { Units => '[/s]' }, \@a);
304 }
305
306 }
307 #---------- right, that was the initialisation ----------
308
309 our $self= url(-relative=>1);
310
311 if (param('debug')) {
312     print "Content-Type: text/plain\n\n";
313 }
314
315 our @navsettings;
316
317 @navsettings= ();
318
319 sub navsetting ($) {
320     my ($nav) = @_;
321     my $var= $nav->{Variable};
322     $$var= param($nav->{Param});
323     $$var= $nav->{Default} if !defined $$var;
324     die $nav->{Param} unless grep { $_ eq $$var } @{ $nav->{Values} };
325     push @navsettings, $nav;
326 }
327
328 our $section;
329
330 navsetting({
331     Desc => 'Section',
332     Param => 'section',
333     Variable => \$section,
334     Default => $sections[0],
335     Values => [@sections],
336     Show => sub { return $_[0]; }
337 });
338
339
340 sub num_param ($$$$) {
341     my ($param,$def,$min,$max) = @_;
342     my $v= param($param);
343     return $def if !defined $v;
344     $v =~ m/^([1-9]\d{0,8})$/ or die;
345     $v= $1;
346     die unless $v >= $min && $v <= $max;
347     return $v + 0;
348 }
349
350 our $group= param('graph');
351
352 our $elem= param('elem');
353 if (defined $elem) {
354     my $g= $graphs{$section,$group,$elem};
355     die unless $g;
356
357     my $width= num_param('w',370,100,1600);
358     my $height= num_param('h',200,100,1600);
359
360     my $sloth= param('sloth');
361     die unless defined $sloth;
362     $sloth =~ m/^(\d+)$/ or die;
363     $sloth= $1+0;
364     my $end= $g->{TimeRanges}[$sloth];
365     die unless defined $end;
366
367     my $cacheid= "$section!$group!$elem!$sloth!$width!$height";
368     my $cachepath= "cache/$cacheid.png";
369
370     my @args= @{ $g->{Args} };
371     s,\<interval/(\d+)\>, $end/$1 ,ge foreach @args;
372     unshift @args, qw(--end now --start), "end-${end}s";
373     
374     my $title= $group;
375     if (length $elem) { $title.= " $elem"; }
376
377     $title .= " $g->{Units}" if $g->{Units};
378     unshift @args, '-t', $title, '-w',$width, '-h',$height;
379     unshift @args, qw(-a PNG --full-size-mode);
380
381     if (param('debug')) {
382         print((join "\n",@args),"\n"); exit 0;
383     }
384
385 #print STDERR "||| ",(join ' ', map { "'$_'" } @args)." |||\n";
386     exec(qw(sh -ec), <<'END', 'x', $cachepath, @args);
387         p="$1"; shift
388         rrdtool graph "$p" --lazy "$@" >/dev/null
389         printf "Content-Type: image/png\n\n"
390         exec cat "$p"
391 END
392     die $!;
393 }
394
395 sub start_page ($) {
396     my ($title) = @_;
397     print header(), start_html($title);
398     my $outerdelim= '';
399     foreach my $nav (@navsettings) {
400         print $outerdelim;
401         print $nav->{Desc}, ": ";
402         my $delim= '';
403         my $current= $nav->{Variable};  $current= $$current;
404         foreach my $couldbe (@{ $nav->{Values} }) {
405             print $delim;
406             my $show= $nav->{Show}($couldbe);
407             if ($couldbe eq $current) {
408                 print "<b>$show</b>";
409             } else {
410                 my $u= $self;
411                 my $delim2= '?';
412                 foreach my $nav2 (@navsettings) {
413                     my $current2= $nav2->{Variable};  $current2= $$current2;
414                     $current2= $couldbe if $nav2->{Param} eq $nav->{Param};
415                     next if $current2 eq $nav2->{Default};
416                     $u .= $delim2;  $u .= "$nav2->{Param}=$current2";
417                     $delim2= '&';
418                 }
419                 print a({href=>$u}, $show);
420             }
421             $delim= ' | ';
422         }
423         $outerdelim= "<br>\n";
424     }
425     print "\n";
426
427     print h1("$title");
428 }
429
430 our $detail= param('detail');
431 if ($detail) {
432     my $elems= $group_elems{$section,$detail};
433     die unless $elems;
434     start_page("$detail graphs");
435     foreach my $tsloth (0..5) {
436         foreach my $elem (@$elems) {
437             my $g= $graphs{$section,$detail,$elem};
438             die unless $g;
439             next if $tsloth >= @{ $g->{TimeRanges} };
440             my $imgurl= "$self?graph=$detail&section=$section".
441                 "&sloth=$tsloth&elem=$elem";
442             print a({href=>"$imgurl&w=780&h=800"},
443                     img({src=>$imgurl, alt=>''}));
444         }
445     }
446     print end_html();
447     exit 0;
448 }
449
450 our $sloth;
451
452 navsetting({
453     Desc => 'Time interval',
454     Param => 'sloth',
455     Variable => \$sloth,
456     Default => 1,
457     Values => [0..3],
458     Show => sub {
459         my ($sl) = @_;
460         return ('Narrower', 'Normal', 'Wider', 'Extra wide')[$sl];
461     }
462 });
463
464 if (param('debug')) {
465     use Data::Dumper;
466     print Dumper(\%graphs);
467     exit 0;
468 }
469
470 start_page("$section graphs");
471
472 foreach my $group (@{ $section_groups{$section} }) {
473     print a({href=>"$self?detail=$group&section=$section"});
474     my $imgurl= "$self?graph=$group&section=$section";
475     print "<span style=\"white-space:nowrap\">";
476     my $elems= $group_elems{$section,$group};
477     foreach my $elem (@$elems) {
478         my $g= $graphs{$section,$group,$elem};
479         print img({src=>"$imgurl&elem=$elem&sloth=".($sloth + $g->{Slower}),
480                    alt=>''});
481     }
482     print "</span>";
483     print "</a>\n";
484 }
485