chiark / gitweb /
5133ffb9d5f36708fd32b447e83c8002b22844f2
[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_sources;
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     push @{ $news_sources{$site}{$inout} }, $src;
178 }
179
180 our @news_graphs;
181
182 foreach my $site (keys %news_sources) {
183     my $sk= $site;
184     for (;;) {
185         last unless $sk =~
186             s/^[^.]*(?:chiark|greenend|news|nntp|peer|feed|in|out)[^.]*\.//;
187         $sk .= " $&";
188     }
189     foreach my $inout (keys %{ $news_sources{$site} }) {
190         push @news_graphs, [ "$sk $inout", $site, $inout ];
191     }
192 }
193
194 foreach my $siteinfo (sort { $a->[0] cmp $b->[0] } @news_graphs) {
195     my ($sortkey, $site, $inout)= @$siteinfo;
196     my @sources= @{ $news_sources{$site}{$inout} };
197
198     my @vals= $inout eq 'out'
199         ? qw(missing deferred unwanted accepted rejected body_missing)
200         : qw(accepted refused rejected duplicate
201              accepted_size duplicate_size);
202     my @defs;
203     foreach my $val (@vals) {
204         my $def= "CDEF:$val=0";
205         foreach my $si (0..$#sources) {
206             my $src= $sources[$si];
207             my $tvar= "${val}_${si}";
208             push @defs, "DEF:$tvar=$src:$val:AVERAGE";
209             $def .= ",$tvar,ADDNAN";
210         }
211         push @defs, $def;
212         if ($val =~ m/_size$/) {
213             push @defs, "CDEF:kb_$`=$val,1024,/";
214         }
215     }
216     graph_of_group("News", $site, $inout,
217           {
218                 Units => '[art/s]',
219                 TimeRanges => [ map { $_*86400 } qw(1 7 31), 366, 366*3 ]
220             }, $inout eq 'out' ?
221           [
222            @defs,
223            "AREA:accepted#00f:ok",
224            "AREA:body_missing#ff0:miss:STACK",
225            "AREA:rejected#f00:rej:STACK",
226            "AREA:unwanted#aaa:unw:STACK",
227            "AREA:deferred#ddd:defer:STACK",
228            ] :
229           [
230            @defs,
231            "AREA:accepted#00f:ok:STACK",
232            "AREA:rejected#f00:rej:STACK",
233            "AREA:duplicate#000:dupe:STACK",
234            "AREA:refused#aaa:unw:STACK",
235            "CDEF:kb_accepted_smooth=kb_accepted,<interval/60>,TREND",
236            "LINE:kb_duplicate#ff0:kb dupe",
237            "LINE:kb_accepted_smooth#008:~kb",
238            ]);
239 }
240
241 our %disk_rdev2rrd;
242
243 foreach my $physdiskrrd (<$R/disk-*/disk_octets.rrd>) {
244     $physdiskrrd =~ s,octets\.rrd$,, or die;
245     $physdiskrrd =~ m,-([^/]+)/disk_$, or die;
246     my $physdev= "/dev/$1";
247     if (!stat $physdev) {
248         die "$physdev $!" unless $!==&ENOENT;
249         next;
250     }
251     die "$physdev ?" unless S_ISBLK((stat _)[2]);
252     $disk_rdev2rrd{(stat _)[6]}= $physdiskrrd;
253 }
254
255 our @disk_vgs;
256
257 sub lvgraphs {
258     my ($vg, $label, $factor, $rcolour, $wcolour) = @_;
259     my @lvs;
260     my $varname= $vg;
261     $varname =~ s/[^0-9a-zA-Y]/ sprintf "Z%02x", ord($&) /ge;
262     my $vginfo= {
263         Name => $label,
264         Varname => $varname,
265         Colour => { 'read' => $rcolour, 'write' => $wcolour },
266         Lvs => []
267     };
268     foreach my $bo (qw(octets ops)) {
269         foreach my $rw (qw(read write)) {
270             $vginfo->{VarDefs}{$bo}{$rw}= [];
271             $vginfo->{Sumdef}{$bo}{$rw}= '0';
272         }
273     }
274     my $ix=0;
275     foreach my $lvpath (</dev/$vg/*>) {
276         my $lv= $lvpath; $lv =~ s,.*/,,;
277         if (!stat $lvpath) {
278             die "$lvpath $!" unless $!==&ENOENT;
279             next;
280         }
281         die "$lvpath ?" unless S_ISBLK((stat _)[2]);
282         my $rrd= $disk_rdev2rrd{(stat _)[6]};
283         next unless defined $rrd;
284
285         my $lvinfo= { Name => $lv };
286         push @{ $vginfo->{Lvs} }, $lvinfo;
287
288         foreach my $bo (qw(octets ops)) {
289             $lvinfo->{Defs}{$bo}=
290               [
291                (map { ("DEF:$_=${rrd}${bo}.rrd:$_:AVERAGE") } qw(read write)),
292                "CDEF:mwrite=0,write,-",
293                "AREA:read#00f:read",
294                "AREA:mwrite#f00:write"
295                ];
296
297             foreach my $rw (qw(read write)) {
298                 $ix++;
299                 my $tvar= "lv_${rw}_${bo}_${varname}_${ix}";
300                 push @{ $vginfo->{VarDefs}{$bo}{$rw} },
301                     "DEF:$tvar=${rrd}${bo}.rrd:$rw:AVERAGE";
302                 $vginfo->{Sumdef}{$bo}{$rw} .= ",$tvar,+";
303             }
304         }
305     }
306     foreach my $bo (qw(octets ops)) {
307         foreach my $rw (qw(read write)) {
308             my $defs= [];
309             push @$defs, @{ $vginfo->{VarDefs}{$bo}{$rw} };
310             push @$defs, "CDEF:${rw}_vg_${varname}=".
311                 $vginfo->{Sumdef}{$bo}{$rw}.
312                 sprintf(",%f,*", $rw eq 'write' ? -$factor : $factor);
313             $vginfo->{Defs}{$bo}{$rw}= $defs;
314         }
315     }
316     push @disk_vgs, $vginfo;
317 }
318
319 lvgraphs('vg-main',          'main',     1, qw(00f f00));
320 lvgraphs('vg-chiark-stripe', 'stripe', 0.5, qw(008 800));
321
322 foreach my $bo (qw(octets ops)) {
323     my @a= ();
324     foreach my $rw (qw(read write)) {
325         my $stack= '';
326         foreach my $vginfo (@disk_vgs) {
327             push @a, @{ $vginfo->{Defs}{$bo}{$rw} };
328             push @a, "AREA:${rw}_vg_$vginfo->{Varname}#".
329                 $vginfo->{Colour}{$rw}.
330                 ":$vginfo->{Name} ".substr($rw,0,1).
331                 $stack;
332             $stack= ':STACK';
333         }
334     }
335     graph_of_group('IO', 'IO', $bo, { Units => '[/s]' }, \@a);
336 }
337
338 foreach my $vginfo (@disk_vgs) {
339     foreach my $bo (qw(octets ops)) {
340         foreach my $lv (@{ $vginfo->{Lvs} }) {
341             graph_of_group('IO', "$vginfo->{Name} $lv->{Name}",
342                            $bo, { Units => '[/s]' }, $lv->{Defs}{$bo});
343         }
344     }
345 }
346
347 push @{ $section_groups{General} }, {
348     Section => 'IO',
349     Group => 'IO',
350     UrlParams => "section=IO&sloth=SLOTH"
351 };
352
353 }
354 #---------- right, that was the initialisation ----------
355
356 our $self= url(-relative=>1);
357
358 if (param('debug')) {
359     print "Content-Type: text/plain\n\n";
360 }
361
362 our @navsettings;
363
364 @navsettings= ();
365
366 sub navsetting ($) {
367     my ($nav) = @_;
368     my $var= $nav->{Variable};
369     $$var= param($nav->{Param});
370     $$var= $nav->{Default} if !defined $$var;
371     die $nav->{Param} unless grep { $_ eq $$var } @{ $nav->{Values} };
372     push @navsettings, $nav;
373 }
374
375 our $section;
376
377 navsetting({
378     Desc => 'Section',
379     Param => 'section',
380     Variable => \$section,
381     Default => $sections[0],
382     Values => [@sections],
383     Show => sub { return $_[0]; }
384 });
385
386
387 sub num_param ($$$$) {
388     my ($param,$def,$min,$max) = @_;
389     my $v= param($param);
390     return $def if !defined $v;
391     $v =~ m/^([1-9]\d{0,8})$/ or die;
392     $v= $1;
393     die unless $v >= $min && $v <= $max;
394     return $v + 0;
395 }
396
397 our $group= param('graph');
398
399 our $elem= param('elem');
400 if (defined $elem) {
401     my $g= $graphs{$section,$group,$elem};
402     die unless $g;
403
404     my $width= num_param('w',370,100,1600);
405     my $height= num_param('h',200,100,1600);
406
407     my $sloth= param('sloth');
408     die unless defined $sloth;
409     $sloth =~ m/^(\d+)$/ or die;
410     $sloth= $1+0;
411     my $end= $g->{TimeRanges}[$sloth];
412     die unless defined $end;
413
414     my $cacheid= "$section!$group!$elem!$sloth!$width!$height";
415     my $cachepath= "cache/$cacheid.png";
416
417     my @args= @{ $g->{Args} };
418     s,\<interval/(\d+)\>, $end/$1 ,ge foreach @args;
419     unshift @args, qw(--end now --start), "end-${end}s";
420     
421     my $title= $group;
422     if (length $elem) { $title.= " $elem"; }
423
424     $title .= " $g->{Units}" if $g->{Units};
425     unshift @args, '-t', $title, '-w',$width, '-h',$height;
426     unshift @args, qw(-a PNG --full-size-mode);
427
428     if (param('debug')) {
429         print((join "\n",@args),"\n"); exit 0;
430     }
431
432 #print STDERR "||| ",(join ' ', map { "'$_'" } @args)." |||\n";
433     exec(qw(sh -ec), <<'END', 'x', $cachepath, @args);
434         p="$1"; shift
435         rrdtool graph "$p" --lazy "$@" >/dev/null
436         printf "Content-Type: image/png\n\n"
437         exec cat "$p"
438 END
439     die $!;
440 }
441
442 sub start_page ($) {
443     my ($title) = @_;
444     print header(), start_html($title);
445     my $outerdelim= '';
446     foreach my $nav (@navsettings) {
447         print $outerdelim;
448         print $nav->{Desc}, ": ";
449         my $delim= '';
450         my $current= $nav->{Variable};  $current= $$current;
451         foreach my $couldbe (@{ $nav->{Values} }) {
452             print $delim;
453             my $show= $nav->{Show}($couldbe);
454             if ($couldbe eq $current) {
455                 print "<b>$show</b>";
456             } else {
457                 my $u= $self;
458                 my $delim2= '?';
459                 foreach my $nav2 (@navsettings) {
460                     my $current2= $nav2->{Variable};  $current2= $$current2;
461                     $current2= $couldbe if $nav2->{Param} eq $nav->{Param};
462                     next if $current2 eq $nav2->{Default};
463                     $u .= $delim2;  $u .= "$nav2->{Param}=$current2";
464                     $delim2= '&';
465                 }
466                 print a({href=>$u}, $show);
467             }
468             $delim= ' | ';
469         }
470         $outerdelim= "<br>\n";
471     }
472     print "\n";
473
474     print h1("$title");
475 }
476
477 our $detail= param('detail');
478 if (defined $detail) {
479     my $elems= $group_elems{$section,$detail};
480     die unless $elems;
481     start_page("$detail - $section - graphs");
482     foreach my $tsloth (0..5) {
483         foreach my $elem (@$elems) {
484             my $g= $graphs{$section,$detail,$elem};
485             die unless $g;
486             next if $tsloth >= @{ $g->{TimeRanges} };
487             my $imgurl= "$self?graph=$detail&section=$section".
488                 "&sloth=$tsloth&elem=$elem";
489             print a({href=>"$imgurl&w=780&h=800"},
490                     img({src=>$imgurl, alt=>''}));
491         }
492     }
493     print end_html();
494     exit 0;
495 }
496
497 our $sloth;
498
499 navsetting({
500     Desc => 'Time interval',
501     Param => 'sloth',
502     Variable => \$sloth,
503     Default => 1,
504     Values => [0..3],
505     Show => sub {
506         my ($sl) = @_;
507         return ('Narrower', 'Normal', 'Wider', 'Extra wide')[$sl];
508     }
509 });
510
511 if (param('debug')) {
512     use Data::Dumper;
513     print Dumper(\%graphs);
514     exit 0;
515 }
516
517 start_page("$section - graphs");
518
519 foreach my $group (@{ $section_groups{$section} }) {
520     my $ref_group= $group;
521     my $ref_section= $section;
522     my $ref_urlparams= "detail=$group&section=$section";
523     if (ref $group) {
524         $ref_group= $group->{Group};
525         $ref_section= $group->{Section};
526         $ref_urlparams= $group->{UrlParams};
527         $ref_urlparams =~ s/\bSLOTH\b/$sloth/;
528     }
529     print a({href=>"$self?$ref_urlparams"});
530     my $imgurl= "$self?graph=$ref_group&section=$ref_section";
531     print "<span style=\"white-space:nowrap\">";
532     my $elems= $group_elems{$ref_section,$ref_group};
533     foreach my $elem (@$elems) {
534         my $g= $graphs{$ref_section,$ref_group,$elem};
535         print img({src=>"$imgurl&elem=$elem&sloth=".($sloth + $g->{Slower}),
536                    alt=>''});
537     }
538     print "</span>";
539     print "</a>\n";
540 }
541