From 944e5035b60f09ce68aa66d6b89168cd9151f482 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 26 Feb 2023 17:03:49 +0200 Subject: clean up perl stuff --- README.txt | 59 ---------------- docs/guprecords.1 | 195 ---------------------------------------------------- docs/guprecords.pod | 82 ---------------------- docs/guprecords.txt | 59 ---------------- guprecords.raku | 161 +++++++++++++++++++++++++++++++++++++++++++ src/guprecords | 181 ------------------------------------------------ src/guprecords.raku | 161 ------------------------------------------- 7 files changed, 161 insertions(+), 737 deletions(-) delete mode 100644 README.txt delete mode 100644 docs/guprecords.1 delete mode 100644 docs/guprecords.pod delete mode 100644 docs/guprecords.txt create mode 100644 guprecords.raku delete mode 100755 src/guprecords delete mode 100644 src/guprecords.raku diff --git a/README.txt b/README.txt deleted file mode 100644 index dac01a6..0000000 --- a/README.txt +++ /dev/null @@ -1,59 +0,0 @@ -NAME - guprecords - Global uptime records - - Shows uprecord stats of several hosts at once. - - Synopsis - guprecords [--help] [--total|--all] [--count=i] [--indir=s] - - Parameters - --all - Shows every individual uptime of all hosts. - - --count=i - Show i num of entries. Default is 23. - - --nofqdn - Don't display the FQDNs - - --help - Shows the help - - --indir=s - Read all the *.records files from dir s. Default is ./ - - --total - Aggregates a total uptime for every single host. - - Quick getting started - Uptimed - Firstival, you need to collect uprecords using the uptimed deaemon. To - install it run: - - sudo aptitude install uptimed - - Please consult the uptimed and uprecords manpages. Please ensure to - understand how it works and what it does. - - uptimed collects uprecords to - - /var/spool/uptimed/records - - And this file is used by guprecords for further processing. - - Collect all the uprecords - You may have several hosts with uptimed running already. Collect all the - records file to a central repository (e.g. git). Name each file - FQDN.records and run - - guprecords --indir ./ - - You may automate the collecting of all the uprecords using something - like cron or puppet. - -LICENSE - See package description or project website. - -AUTHOR - Paul Buetow - - diff --git a/docs/guprecords.1 b/docs/guprecords.1 deleted file mode 100644 index ce69da5..0000000 --- a/docs/guprecords.1 +++ /dev/null @@ -1,195 +0,0 @@ -.\" Automatically generated by Pod::Man 2.25 (Pod::Simple 3.16) -.\" -.\" Standard preamble: -.\" ======================================================================== -.de Sp \" Vertical space (when we can't use .PP) -.if t .sp .5v -.if n .sp -.. -.de Vb \" Begin verbatim text -.ft CW -.nf -.ne \\$1 -.. -.de Ve \" End verbatim text -.ft R -.fi -.. -.\" Set up some character translations and predefined strings. \*(-- will -.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left -.\" double quote, and \*(R" will give a right double quote. \*(C+ will -.\" give a nicer C++. Capital omega is used to do unbreakable dashes and -.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, -.\" nothing in troff, for use with C<>. -.tr \(*W- -.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' -.ie n \{\ -. ds -- \(*W- -. ds PI pi -. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch -. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch -. ds L" "" -. ds R" "" -. ds C` "" -. ds C' "" -'br\} -.el\{\ -. ds -- \|\(em\| -. ds PI \(*p -. ds L" `` -. ds R" '' -'br\} -.\" -.\" Escape single quotes in literal strings from groff's Unicode transform. -.ie \n(.g .ds Aq \(aq -.el .ds Aq ' -.\" -.\" If the F register is turned on, we'll generate index entries on stderr for -.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index -.\" entries marked with X<> in POD. Of course, you'll have to process the -.\" output yourself in some meaningful fashion. -.ie \nF \{\ -. de IX -. tm Index:\\$1\t\\n%\t"\\$2" -.. -. nr % 0 -. rr F -.\} -.el \{\ -. de IX -.. -.\} -.\" -.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). -.\" Fear. Run. Save yourself. No user-serviceable parts. -. \" fudge factors for nroff and troff -.if n \{\ -. ds #H 0 -. ds #V .8m -. ds #F .3m -. ds #[ \f1 -. ds #] \fP -.\} -.if t \{\ -. ds #H ((1u-(\\\\n(.fu%2u))*.13m) -. ds #V .6m -. ds #F 0 -. ds #[ \& -. ds #] \& -.\} -. \" simple accents for nroff and troff -.if n \{\ -. ds ' \& -. ds ` \& -. ds ^ \& -. ds , \& -. ds ~ ~ -. ds / -.\} -.if t \{\ -. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" -. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' -. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' -. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' -. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' -. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' -.\} -. \" troff and (daisy-wheel) nroff accents -.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' -.ds 8 \h'\*(#H'\(*b\h'-\*(#H' -.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] -.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' -.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' -.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] -.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] -.ds ae a\h'-(\w'a'u*4/10)'e -.ds Ae A\h'-(\w'A'u*4/10)'E -. \" corrections for vroff -.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' -.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' -. \" for low resolution devices (crt and lpr) -.if \n(.H>23 .if \n(.V>19 \ -\{\ -. ds : e -. ds 8 ss -. ds o a -. ds d- d\h'-1'\(ga -. ds D- D\h'-1'\(hy -. ds th \o'bp' -. ds Th \o'LP' -. ds ae ae -. ds Ae AE -.\} -.rm #[ #] #H #V #F C -.\" ======================================================================== -.\" -.IX Title "GUPRECORDS 1" -.TH GUPRECORDS 1 "2014-06-20" "guprecords 0.2.2" "User Commands" -.\" For nroff, turn off justification. Always turn off hyphenation; it makes -.\" way too many mistakes in technical documents. -.if n .ad l -.nh -.SH "NAME" -guprecords \- Global uptime records -.PP -Shows uprecord stats of several hosts at once. -.SS "Synopsis" -.IX Subsection "Synopsis" -guprecords [\-\-help] [\-\-total|\-\-all] [\-\-count=i] [\-\-indir=s] -.SS "Parameters" -.IX Subsection "Parameters" -.IP "\-\-all" 4 -.IX Item "--all" -Shows every individual uptime of all hosts. -.IP "\-\-count=i" 4 -.IX Item "--count=i" -Show i num of entries. Default is 23. -.IP "\-\-nofqdn" 4 -.IX Item "--nofqdn" -Don't display the FQDNs -.IP "\-\-help" 4 -.IX Item "--help" -Shows the help -.IP "\-\-indir=s" 4 -.IX Item "--indir=s" -Read all the *.records files from dir s. Default is ./ -.IP "\-\-total" 4 -.IX Item "--total" -Aggregates a total uptime for every single host. -.SS "Quick getting started" -.IX Subsection "Quick getting started" -\fIUptimed\fR -.IX Subsection "Uptimed" -.PP -Firstival, you need to collect uprecords using the uptimed deaemon. To install it run: -.PP -.Vb 1 -\& sudo aptitude install uptimed -.Ve -.PP -Please consult the uptimed and uprecords manpages. Please ensure to understand how it works and what it does. -.PP -uptimed collects uprecords to -.PP -.Vb 1 -\& /var/spool/uptimed/records -.Ve -.PP -And this file is used by guprecords for further processing. -.PP -\fICollect all the uprecords\fR -.IX Subsection "Collect all the uprecords" -.PP -You may have several hosts with uptimed running already. Collect all the records file to a central repository (e.g. git). Name each file \s-1FQDN\s0.records and run -.PP -.Vb 1 -\& guprecords \-\-indir ./ -.Ve -.PP -You may automate the collecting of all the uprecords using something like cron or puppet. -.SH "LICENSE" -.IX Header "LICENSE" -See package description or project website. -.SH "AUTHOR" -.IX Header "AUTHOR" -Paul Buetow \- diff --git a/docs/guprecords.pod b/docs/guprecords.pod deleted file mode 100644 index d021ea3..0000000 --- a/docs/guprecords.pod +++ /dev/null @@ -1,82 +0,0 @@ -=head1 NAME - -guprecords - Global uptime records - -Shows uprecord stats of several hosts at once. - -=head2 Synopsis - -guprecords [--help] [--total|--all] [--count=i] [--indir=s] - -=head2 Parameters - -=over - -=item --all - -Shows every individual uptime of all hosts. - -=item --count=i - -Show i num of entries. Default is 23. - -=item --nocolor - -Don't display with ANSI colors - -=item --nofqdn - -Don't display the FQDNs - -=item --help - -Shows the help - -=item --indir=s - -Read all the *.records files from dir s. Default is ./ - -=item --oldage=i - -The number of days until a host is defined as old. - -=item --total - -Aggregates a total uptime for every single host. - -=back - - -=head2 Quick getting started - -=head3 Uptimed - -Firstival, you need to collect uprecords using the uptimed deaemon. To install it run: - - sudo aptitude install uptimed - -Please consult the L and L manpages. Please ensure to understand how it works and what it does. - -uptimed collects uprecords to - - /var/spool/uptimed/records - -And this file is used by guprecords for further processing. - -=head3 Collect all the uprecords - -You may have several hosts with uptimed running already. Collect all the records file to a central repository (e.g. git). Name each file FQDN.records and run - - guprecords --indir ./ - -You may automate the collecting of all the uprecords using something like cron or puppet. - -=head1 LICENSE - -See package description or project website. - -=head1 AUTHOR - -Paul Buetow - - -=cut diff --git a/docs/guprecords.txt b/docs/guprecords.txt deleted file mode 100644 index c15a6b4..0000000 --- a/docs/guprecords.txt +++ /dev/null @@ -1,59 +0,0 @@ -NAME - guprecords - Global uptime records - - Shows uprecord stats of several hosts at once. - - Synopsis - guprecords [--help] [--total|--all] [--count=i] [--indir=s] - - Parameters - --all - Shows every individual uptime of all hosts. - - --count=i - Show i num of entries. Default is 23. - - --nofqdn - Don't display the FQDNs - - --help - Shows the help - - --indir=s - Read all the *.records files from dir s. Default is ./ - - --total - Aggregates a total uptime for every single host. - - Quick getting started - Uptimed - Firstival, you need to collect uprecords using the uptimed deaemon. To - install it run: - - sudo aptitude install uptimed - - Please consult the uptimed and uprecords manpages. Please ensure to - understand how it works and what it does. - - uptimed collects uprecords to - - /var/spool/uptimed/records - - And this file is used by guprecords for further processing. - - Collect all the uprecords - You may have several hosts with uptimed running already. Collect all the - records file to a central repository (e.g. git). Name each file - FQDN.records and run - - guprecords --indir ./ - - You may automate the collecting of all the uprecords using something - like cron or puppet. - -LICENSE - See package description or project website. - -AUTHOR - Paul Buetow - - diff --git a/guprecords.raku b/guprecords.raku new file mode 100644 index 0000000..976a83c --- /dev/null +++ b/guprecords.raku @@ -0,0 +1,161 @@ +#!/usr/bin/env raku + +use v6.d; + +subset Nat of Int where * >= 0; +subset Cat of Str where * eq any ; +subset SubCat of Str where * eq any ; +subset HostOnlyCat of Cat where * eq 'host'; +subset BasicSubCat of SubCat where * ne any ; + +our Nat constant DAY = 1 * 24 * 3600; +our Nat constant MONTH = 30 * DAY; + +class Epoch { + has Nat $.value is required; + + submethod new (Nat $value) { self.bless(:$value) } + + method human-duration returns Str { + my DateTime \dt .= new(Instant.from-posix: $!value); + "{dt.year-1970} years, {dt.month} months, {dt.day} days"; + } + + method human-date returns Str { + DateTime.new(Instant.from-posix: $!value).yyyy-mm-dd; + } + + method newer-than(Nat:D \limit) returns Bool { + (DateTime.now - DateTime.new(Instant.from-posix: $!value)) < limit * DAY; + } +} + +class Aggregate { + has Str $.name is required; + has Nat $.uptime; + has Nat $.first-boot; + has Nat $.last-seen; + has Nat $.boots; + + method new (Str $name) { self.bless(:$name) } + + method add-record(Str:D :$uptime is readonly, Str:D :$boot-time is readonly) { + my Int $last-seen = $uptime + $boot-time; + $!uptime += $uptime; + $!boots++; + + $!first-boot = +$boot-time if not defined $!first-boot or $!first-boot > $boot-time; + $!last-seen = $last-seen if not defined $!last-seen or $!last-seen < $last-seen; + } + + method meta-score returns Nat { + Nat((($!uptime * 2) + ($!boots * DAY) + (self.is-active ?? MONTH !! 0))/1000000) + } + + method is-active(Nat:D \limit = 90) returns Bool { + Epoch.new($!last-seen).newer-than: limit; + } +} + +class HostAggregate is Aggregate { + method lifespan returns Nat { $.last-seen - $.first-boot } + method downtime returns Nat { self.lifespan - $.uptime } + method meta-score returns Nat { Nat(self.downtime / 1000000) + callsame } +} + +class Aggregator { + has Hash %.aggregates = { host => {}, os => {}, uname => {}, os-major => {} } + + method add-file(IO::Path:D $file is readonly) { + my Str $host = $file.IO.basename.split('.').first; + + die "Record file for {$host} already processed - duplicate inputs?" + if %!aggregates{$host}:exists; + %!aggregates{$host} = HostAggregate.new($host); + + for $file.IO.lines -> Str $line { self!add-line(:$line, :$host) } + } + + method !add-line(Str:D :$line is readonly, Str:D :$host is readonly) { + my Str ($uptime, $boot-time, $os) = $line.trim.split(':'); + my Str $uname = $os.split(' ').first; + my Str $os-major = "$uname {$os.split(' ')[1].split('.').first}..."; + + %!aggregates{$os} //= Aggregate.new($os); + %!aggregates{$uname} //= Aggregate.new($uname); + %!aggregates{$os-major} //= Aggregate.new($os-major); + + for %!aggregates{$host}, %!aggregates{$os}, + %!aggregates{$uname}, %!aggregates{$os-major} { + .add-record(:$uptime, :$boot-time); + } + } +} + +class Reporter { + has Cat $.cat is required; + has SubCat $.sub-cat is required; + has Nat $.first is required; + has Hash %.aggregates; + + method report { + say "Top {$.first} {$.sub-cat}'s by {$.cat}:\n"; + my Nat $count = 0; + + for self.sort-by($!sub-cat) -> Aggregate $what { + self!pretty-say($what, $count+1); + last if ++$count == $.first; + } + } + + method !pretty-say(Aggregate:D \what, Nat:D \position) { + my Str \active = what.is-active ?? ' (still active)' !! ''; + say "{position}. {what.name}{active}:\n\t{self.human-str($.sub-cat, what)}"; + } + + multi method sort-by('uptime') { self.sort-by: *.uptime } + multi method sort-by('boots') { self.sort-by: *.boots } + multi method sort-by('meta-score') { self.sort-by: *.meta-score } + + multi method sort-by(Code:D $sort-by) { + %!aggregates{$!cat}.values.sort(&$sort-by).reverse; + } + + multi method human-str('uptime', Aggregate:D $what) { "Uptime: {Epoch.new($what.uptime).human-duration}" } + multi method human-str('boots', Aggregate:D $what) { "Number of boots: {$what.boots}" } + multi method human-str('meta-score', Aggregate:D $what) { "Meta score: {$what.meta-score}" } +} + +class HostReporter is Reporter { + multi method sort-by('downtime') { self.sort-by: *.downtime } + multi method sort-by('lifespan') { self.sort-by: *.lifespan } + + multi method human-str('downtime', Aggregate:D $what) { "Downtime: {Epoch.new($what.downtime).human-duration}" } + multi method human-str('lifespan', Aggregate:D $what) { "Lifespan: {Epoch.new($what.lifespan).human-duration}" } +} + +sub do-it(Str:D \stats-dir, Reporter:D \reporter) { + my Aggregator \aggregator .= new; + aggregator.add-file($_) for dir(stats-dir, test => { /.records$/ }); + reporter.aggregates = aggregator.aggregates; + reporter.report; +} + +multi MAIN( + Str :$stats-dir is required, #= The uptimed raw record input dir. + HostOnlyCat :$cat = 'host', #= Category, one of host, os os-major and uname. + SubCat :$sub-cat = 'uptime', #= Sort by one of boots uptime downtime and lifespan. + Nat :$first = 13, #= Only show top N entries. +) { + do-it($stats-dir, HostReporter.new(:$cat, :$sub-cat, :$first)); +} + +multi MAIN( + Str :$stats-dir is required, + Cat :$cat, + BasicSubCat :$sub-cat = 'uptime', + Nat :$first = 13, +) { + do-it($stats-dir, Reporter.new(:$cat, :$sub-cat, :$first)); +} + diff --git a/src/guprecords b/src/guprecords deleted file mode 100755 index 3722470..0000000 --- a/src/guprecords +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env perl - -# guprecords (c) 2014, Paul Buetow -# E-Mail: guprecords@mx.buetow.org WWW: http://codeberg.org/snonux/guprecords - -use strict; -use warnings; -use v5.14; - -use Getopt::Long; -use POSIX qw(strftime); -use Term::ANSIColor; - -our $VERSION = 'VERSION_DEVEL'; -our $FLAG = 0; -our $NOW = time(); - -our %ARGS = ( - all => 0, - help => 0, - nofqdn => 0, - reverse => 0, - total => 0, - nocolor => 0, - 'oldage=i' => 180, - 'count=i' => 23, - 'indir=s' => '.', -); - -our %OPTS = map { $_ => \$ARGS{$_} } keys %ARGS; -GetOptions %OPTS or die "Error in command line arguments. Try --help"; - -sub help() { - say "Ths is guprecords Version $VERSION"; - print "Usage: $0\n"; - say "\t--$_" for sort keys %ARGS; - say 'Please also consult the guprecords manual page.' -} - -sub uptime($) { - my $uptime = shift; - my ($s,$m,$h,undef,undef,$y,undef,$d) = localtime($uptime); - - $y -= 70; - $d += $y * 365; - - sprintf "%3dd %02d:%02d:%02d", $d, $h, $m, $s; -} - -sub trimlen($$) { - my ($string,$len) = @_; - - if (length $string > $len) { - substr($string, 0, ($len-2)).'..'; - } else { - $string; - } -} - -sub out(\@;$) { - my ($records,$show_bt) = @_; - - $FLAG = 1; - my $count = 0; - - printf "%3s | %17s | %20s | %13s | %24s\n", - 'Pos', - 'System', - 'Kernel', - 'Uptime', - (defined $show_bt ? 'Boot time' : ''); - - map { - return if $count++ == $ARGS{'count=i'}; - - my $name = $ARGS{nofqdn} ? $_->{hostname} : $_->{fqdn}; - - print color 'bold' if !$ARGS{nocolor} and $_->{is_old}; - - printf "%3d | %17s | %20s | %13s | %24s\n", - $count, - trimlen($name,17), - trimlen($_->{kernel},20), - uptime($_->{uptime}), - (defined $show_bt ? ''.localtime($_->{bootime}) : ''); - - print color 'reset' if !$ARGS{nocolor} and $_->{is_old}; - } - do { - unless ($ARGS{reverse}) { - sort { $b->{uptime} <=> $a->{uptime} } @$records - } else { - sort { $a->{uptime} <=> $b->{uptime} } @$records - } - }; -} - -sub is_old($) { - my $time = shift; - # Count per day - my $oldage = 86400 * $ARGS{'oldage=i'}; - - return $NOW - $time < $oldage ? 1 : 0; -} - -sub all() { - my @records; - - for my $file (<$ARGS{'indir=s'}/*.records>) { - my ($fqdn) = $file =~ m#.*/(.*)\.records#; - my ($hostname) = $fqdn =~m#([^\.]+)#; - - my $uptime_total = 0; - - open my $fh, $file or die "$file: $!\n"; - while (<$fh>) { - chomp; - my ($uptime,$boot,$kernel) = split ':'; - - push @records, { - bootime => $boot, - fqdn => $fqdn, - hostname => $hostname, - kernel => $kernel, - uptime => $uptime, - is_old => is_old $uptime + $boot, - }; - - } - close $file; - - } - - out @records, 'show_bt'; -} - -sub total() { - my @records; - - for my $file (<$ARGS{'indir=s'}/*.records>) { - my ($fqdn) = $file =~ m#.*/(.*)\.records#; - - my $uptime_ = 0; - my $kernel_; - - my ($highest, $newest) = (0, 0); - - open my $fh, $file or die "$file: $!\n"; - while (<$fh>) { - chomp; - my ($uptime,$boot,$kernel) = split ':'; - - $uptime_ += $uptime; - - if ($highest < $uptime) { - $highest = $uptime; - $kernel_ = $kernel; - } - - $newest = $uptime + $boot if $newest < $uptime + $boot; - } - close $file; - - push @records, { - uptime => $uptime_, - kernel => $kernel_, - fqdn => $fqdn, - is_old => is_old $newest, - }; - - } - - out @records; -} - -help if $ARGS{help}; -all if $ARGS{all}; -total if $ARGS{total}; - -help unless $FLAG; - diff --git a/src/guprecords.raku b/src/guprecords.raku deleted file mode 100644 index 976a83c..0000000 --- a/src/guprecords.raku +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env raku - -use v6.d; - -subset Nat of Int where * >= 0; -subset Cat of Str where * eq any ; -subset SubCat of Str where * eq any ; -subset HostOnlyCat of Cat where * eq 'host'; -subset BasicSubCat of SubCat where * ne any ; - -our Nat constant DAY = 1 * 24 * 3600; -our Nat constant MONTH = 30 * DAY; - -class Epoch { - has Nat $.value is required; - - submethod new (Nat $value) { self.bless(:$value) } - - method human-duration returns Str { - my DateTime \dt .= new(Instant.from-posix: $!value); - "{dt.year-1970} years, {dt.month} months, {dt.day} days"; - } - - method human-date returns Str { - DateTime.new(Instant.from-posix: $!value).yyyy-mm-dd; - } - - method newer-than(Nat:D \limit) returns Bool { - (DateTime.now - DateTime.new(Instant.from-posix: $!value)) < limit * DAY; - } -} - -class Aggregate { - has Str $.name is required; - has Nat $.uptime; - has Nat $.first-boot; - has Nat $.last-seen; - has Nat $.boots; - - method new (Str $name) { self.bless(:$name) } - - method add-record(Str:D :$uptime is readonly, Str:D :$boot-time is readonly) { - my Int $last-seen = $uptime + $boot-time; - $!uptime += $uptime; - $!boots++; - - $!first-boot = +$boot-time if not defined $!first-boot or $!first-boot > $boot-time; - $!last-seen = $last-seen if not defined $!last-seen or $!last-seen < $last-seen; - } - - method meta-score returns Nat { - Nat((($!uptime * 2) + ($!boots * DAY) + (self.is-active ?? MONTH !! 0))/1000000) - } - - method is-active(Nat:D \limit = 90) returns Bool { - Epoch.new($!last-seen).newer-than: limit; - } -} - -class HostAggregate is Aggregate { - method lifespan returns Nat { $.last-seen - $.first-boot } - method downtime returns Nat { self.lifespan - $.uptime } - method meta-score returns Nat { Nat(self.downtime / 1000000) + callsame } -} - -class Aggregator { - has Hash %.aggregates = { host => {}, os => {}, uname => {}, os-major => {} } - - method add-file(IO::Path:D $file is readonly) { - my Str $host = $file.IO.basename.split('.').first; - - die "Record file for {$host} already processed - duplicate inputs?" - if %!aggregates{$host}:exists; - %!aggregates{$host} = HostAggregate.new($host); - - for $file.IO.lines -> Str $line { self!add-line(:$line, :$host) } - } - - method !add-line(Str:D :$line is readonly, Str:D :$host is readonly) { - my Str ($uptime, $boot-time, $os) = $line.trim.split(':'); - my Str $uname = $os.split(' ').first; - my Str $os-major = "$uname {$os.split(' ')[1].split('.').first}..."; - - %!aggregates{$os} //= Aggregate.new($os); - %!aggregates{$uname} //= Aggregate.new($uname); - %!aggregates{$os-major} //= Aggregate.new($os-major); - - for %!aggregates{$host}, %!aggregates{$os}, - %!aggregates{$uname}, %!aggregates{$os-major} { - .add-record(:$uptime, :$boot-time); - } - } -} - -class Reporter { - has Cat $.cat is required; - has SubCat $.sub-cat is required; - has Nat $.first is required; - has Hash %.aggregates; - - method report { - say "Top {$.first} {$.sub-cat}'s by {$.cat}:\n"; - my Nat $count = 0; - - for self.sort-by($!sub-cat) -> Aggregate $what { - self!pretty-say($what, $count+1); - last if ++$count == $.first; - } - } - - method !pretty-say(Aggregate:D \what, Nat:D \position) { - my Str \active = what.is-active ?? ' (still active)' !! ''; - say "{position}. {what.name}{active}:\n\t{self.human-str($.sub-cat, what)}"; - } - - multi method sort-by('uptime') { self.sort-by: *.uptime } - multi method sort-by('boots') { self.sort-by: *.boots } - multi method sort-by('meta-score') { self.sort-by: *.meta-score } - - multi method sort-by(Code:D $sort-by) { - %!aggregates{$!cat}.values.sort(&$sort-by).reverse; - } - - multi method human-str('uptime', Aggregate:D $what) { "Uptime: {Epoch.new($what.uptime).human-duration}" } - multi method human-str('boots', Aggregate:D $what) { "Number of boots: {$what.boots}" } - multi method human-str('meta-score', Aggregate:D $what) { "Meta score: {$what.meta-score}" } -} - -class HostReporter is Reporter { - multi method sort-by('downtime') { self.sort-by: *.downtime } - multi method sort-by('lifespan') { self.sort-by: *.lifespan } - - multi method human-str('downtime', Aggregate:D $what) { "Downtime: {Epoch.new($what.downtime).human-duration}" } - multi method human-str('lifespan', Aggregate:D $what) { "Lifespan: {Epoch.new($what.lifespan).human-duration}" } -} - -sub do-it(Str:D \stats-dir, Reporter:D \reporter) { - my Aggregator \aggregator .= new; - aggregator.add-file($_) for dir(stats-dir, test => { /.records$/ }); - reporter.aggregates = aggregator.aggregates; - reporter.report; -} - -multi MAIN( - Str :$stats-dir is required, #= The uptimed raw record input dir. - HostOnlyCat :$cat = 'host', #= Category, one of host, os os-major and uname. - SubCat :$sub-cat = 'uptime', #= Sort by one of boots uptime downtime and lifespan. - Nat :$first = 13, #= Only show top N entries. -) { - do-it($stats-dir, HostReporter.new(:$cat, :$sub-cat, :$first)); -} - -multi MAIN( - Str :$stats-dir is required, - Cat :$cat, - BasicSubCat :$sub-cat = 'uptime', - Nat :$first = 13, -) { - do-it($stats-dir, Reporter.new(:$cat, :$sub-cat, :$first)); -} - -- cgit v1.2.3