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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
#!/usr/bin/env raku
use v6.d;
enum Category <Host OS OSMajor Uname>;
enum Metric <Boots Uptime MetaScore Downtime Lifespan>;
subset MetricSubset of Metric where * ne any (Downtime, Lifespan);
subset Natural of Int where * >= 0;
our Natural constant DAY = 1 * 24 * 3600;
our Natural constant MONTH = 30 * DAY;
class Epoch {
has Natural $.value is required;
submethod new (Natural $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(Natural:D \limit) returns Bool {
(DateTime.now - DateTime.new(Instant.from-posix: $!value)) < limit * DAY;
}
}
class Aggregate {
has Str $.name is required;
has Natural $.uptime;
has Natural $.first-boot;
has Natural $.last-seen;
has Natural $.boots;
method new (Str:D $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 Natural {
Natural((($!uptime * 2) + ($!boots * DAY) + (self.is-active ?? MONTH !! 0))/1000000)
}
method is-active(Natural:D \limit = 90) returns Bool {
Epoch.new($!last-seen).newer-than: limit;
}
}
class HostAggregate is Aggregate {
method lifespan returns Natural { $.last-seen - $.first-boot }
method downtime returns Natural { self.lifespan - $.uptime }
method meta-score returns Natural { Natural(self.downtime / 1000000) + callsame }
}
class Aggregator {
has Hash %.aggregates = { Host => {}, OS => {}, Uname => {}, OSMajor => {} }
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<OSMajor>{$os-major} //= Aggregate.new($os-major);
for %!aggregates<Host>{$host}, %!aggregates<OS>{$os},
%!aggregates<Uname>{$uname}, %!aggregates<OSMajor>{$os-major} {
.add-record(:$uptime, :$boot-time);
}
}
}
class Reporter {
has Category $.category = Host;
has Metric $.metric is required;
has Natural $.limit is required;
has Hash %.aggregates;
method report {
say "Top {$.limit} {$.metric}'s by {$.category}:\n";
with self!table -> (@table, %size) {
my Str \format = '|' ~ join '|',
" %{%size<count>}s ", " %{%size<name>}s ", " %{%size<value>}s ", "\n";
my Str \border = '+' ~ join '+',
'-' x (2+%size<count>), '-' x (2+%size<name>), '-' x (2+%size<value>), "\n";
print border;
printf format, 'Pos', $.category, $.metric;
print border;
for @table -> \position, \name, \value {
printf format, position, name, value;
}
print border;
}
}
method !table returns List {
my Natural $count = 0;
my @table;
# Initial table size
my %size =
:count('Pos'.chars), :name($.category.chars),
:value($.metric.chars);
for self.sort-by($.metric) -> Aggregate \what {
my Str \active = what.is-active ?? '*' !! ' ';
my Str \name = active ~ what.name;
my Str \value = self.human-str($.metric, what).Str;
# Adjust size
%size{.key} = .value if %size{.key} < .value for
:count($count.Str.chars+1), :name(name.chars), :value(value.chars);
@table.push: "{$count+1}.", name, value;
last if ++$count == $.limit;
}
return @table, %size;
}
multi method sort-by(Uptime) { self.sort-by: *.uptime }
multi method sort-by(Boots) { self.sort-by: *.boots }
multi method sort-by(MetaScore) { self.sort-by: *.meta-score }
multi method sort-by(Code:D $sort-by) {
%!aggregates{$!category}.values.sort(&$sort-by).reverse;
}
multi method human-str(Uptime, Aggregate:D $what) { Epoch.new($what.uptime).human-duration }
multi method human-str(Boots, Aggregate:D $what) { $what.boots }
multi method human-str(MetaScore, Aggregate:D $what) { $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) { Epoch.new($what.downtime).human-duration }
multi method human-str(Lifespan, Aggregate:D $what) { 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;
}
sub MAIN(
Str :$stats-dir is required, #= The uptimed raw record input dir.
Category :$category = Host, #= The category, one of Host, OS, OSMajor, Uname [default: 'Host']
Metric :$metric = Uptime, #= The metric, one of Boots, Uptime, MetaScore, Downtime, Lifespan
Natural :$limit = 20, #= Limit output to num of entries.
) {
if $category ~~ Host {
do-it($stats-dir, HostReporter.new(:$metric, :$limit));
} elsif $metric ~~ MetricSubset {
do-it($stats-dir, Reporter.new(:$category, :$metric, :$limit));
} else {
die "Category $category only supports the following metrics: {Metric.^enum_value_list.grep: * ~~ MetricSubset}";
}
}
|