summaryrefslogtreecommitdiff
path: root/src/guprecords.raku
blob: 976a83c8f10166169bc590cafab66445cb30546b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env raku

use v6.d;

subset Nat of Int where * >= 0;
subset Cat of Str where * eq any <host os os-major uname>;
subset SubCat of Str where * eq any <boots uptime downtime lifespan meta-score>;
subset HostOnlyCat of Cat where * eq 'host';
subset BasicSubCat of SubCat where * ne any <downtime lifespan>;

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>{$host}:exists;
    %!aggregates<host>{$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>{$os} //= Aggregate.new($os);
    %!aggregates<uname>{$uname} //= Aggregate.new($uname);
    %!aggregates<os-major>{$os-major} //= Aggregate.new($os-major);

    for %!aggregates<host>{$host}, %!aggregates<os>{$os},
        %!aggregates<uname>{$uname}, %!aggregates<os-major>{$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));
}