chiark / gitweb /
c35a5b2fd2d06483db0b646a45876285957bc5d7
[rrd-graphs.git] / cgi
1 #!/usr/bin/speedy -w -- -t100 -M1
2 # -*- perl -*-
3 # Main CGI program's logic; must be run inside a lock.
4
5 # rrd-graphs/cgi - part of rrd-graphs, a tool for online graphs
6 # Copyright 2010, 2012 Ian Jackson
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU Affero General Public License for more details.
17 #
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22 use strict qw(vars);
23 use CGI::SpeedyCGI qw/:standard -no_xhtml/;
24 use CGI qw/:standard -no_xhtml/;
25 use POSIX;
26 use MD5;
27
28 sub fail ($) {
29     print(header(-status=>500),
30           start_html('Error'),
31           h1('Error'),
32           escapeHTML($_[0]),
33           end_html());
34     exit 0;
35 }
36
37 our (@sections, %section_groups, %group_elems, %graphs);
38
39 #---------- initialisation code, run once - graphs setup ----------
40
41 BEGIN {
42
43 our $R= '/var/lib/collectd/rrd/chiark.greenend.org.uk';
44 our $SELF= '/home/ijackson/things/rrd-graphs';
45
46 our @timeranges= (3600, map { $_*86400 } qw(1 7 28), 13*7+1, 366);
47
48 sub graph_of_group ($$$$$;$) {
49     my ($section, $group, $elem, $basis, $args, $title) = @_;
50     $basis->{Args}= $args;
51     $basis->{Slower}= 0 unless exists $basis->{Slower};
52     $basis->{TimeRanges} ||= \@timeranges;
53     if (!defined $title) {
54         $title = $group;
55         if (length $elem) { $title.= " $elem"; }
56     }
57     $basis->{Title} = $title;
58     $graphs{$section,$group,$elem}= $basis;
59     if (!exists $group_elems{$section,$group}) {
60         # new group then
61         if (!exists $section_groups{$section}) {
62             # new section even
63             push @sections, $section;
64         }
65         push @{ $section_groups{$section} }, $group;
66     }
67     push @{ $group_elems{$section,$group} }, $elem;
68 }
69
70 sub graph ($$$$) {
71     my ($section, $gname, $basis, $args) = @_;
72     graph_of_group($section, $gname,'', $basis, $args);
73 }
74
75 graph('General', 'Load and processes', { },
76       [
77        "DEF:load=$R/load/load.rrd:shortterm:AVERAGE",
78        (map { "DEF:$_=$R/processes/ps_state-$_.rrd:value:AVERAGE" }
79             qw(blocked running stopped paging sleeping zombies)),
80        "AREA:running#88f:running:STACK",
81        "AREA:blocked#8f8:disk wait:STACK",
82        "AREA:paging#f88:paging:STACK",
83        "LINE:load#000:load",
84        ]);
85
86 graph('General', 'Processes', { },
87       [
88        (map { "DEF:$_=$R/processes/ps_state-$_.rrd:value:AVERAGE" }
89             qw(blocked running stopped paging sleeping zombies)),
90        "CDEF:busy=0".(join '', map { ",$_,+" } qw(running blocked paging)),
91        "AREA:sleeping#ccc:sleeping:STACK",
92        "AREA:stopped#00f:stopped:STACK",
93        "AREA:zombies#ff0:zombie:STACK",
94        "AREA:busy#000:busy:STACK",
95        ]);
96
97 graph('General', 'CPU', { Units => '[%]' },
98       [
99        (map {
100            my $thing= $_;
101            (map { "DEF:$thing$_=$R/cpu-$_/cpu-$thing.rrd:value:AVERAGE" }
102                 (0..7)),
103            "CDEF:$thing=0".join('', map { ",$thing$_,+" } (0..7)).",8.0,/";
104        } qw(idle interrupt nice softirq steal system user wait)),
105        "CDEF:allintr=softirq,steal,+,interrupt,+",
106        "AREA:allintr#ff0:interrupt:STACK",
107        "AREA:system#88f:system:STACK",
108        "AREA:user#00f:user:STACK",
109        "AREA:nice#ccc:nice:STACK",
110        "AREA:wait#f00:wait:STACK",
111        ]);
112
113 graph('General', 'Memory', { },
114       [ '-b',1024,
115        (map { "DEF:swap_$_=$R/swap/swap-$_.rrd:value:AVERAGE" }
116             qw(free used cached)),
117        (map { "DEF:mem_$_=$R/memory/memory-$_.rrd:value:AVERAGE" }
118             qw(buffered free used cached)),
119        "CDEF:c_swap_used=0,swap_used,-",
120        "CDEF:c_swap_cached=0,swap_cached,-",
121        "CDEF:c_swap_free=0,swap_free,-",
122        "AREA:c_swap_used#000:used swap",
123        "AREA:c_swap_cached#888:\"cached\" swap:STACK",
124 #       "AREA:c_swap_free#88f:free swap:STACK",
125        "AREA:mem_used#ff0:used memory",
126        "AREA:mem_buffered#00f:page cache:STACK",
127        "AREA:mem_cached#008:buffer cache:STACK",
128        "AREA:mem_free#ccc:unused memory:STACK",
129        ]);
130
131 graph('General', 'Network', { Units => '[/sec; tx +ve; errs x1000]' },
132       [
133        (map {
134            ("DEF:tx_$_=$R/interface/if_$_-eth0.rrd:tx:AVERAGE",
135             "DEF:rx_$_=$R/interface/if_$_-eth0.rrd:rx:AVERAGE",
136             "CDEF:mrx_$_=0,rx_$_,-")
137            } qw(octets packets errors)),
138        (map {
139            ("CDEF:${_}_kb=${_}_octets,1024,/",
140             "CDEF:${_}_errsx=${_}_errors,1000,*")
141            } qw(mrx tx)),
142        "AREA:tx_kb#080:kby",
143        "LINE:tx_packets#0f0:pkts",
144        "LINE:tx_errsx#000:errs",
145        "AREA:mrx_kb#008:kby",
146        "LINE:mrx_packets#00f:pkts",
147        "LINE:mrx_errsx#444:errs",
148       ]);
149
150 graph('General', 'Users', {  },
151       [
152        "DEF:users=$R/users/users.rrd:users:AVERAGE",
153        "LINE:users#008:users"
154        ]);
155
156 foreach my $src (<$R/df/df-*.rrd>) {
157     my $vol= $src;
158     $vol =~ s,\.rrd$,, or next;
159     $vol =~ s,.*/,,;
160     $vol =~ s,^df-,,;
161     graph('Disk space', $vol, {
162              Slower => 1,
163           },
164           [ '-A','-l',0,'-r',
165            qw(-b 1024 -l 0),
166            (map { "DEF:$_=$src:$_:AVERAGE" } qw(free used)),
167            "AREA:used#000:used:STACK",
168            "AREA:free#88f:free:STACK",
169            ]);
170 }
171
172 our %news_name_map;
173
174 if (!open NM, '<', "$SELF/data/news/name-map") {
175     die unless $!==&ENOENT;
176 } else {
177     while (<NM>) {
178         s/^\s*//; s/\s+$//;
179         next unless m/^[^\#]/;
180         m/^(\S+)\s+(in|out|\*)\s+(\S+)$/ or die;
181         if ($2 eq '*') {
182             $news_name_map{$1,$_}= $3 foreach qw(in out);
183         } else {
184             $news_name_map{$1,$2}= $3;
185         }
186     }
187 }
188
189 our %news_sources;
190
191 foreach my $src (<$SELF/data/news/*.rrd>) {
192     my $site= $src;
193     $site =~ s,\.rrd$,, or next;
194     $site =~ s,.*/,,;
195     $site =~ s,_(in|out)$,,;
196     my $inout= $1;
197     $site =~ s/^([-.0-9a-z]+)_//;
198     my $us= $1; # all very well but we ignore it
199     my $newsite= $news_name_map{$site,$inout};
200     $site= $newsite if defined $newsite;
201     next if $site eq '-';
202     push @{ $news_sources{$site}{$inout} }, $src;
203 }
204
205 our @news_graphs;
206
207 foreach my $site (keys %news_sources) {
208     my $sk= $site;
209     $sk =~ s/^[.0-9]+$/~$&/;
210     for (;;) {
211         last unless $sk =~
212  s/^[^. ]*\b(?:chiark|greenend|news|newsfeed|nntp|peer|feed|in|out)\b[^.]*\.//;
213         $sk .= " $&";
214     }
215 print STDERR "$site => $sk\n";
216     foreach my $inout (keys %{ $news_sources{$site} }) {
217         my $skio = $sk;
218         $skio =~ s/ / [$inout]/;
219         push @news_graphs, [ $skio, $site, $inout ];
220     }
221 }
222
223 foreach my $siteinfo (sort { $a->[0] cmp $b->[0] } @news_graphs) {
224     my ($sortkey, $site, $inout)= @$siteinfo;
225     my @sources= @{ $news_sources{$site}{$inout} };
226
227     my @vals= $inout eq 'out'
228         ? qw(missing deferred unwanted accepted rejected body_missing)
229         : qw(accepted refused rejected duplicate
230              accepted_size duplicate_size);
231     my @defs;
232     foreach my $val (@vals) {
233         my $def= "CDEF:$val=0";
234         foreach my $si (0..$#sources) {
235             my $src= $sources[$si];
236             my $tvar= "${val}_${si}";
237             push @defs, "DEF:$tvar=$src:$val:AVERAGE";
238             $def .= ",$tvar,ADDNAN";
239         }
240         push @defs, $def;
241         if ($val =~ m/_size$/) {
242             push @defs, "CDEF:kb_$`=$val,1024,/";
243         }
244     }
245     my $group = $sortkey;
246     $group =~ s/ .*//;
247     $group = $site unless length $group;
248     graph_of_group("News", $group, "$site $inout",
249           {
250                 Units => '[art/s]',
251                 TimeRanges => [ map { $_*86400 } qw(1 7 31), 366, 366*3 ]
252             }, $inout eq 'out' ?
253           [
254            @defs,
255            "AREA:accepted#00f:ok",
256            "AREA:body_missing#ff0:miss:STACK",
257            "AREA:rejected#f00:rej:STACK",
258            "AREA:unwanted#aaa:unw:STACK",
259            "AREA:deferred#ddd:defer:STACK",
260            ] :
261           [
262            @defs,
263            "AREA:accepted#00f:ok:STACK",
264            "AREA:rejected#f00:rej:STACK",
265            "AREA:duplicate#000:dupe:STACK",
266            "AREA:refused#aaa:unw:STACK",
267            "CDEF:kb_accepted_smooth=kb_accepted,<interval/60>,TREND",
268            "LINE:kb_duplicate#ff0:kb dupe",
269            "LINE:kb_accepted_smooth#008:~kb",
270            ],
271         "$site $inout");
272 }
273
274 our %disk_rdev2rrd;
275
276 foreach my $physdiskrrd (<$R/disk-*/disk_octets.rrd>) {
277     $physdiskrrd =~ s,octets\.rrd$,, or die;
278     $physdiskrrd =~ m,-([^/]+)/disk_$, or die;
279     my $physdev= "/dev/$1";
280     if (!stat $physdev) {
281         die "$physdev $!" unless $!==&ENOENT;
282         next;
283     }
284     die "$physdev ?" unless S_ISBLK((stat _)[2]);
285     $disk_rdev2rrd{(stat _)[6]}= $physdiskrrd;
286 }
287
288 our @disk_vgs;
289
290 sub lvgraphs {
291     my ($vg, $label, $factor, $rcolour, $wcolour) = @_;
292     my @lvs;
293     my $varname= $vg;
294     $varname =~ s/[^0-9a-zA-Y]/ sprintf "Z%02x", ord($&) /ge;
295     my $vginfo= {
296         Name => $label,
297         Varname => $varname,
298         Colour => { 'read' => $rcolour, 'write' => $wcolour },
299         Lvs => []
300     };
301     foreach my $bo (qw(octets ops)) {
302         foreach my $rw (qw(read write)) {
303             $vginfo->{VarDefs}{$bo}{$rw}= [];
304             $vginfo->{Sumdef}{$bo}{$rw}= '0';
305         }
306     }
307     my $ix=0;
308     foreach my $lvpath (</dev/$vg/*>) {
309         my $lv= $lvpath; $lv =~ s,.*/,,;
310         if (!stat $lvpath) {
311             die "$lvpath $!" unless $!==&ENOENT;
312             next;
313         }
314         die "$lvpath ?" unless S_ISBLK((stat _)[2]);
315         my $rrd= $disk_rdev2rrd{(stat _)[6]};
316         next unless defined $rrd;
317
318         my $lvinfo= { Name => $lv };
319         push @{ $vginfo->{Lvs} }, $lvinfo;
320
321         foreach my $bo (qw(octets ops)) {
322             $lvinfo->{Defs}{$bo}=
323               [
324                (map { ("DEF:$_=${rrd}${bo}.rrd:$_:AVERAGE") } qw(read write)),
325                "CDEF:mwrite=0,write,-",
326                "AREA:read#00f:read",
327                "AREA:mwrite#f00:write"
328                ];
329
330             foreach my $rw (qw(read write)) {
331                 $ix++;
332                 my $tvar= "lv_${rw}_${bo}_${varname}_${ix}";
333                 push @{ $vginfo->{VarDefs}{$bo}{$rw} },
334                     "DEF:$tvar=${rrd}${bo}.rrd:$rw:AVERAGE";
335                 $vginfo->{Sumdef}{$bo}{$rw} .= ",$tvar,+";
336             }
337         }
338     }
339     foreach my $bo (qw(octets ops)) {
340         foreach my $rw (qw(read write)) {
341             my $defs= [];
342             push @$defs, @{ $vginfo->{VarDefs}{$bo}{$rw} };
343             push @$defs, "CDEF:${rw}_vg_${varname}=".
344                 $vginfo->{Sumdef}{$bo}{$rw}.
345                 sprintf(",%f,*", $rw eq 'write' ? -$factor : $factor);
346             $vginfo->{Defs}{$bo}{$rw}= $defs;
347         }
348     }
349     push @disk_vgs, $vginfo;
350 }
351
352 lvgraphs('vg-main',          'main',     1, qw(00f f00));
353 lvgraphs('vg-chiark-stripe', 'stripe', 0.5, qw(008 800));
354
355 foreach my $bo (qw(octets ops)) {
356     my @a= ();
357     foreach my $rw (qw(read write)) {
358         my $stack= '';
359         foreach my $vginfo (@disk_vgs) {
360             push @a, @{ $vginfo->{Defs}{$bo}{$rw} };
361             push @a, "AREA:${rw}_vg_$vginfo->{Varname}#".
362                 $vginfo->{Colour}{$rw}.
363                 ":$vginfo->{Name} ".substr($rw,0,1).
364                 $stack;
365             $stack= ':STACK';
366         }
367     }
368     graph_of_group('IO', 'IO', $bo, { Units => '[/s]' }, \@a);
369 }
370
371 foreach my $vginfo (@disk_vgs) {
372     foreach my $bo (qw(octets ops)) {
373         foreach my $lv (@{ $vginfo->{Lvs} }) {
374             graph_of_group('IO', "$vginfo->{Name} $lv->{Name}",
375                            $bo, { Units => '[/s]' }, $lv->{Defs}{$bo});
376         }
377     }
378 }
379
380 push @{ $section_groups{General} }, {
381     Section => 'IO',
382     Group => 'IO',
383     UrlParams => "section=IO&sloth=SLOTH"
384 };
385
386 }
387 #---------- right, that was the initialisation ----------
388
389 our $self= url(-relative=>1);
390
391 if (param('debug')) {
392     print "Content-Type: text/plain\n\n";
393 }
394
395 sub source_tarball ($$) {
396     my ($spitoutfn) = @_;
397 }
398
399 if (path_info() =~ m/\.tar\.gz$/) {
400     print "Content-Type: application/octet-stream\n\n";
401
402     exec '/bin/sh','-c','
403                 (
404                  git-ls-files -z;
405                  git-ls-files -z --others --exclude-from=.gitignore;
406                  if test -d .git; then find .git -print0; fi
407                 ) | (
408                  cpio -Hustar -o --quiet -0 -R 1000:1000 || \
409                  cpio -Hustar -o --quiet -0
410                 ) | gzip
411         ';
412     die $!;
413 }
414
415 our @navsettings;
416
417 @navsettings= ();
418
419 sub navsetting ($) {
420     my ($nav) = @_;
421     my $var= $nav->{Variable};
422     $$var= param($nav->{Param});
423     $$var= $nav->{Default} if !defined $$var;
424     die $nav->{Param} unless grep { $_ eq $$var } @{ $nav->{Values} };
425     push @navsettings, $nav;
426 }
427
428 our $section;
429
430 navsetting({
431     Desc => 'Section',
432     Param => 'section',
433     Variable => \$section,
434     Default => $sections[0],
435     Values => [@sections],
436     Show => sub { return $_[0]; }
437 });
438
439
440 sub num_param ($$$$) {
441     my ($param,$def,$min,$max) = @_;
442     my $v= param($param);
443     return $def if !defined $v;
444     $v =~ m/^([1-9]\d{0,8})$/ or die;
445     $v= $1;
446     die unless $v >= $min && $v <= $max;
447     return $v + 0;
448 }
449
450 our $group= param('graph');
451
452 my $defwidth= 370;
453 my $defheight= 200;
454
455 our $elem= param('elem');
456 if (defined $elem) {
457     my $g= $graphs{$section,$group,$elem};
458     die unless $g;
459
460     my $width= num_param('w',$defwidth,100,1600);
461     my $height= num_param('h',$defheight,100,1600);
462
463     my $sloth= param('sloth');
464     die unless defined $sloth;
465     $sloth =~ m/^(\d+)$/ or die;
466     $sloth= $1+0;
467     my $end= $g->{TimeRanges}[$sloth];
468     die unless defined $end;
469
470     my @args= @{ $g->{Args} };
471     s,\<interval/(\d+)\>, $end/$1 ,ge foreach @args;
472     unshift @args, qw(--end now --start), "end-${end}s";
473     
474     my $title = $g->{Title};
475     $title .= " $g->{Units}" if $g->{Units};
476     unshift @args, '-t', $title, '-w',$width, '-h',$height;
477     unshift @args, qw(-a PNG --full-size-mode);
478
479     my $cacheid= "$section!$group!$elem!$sloth!$width!$height!";
480     $cacheid .= unpack "H*", MD5->hash(join '\0', @args);
481     my $cachepath= "cache/$cacheid.png";
482
483     if (param('debug')) {
484         print((join "\n",@args),"\n"); exit 0;
485     }
486
487 #print STDERR "||| ",(join ' ', map { "'$_'" } @args)." |||\n";
488     exec(qw(sh -ec), <<'END', 'x', $cachepath, @args);
489         p="$1"; shift
490         rrdtool graph "$p" --lazy "$@" >/dev/null
491         printf "Content-Type: image/png\n\n"
492         exec cat "$p"
493 END
494     die $!;
495 }
496
497 sub start_page ($) {
498     my ($title) = @_;
499     print header(), start_html($title);
500     my $outerdelim= '';
501     foreach my $nav (@navsettings) {
502         print $outerdelim;
503         print $nav->{Desc}, ": ";
504         my $delim= '';
505         my $current= $nav->{Variable};  $current= $$current;
506         foreach my $couldbe (@{ $nav->{Values} }) {
507             print $delim;
508             my $show= $nav->{Show}($couldbe);
509             if ($couldbe eq $current) {
510                 print "<b>$show</b>";
511             } else {
512                 my $u= $self;
513                 my $delim2= '?';
514                 foreach my $nav2 (@navsettings) {
515                     my $current2= $nav2->{Variable};  $current2= $$current2;
516                     $current2= $couldbe if $nav2->{Param} eq $nav->{Param};
517                     next if $current2 eq $nav2->{Default};
518                     $u .= $delim2;  $u .= "$nav2->{Param}=$current2";
519                     $delim2= '&';
520                 }
521                 print a({href=>$u}, $show);
522             }
523             $delim= ' | ';
524         }
525         $outerdelim= "<br>\n";
526     }
527     print "\n";
528
529     print h1("$title");
530 }
531
532 our $detail= param('detail');
533 if (defined $detail) {
534     my $elems= $group_elems{$section,$detail};
535     die unless $elems;
536     start_page("$detail - $section - graphs");
537     foreach my $tsloth (0..5) {
538         foreach my $elem (@$elems) {
539             my $g= $graphs{$section,$detail,$elem};
540             die unless $g;
541             next if $tsloth >= @{ $g->{TimeRanges} };
542             my $imgurl= "$self?graph=$detail&section=$section".
543                 "&sloth=$tsloth&elem=$elem";
544             print a({href=>"$imgurl&w=780&h=800"},
545                     img({src=>$imgurl, alt=>'',
546                          width=>$defwidth, height=>$defheight}));
547         }
548     }
549     print end_html();
550     exit 0;
551 }
552
553 our $sloth;
554
555 navsetting({
556     Desc => 'Time interval',
557     Param => 'sloth',
558     Variable => \$sloth,
559     Default => 1,
560     Values => [0..3],
561     Show => sub {
562         my ($sl) = @_;
563         return ('Narrower', 'Normal', 'Wider', 'Extra wide')[$sl];
564     }
565 });
566
567 if (param('debug')) {
568     use Data::Dumper;
569     print Dumper(\%graphs);
570     exit 0;
571 }
572
573 start_page("$section - graphs");
574
575 foreach my $group (@{ $section_groups{$section} }) {
576     my $ref_group= $group;
577     my $ref_section= $section;
578     my $ref_urlparams= "detail=$group&section=$section";
579     if (ref $group) {
580         $ref_group= $group->{Group};
581         $ref_section= $group->{Section};
582         $ref_urlparams= $group->{UrlParams};
583         $ref_urlparams =~ s/\bSLOTH\b/$sloth/;
584     }
585     print a({href=>"$self?$ref_urlparams"});
586     my $imgurl= "$self?graph=$ref_group&section=$ref_section";
587     print "<span style=\"white-space:nowrap\">";
588     my $elems= $group_elems{$ref_section,$ref_group};
589     foreach my $elem (@$elems) {
590         my $g= $graphs{$ref_section,$ref_group,$elem};
591         print img({src=>"$imgurl&elem=$elem&sloth=".($sloth + $g->{Slower}),
592                    alt=>'', width=>$defwidth, height=>$defheight});
593     }
594     print "</span>";
595     print "</a>\n";
596 }
597
598 my $homeurl =
599     'http://www.chiark.greenend.org.uk/ucgi/~ijackson/git/rrd-graphs/';
600 print <<END
601 <hr>Generated by "<a href="$homeurl">rrd-graphs</a>".
602 Copyright 2010,2012 Ian Jackson.  There is <strong>NO WARRANTY</strong>.
603 Available under the Affero Public General Licence, version 3 or any
604 later version.  You may download the
605 <a href="$self/rrd-graphs.tar.gz">source code</a>
606 for the version currently running here.
607 END