chiark / gitweb /
disk 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, $label, $factor, $rcolour, $wcolour) = @_;
234     my @lvs;
235     my $varname= $vg;
236     $varname =~ s/[^0-9a-zA-Y]/ sprintf "Z%02x", ord($&) /ge;
237     my $vginfo= {
238         Name => $label,
239         Varname => $varname,
240         Colour => { 'read' => $rcolour, 'write' => $wcolour },
241         Lvs => []
242     };
243     foreach my $bo (qw(octets ops)) {
244         foreach my $rw (qw(read write)) {
245             $vginfo->{VarDefs}{$bo}{$rw}= [];
246             $vginfo->{Sumdef}{$bo}{$rw}= '0';
247         }
248     }
249     my $ix=0;
250     foreach my $lvpath (</dev/$vg/*>) {
251         my $lv= $lvpath; $lv =~ s,.*/,,;
252         if (!stat $lvpath) {
253             die "$lvpath $!" unless $!==&ENOENT;
254             next;
255         }
256         die "$lvpath ?" unless S_ISBLK((stat _)[2]);
257         my $rrd= $disk_rdev2rrd{(stat _)[6]};
258         next unless defined $rrd;
259
260         my $lvinfo= { Name => $lv };
261         push @{ $vginfo->{Lvs} }, $lvinfo;
262
263         foreach my $bo (qw(octets ops)) {
264             $lvinfo->{Defs}{$bo}=
265               [
266                (map { ("DEF:$_=${rrd}${bo}.rrd:$_:AVERAGE") } qw(read write)),
267                "CDEF:mwrite=0,write,-",
268                "AREA:read#00f:read",
269                "AREA:mwrite#f00:write"
270                ];
271
272             foreach my $rw (qw(read write)) {
273                 $ix++;
274                 my $tvar= "lv_${rw}_${bo}_${varname}_${ix}";
275                 push @{ $vginfo->{VarDefs}{$bo}{$rw} },
276                     "DEF:$tvar=${rrd}${bo}.rrd:$rw:AVERAGE";
277                 $vginfo->{Sumdef}{$bo}{$rw} .= ",$tvar,+";
278             }
279         }
280     }
281     foreach my $bo (qw(octets ops)) {
282         foreach my $rw (qw(read write)) {
283             my $defs= [];
284             push @$defs, @{ $vginfo->{VarDefs}{$bo}{$rw} };
285             push @$defs, "CDEF:${rw}_vg_${varname}=".
286                 $vginfo->{Sumdef}{$bo}{$rw}.
287                 sprintf(",%f,*", $rw eq 'write' ? -$factor : $factor);
288             $vginfo->{Defs}{$bo}{$rw}= $defs;
289         }
290     }
291     push @disk_vgs, $vginfo;
292 }
293
294 lvgraphs('vg-main',          'main',     1, qw(00f f00));
295 lvgraphs('vg-chiark-stripe', 'stripe', 0.5, qw(008 800));
296
297 foreach my $bo (qw(octets ops)) {
298     my @a= ();
299     foreach my $rw (qw(read write)) {
300         my $stack= '';
301         foreach my $vginfo (@disk_vgs) {
302             push @a, @{ $vginfo->{Defs}{$bo}{$rw} };
303             push @a, "AREA:${rw}_vg_$vginfo->{Varname}#".
304                 $vginfo->{Colour}{$rw}.
305                 ":$vginfo->{Name} ".substr($rw,0,1).
306                 $stack;
307             $stack= ':STACK';
308         }
309     }
310     graph_of_group('IO', 'IO', $bo, { Units => '[/s]' }, \@a);
311 }
312
313 foreach my $vginfo (@disk_vgs) {
314     foreach my $bo (qw(octets ops)) {
315         foreach my $lv (@{ $vginfo->{Lvs} }) {
316             graph_of_group('IO', "$vginfo->{Name} $lv->{Name}",
317                            $bo, { Units => '[/s]' }, $lv->{Defs}{$bo});
318         }
319     }
320 }
321
322 push @{ $section_groups{General} }, {
323     Section => 'IO',
324     Group => 'IO',
325     UrlParams => "section=IO&sloth=SLOTH"
326 };
327
328 }
329 #---------- right, that was the initialisation ----------
330
331 our $self= url(-relative=>1);
332
333 if (param('debug')) {
334     print "Content-Type: text/plain\n\n";
335 }
336
337 our @navsettings;
338
339 @navsettings= ();
340
341 sub navsetting ($) {
342     my ($nav) = @_;
343     my $var= $nav->{Variable};
344     $$var= param($nav->{Param});
345     $$var= $nav->{Default} if !defined $$var;
346     die $nav->{Param} unless grep { $_ eq $$var } @{ $nav->{Values} };
347     push @navsettings, $nav;
348 }
349
350 our $section;
351
352 navsetting({
353     Desc => 'Section',
354     Param => 'section',
355     Variable => \$section,
356     Default => $sections[0],
357     Values => [@sections],
358     Show => sub { return $_[0]; }
359 });
360
361
362 sub num_param ($$$$) {
363     my ($param,$def,$min,$max) = @_;
364     my $v= param($param);
365     return $def if !defined $v;
366     $v =~ m/^([1-9]\d{0,8})$/ or die;
367     $v= $1;
368     die unless $v >= $min && $v <= $max;
369     return $v + 0;
370 }
371
372 our $group= param('graph');
373
374 our $elem= param('elem');
375 if (defined $elem) {
376     my $g= $graphs{$section,$group,$elem};
377     die unless $g;
378
379     my $width= num_param('w',370,100,1600);
380     my $height= num_param('h',200,100,1600);
381
382     my $sloth= param('sloth');
383     die unless defined $sloth;
384     $sloth =~ m/^(\d+)$/ or die;
385     $sloth= $1+0;
386     my $end= $g->{TimeRanges}[$sloth];
387     die unless defined $end;
388
389     my $cacheid= "$section!$group!$elem!$sloth!$width!$height";
390     my $cachepath= "cache/$cacheid.png";
391
392     my @args= @{ $g->{Args} };
393     s,\<interval/(\d+)\>, $end/$1 ,ge foreach @args;
394     unshift @args, qw(--end now --start), "end-${end}s";
395     
396     my $title= $group;
397     if (length $elem) { $title.= " $elem"; }
398
399     $title .= " $g->{Units}" if $g->{Units};
400     unshift @args, '-t', $title, '-w',$width, '-h',$height;
401     unshift @args, qw(-a PNG --full-size-mode);
402
403     if (param('debug')) {
404         print((join "\n",@args),"\n"); exit 0;
405     }
406
407 #print STDERR "||| ",(join ' ', map { "'$_'" } @args)." |||\n";
408     exec(qw(sh -ec), <<'END', 'x', $cachepath, @args);
409         p="$1"; shift
410         rrdtool graph "$p" --lazy "$@" >/dev/null
411         printf "Content-Type: image/png\n\n"
412         exec cat "$p"
413 END
414     die $!;
415 }
416
417 sub start_page ($) {
418     my ($title) = @_;
419     print header(), start_html($title);
420     my $outerdelim= '';
421     foreach my $nav (@navsettings) {
422         print $outerdelim;
423         print $nav->{Desc}, ": ";
424         my $delim= '';
425         my $current= $nav->{Variable};  $current= $$current;
426         foreach my $couldbe (@{ $nav->{Values} }) {
427             print $delim;
428             my $show= $nav->{Show}($couldbe);
429             if ($couldbe eq $current) {
430                 print "<b>$show</b>";
431             } else {
432                 my $u= $self;
433                 my $delim2= '?';
434                 foreach my $nav2 (@navsettings) {
435                     my $current2= $nav2->{Variable};  $current2= $$current2;
436                     $current2= $couldbe if $nav2->{Param} eq $nav->{Param};
437                     next if $current2 eq $nav2->{Default};
438                     $u .= $delim2;  $u .= "$nav2->{Param}=$current2";
439                     $delim2= '&';
440                 }
441                 print a({href=>$u}, $show);
442             }
443             $delim= ' | ';
444         }
445         $outerdelim= "<br>\n";
446     }
447     print "\n";
448
449     print h1("$title");
450 }
451
452 our $detail= param('detail');
453 if (defined $detail) {
454     my $elems= $group_elems{$section,$detail};
455     die unless $elems;
456     start_page("$detail - $section - graphs");
457     foreach my $tsloth (0..5) {
458         foreach my $elem (@$elems) {
459             my $g= $graphs{$section,$detail,$elem};
460             die unless $g;
461             next if $tsloth >= @{ $g->{TimeRanges} };
462             my $imgurl= "$self?graph=$detail&section=$section".
463                 "&sloth=$tsloth&elem=$elem";
464             print a({href=>"$imgurl&w=780&h=800"},
465                     img({src=>$imgurl, alt=>''}));
466         }
467     }
468     print end_html();
469     exit 0;
470 }
471
472 our $sloth;
473
474 navsetting({
475     Desc => 'Time interval',
476     Param => 'sloth',
477     Variable => \$sloth,
478     Default => 1,
479     Values => [0..3],
480     Show => sub {
481         my ($sl) = @_;
482         return ('Narrower', 'Normal', 'Wider', 'Extra wide')[$sl];
483     }
484 });
485
486 if (param('debug')) {
487     use Data::Dumper;
488     print Dumper(\%graphs);
489     exit 0;
490 }
491
492 start_page("$section - graphs");
493
494 foreach my $group (@{ $section_groups{$section} }) {
495     my $ref_group= $group;
496     my $ref_section= $section;
497     my $ref_urlparams= "detail=$group&section=$section";
498     if (ref $group) {
499         $ref_group= $group->{Group};
500         $ref_section= $group->{Section};
501         $ref_urlparams= $group->{UrlParams};
502         $ref_urlparams =~ s/\bSLOTH\b/$sloth/;
503     }
504     print a({href=>"$self?$ref_urlparams"});
505     my $imgurl= "$self?graph=$ref_group&section=$ref_section";
506     print "<span style=\"white-space:nowrap\">";
507     my $elems= $group_elems{$ref_section,$ref_group};
508     foreach my $elem (@$elems) {
509         my $g= $graphs{$ref_section,$ref_group,$elem};
510         print img({src=>"$imgurl&elem=$elem&sloth=".($sloth + $g->{Slower}),
511                    alt=>''});
512     }
513     print "</span>";
514     print "</a>\n";
515 }
516