File Coverage

blib/lib/Mail/Milter/Authentication/Metric.pm
Criterion Covered Total %
statement 110 169 65.0
branch 20 42 47.6
condition 2 8 25.0
subroutine 16 18 88.8
pod 8 8 100.0
total 156 245 63.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Metric;
2 99     99   599 use strict;
  99         244  
  99         2438  
3 99     99   409 use warnings;
  99         243  
  99         5538  
4             our $VERSION = '20191206'; # VERSION
5              
6              
7 99     99   587 use English qw{ -no_match_vars };
  99         200  
  99         888  
8 99     99   33818 use JSON;
  99         242  
  99         658  
9 99     99   10343 use TOML;
  99         200  
  99         5707  
10 99     99   37858 use Prometheus::Tiny::Shared;
  99         591633  
  99         3040  
11 99     99   629 use Mail::Milter::Authentication::Config qw{ get_config };
  99         203  
  99         4389  
12 99     99   40236 use Mail::Milter::Authentication::Metric::Grafana;
  99         251  
  99         2770  
13 99     99   38942 use Mail::Milter::Authentication::HTDocs;
  99         381  
  99         165209  
14              
15              
16             sub new {
17 37     37 1 153 my ( $class ) = @_;
18 37         121 my $self = {};
19 37         250 $self->{'counter'} = {};
20 37         344 $self->{'help'} = {};
21 37         232 $self->{'start_time'} = time;
22 37         296 $self->{'queue'} = [];
23              
24 37         242 my $config = get_config();
25              
26             $self->{'enabled'} = defined( $config->{'metric_port'} ) ? 1
27 37 100       552 : defined( $config->{'metric_connection'} ) ? 1
    50          
28             : 0;
29              
30 37 100       185 if ( $self->{'enabled'} ) {
31 28         66 my $cache_args = {};
32 28         300 $cache_args->{'init_file'} = 1;
33 28 50       3379 if ( defined( $config->{'metric_tempfile'} ) ) {
34 0         0 $cache_args->{'share_file'} = $config->{'metric_tempfile'};
35             }
36 28         795 $self->{'prom'} = Prometheus::Tiny::Shared->new( cache_args => $cache_args );
37 28         307602 $self->{'prom'}->declare( 'authmilter_uptime_seconds_total', help => 'Number of seconds since server startup', type => 'counter' );
38 28         11209 $self->{'prom'}->declare( 'authmilter_processes_waiting', help => 'The number of authentication milter processes in a waiting state', type => 'gauge' );
39 28         4644 $self->{'prom'}->declare( 'authmilter_processes_processing', help => 'The number of authentication milter processes currently processing data', type => 'gauge' );
40             }
41              
42 37         5083 bless $self, $class;
43 37         488 return $self;
44             }
45              
46              
47             sub get_timeout {
48 9     9 1 56 my ( $self ) = @_;
49 9         76 my $config = get_config();
50 9   50     242 return $config->{ 'metric_timeout' } || 5;
51             }
52              
53              
54             sub clean_label {
55 42399     42399 1 77968 my ( $self, $text ) = @_;
56 42399         78209 $text = lc $text;
57 42399         128012 $text =~ s/[^a-z0-9]/_/g;
58 42399 100       88459 if ( $text eq q{} ) {
59 57         142 $text = '-none-';
60             }
61 42399         104206 return $text;
62             }
63              
64              
65             sub count {
66 8868     8868 1 18206 my ( $self, $args ) = @_;
67 8868 100       23308 return if ( ! $self->{ 'enabled' } );
68              
69 6973         14036 my $count_id = $args->{ 'count_id' };
70 6973         12506 my $labels = $args->{ 'labels' };
71 6973         11024 my $server = $args->{ 'server' };
72 6973         11274 my $count = $args->{ 'count' };
73              
74 6973 50       15341 $count = 1 if ! defined $count;
75              
76 6973         15229 $count_id = $self->clean_label( $count_id );
77              
78 6973         14015 my $clean_labels = {};
79 6973 50       18925 if ( $labels ) {
80 6973         29819 foreach my $l ( sort keys %$labels ) {
81 13855         28937 $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} );
82             }
83             }
84              
85 6973         16926 $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT );
86              
87 6973         12486 eval{ $self->{prom}->add( 'authmilter_' . $count_id, $count, $clean_labels ); };
  6973         32121  
88             ## TODO catch and re-throw timeouts
89              
90 6973         1503289 return;
91             }
92              
93              
94             sub send { ## no critic
95 0     0 1 0 my ( $self, $server ) = @_;
96 0         0 return;
97             }
98              
99              
100             sub register_metrics {
101 362     362 1 1004 my ( $self, $hash ) = @_;
102 362 100       1378 return if ( ! $self->{ 'enabled' } );
103              
104 346         1496 foreach my $metric ( keys %$hash ) {
105 462         16533 my $help = $hash->{ $metric };
106 462         2369 $self->{prom}->declare( 'authmilter_' . $metric, help => $help, type => 'counter');
107 462         102436 $self->{prom}->set( 'authmilter_' . $metric,0, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
108             }
109 346         51117 return;
110             }
111              
112              
113             sub master_metric_update {
114 136     136 1 636 my ( $self, $server ) = @_;
115 136 50       952 return if ( ! $self->{ 'enabled' } );
116              
117 136         553 eval {
118 136         599 foreach my $type ( qw { waiting processing } ) {
119 272         38296 $self->{prom}->set('authmilter_processes_' . $type, $server->{'server'}->{'tally'}->{ $type }, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
120             }
121             };
122              
123 136         19259 return;
124             }
125              
126              
127             sub child_handler {
128 9     9 1 46 my ( $self, $server ) = @_;
129 9 50       55 return if ( ! $self->{ 'enabled' } );
130              
131 9         94 my $config = get_config();
132              
133 9         35 eval {
134 9     0   442 local $SIG{'ALRM'} = sub{ die "Timeout\n" };
  0         0  
135 9         80 alarm( $self->get_timeout() );
136              
137 9         58 my $socket = $server->{'server'}->{'client'};
138 9         29 my $req;
139              
140 9         162 $PROGRAM_NAME = $Mail::Milter::Authentication::Config::IDENT . ':metrics';
141              
142 9         595 $req = <$socket>;
143 9         264 $req =~ s/[\n\r]+$//;
144              
145 9 50 33     269 if (!defined($req) || $req !~ m{ ^\s*(GET|POST|PUT|DELETE|PUSH|HEAD|OPTIONS)\s+(.+)\s+(HTTP/1\.[01])\s*$ }ix) {
146 0         0 print $socket "HTTP/1.0 500 Server Error\n";
147 0         0 print $socket "\n";
148 0         0 print $socket "Invalid Request Error\n";
149 0         0 return;
150             }
151              
152 9         85 my $request_method = uc $1;
153 9         151 my $request_uri = $2;
154 9         76 my $server_protocol = $3;
155              
156 9 50       63 if ( $request_method ne 'GET' ) {
157 0         0 print $socket "HTTP/1.0 500 Server Error\n";
158 0         0 print $socket "\n";
159 0         0 print $socket "Server Error\n";
160 0         0 return;
161             }
162              
163             # Ignore the rest of the HTTP request
164 9         151 while ( $req = <$socket> ) {
165 9         109 $req =~ s/[\n\r]+$//;
166 9 50       70 last if $req eq q{};
167             }
168              
169 9 50 0     979 if ( $request_uri eq '/metrics' ) {
    0          
    0          
    0          
    0          
170 9         108 $self->{prom}->set( 'authmilter_uptime_seconds_total', time - $self->{'start_time'}, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
171              
172 9         2953 print $socket "HTTP/1.0 200 OK\n";
173 9         117 print $socket "Content-Type: text/plain\n";
174 9         100 print $socket "\n";
175 9         112 print $socket $self->{prom}->format();
176              
177             }
178             elsif ( $request_uri eq '/' ){
179 0         0 my $config = get_config();
180 0         0 print $socket "HTTP/1.0 200 OK\n";
181 0         0 print $socket "Content-Type: text/html\n";
182 0         0 print $socket "\n";
183 0         0 print $socket qq{
184             <html>
185             <head>
186             <title>Authentication Milter</title>
187             <link rel="stylesheet" href="/css/normalize.css" />
188             <link rel="stylesheet" href="/css/skeleton.css" />
189             <link rel="stylesheet" href="/css/authmilter.css" />
190             </head>
191             <body>
192              
193             <div class="container">
194              
195             <h1>Authentication Milter</h1>
196              
197             <span class="versionBlock">Version: } . $Mail::Milter::Authentication::VERSION . qq{<br />Ident: } . $Mail::Milter::Authentication::Config::IDENT . qq{</span>
198              
199             <h2>Installed Handlers</h2>
200             <div class="spaceAfter">};
201              
202 0         0 foreach my $Handler ( sort keys %{ $server->{ 'handler' } } ) {
  0         0  
203 0 0       0 next if $Handler eq '_Handler';
204 0         0 print $socket ' <span class="handler">' . $Handler . ' (' . $server->{ 'handler' }->{ $Handler }->get_version(). ')</span> ';
205             }
206              
207 0         0 print $socket qq{
208             </div>
209              
210             <h2>Registered Callbacks</h2>
211             <table class="callbacksTable">};
212              
213 0         0 foreach my $stage ( qw{ setup connect helo envfrom envrcpt header eoh body eom abort close addheader } ) {
214 0         0 my $callbacks = $server->{ 'handler' }->{ '_Handler' }->get_callbacks( $stage );
215 0         0 print $socket "<tr><td>$stage</td><td>" . join( ' ', map{ "<span class=\"handler\">$_</span>" } @$callbacks ) . "</td></tr>";
  0         0  
216             }
217              
218 0         0 print $socket qq{</table>
219              
220             <h2>Connection/Config Details</h2>
221             <ul>};
222 0         0 print $socket '<li>Protocol: ' . $config->{'protocol'} . '</li>';
223 0         0 my $connections = $config->{'connections'};
224 0         0 $connections->{'default'} = { 'connection' => $config->{'connection'} };
225 0         0 foreach my $connection ( sort keys %$connections ) {
226 0         0 print $socket '<li>' . $connection . ': ' . $connections->{ $connection }->{ 'connection' } . '</li>'
227             }
228 0         0 print $socket qq{
229             <li>Effective config (<a href="/config/toml">toml</a>/<a href="/config/json">json</a>)</li>
230             </ul>
231              
232             <h2>Metrics</h2>
233             <ul>
234             <li><a href="/metrics">Prometheus metrics endpoint</a></li>
235             <li>Example <a href="/grafana">Grafana dashboard</a> for this setup</li>
236             </ul>
237              
238             <hr />
239              
240             </div>
241             </body>
242             };
243             }
244             elsif ( $request_uri eq '/config/json' || $request_uri eq '/config' ) {
245 0         0 print $socket "HTTP/1.0 200 OK\n";
246 0         0 print $socket "Content-Type: text/plain\n";
247 0         0 print $socket "\n";
248 0         0 my $json = JSON->new();
249 0         0 $json->canonical();
250 0         0 $json->pretty();
251 0         0 print $socket $json->encode( $config );;
252             }
253             elsif ( $request_uri eq '/config/toml' ) {
254 0         0 print $socket "HTTP/1.0 200 OK\n";
255 0         0 print $socket "Content-Type: text/plain\n";
256 0         0 print $socket "\n";
257 0         0 my $toml = TOML::to_toml( $config );
258 0         0 $toml =~ s/\n\[/\n\n\[/g;
259 0         0 print $socket $toml;
260             }
261             elsif ( $request_uri eq '/grafana' ) {
262 0         0 print $socket "HTTP/1.0 200 OK\n";
263 0         0 print $socket "Content-Type: application/json\n";
264 0         0 print $socket "\n";
265              
266 0         0 my $Grafana = Mail::Milter::Authentication::Metric::Grafana->new();
267 0         0 print $socket $Grafana->get_dashboard( $server );
268             }
269             else {
270 0         0 my $htdocs = Mail::Milter::Authentication::HTDocs->new();
271 0         0 my $result = $htdocs->get_file( $request_uri );
272 0 0       0 if ( $result ) {
273 0         0 print $socket $result;
274             }
275             else {
276 0         0 print $socket "HTTP/1.0 404 Not Found\n";
277 0         0 print $socket "Content-Type: text/plain\n";
278 0         0 print $socket "\n";
279 0         0 print $socket "Not Found\n";
280             }
281             }
282              
283 9         46728 alarm( 0 );
284             };
285              
286 9         65 return;
287             }
288              
289             1;
290              
291             __END__
292              
293             =pod
294              
295             =encoding UTF-8
296              
297             =head1 NAME
298              
299             Mail::Milter::Authentication::Metric
300              
301             =head1 VERSION
302              
303             version 20191206
304              
305             =head1 DESCRIPTION
306              
307             Handle metrics collection and production for prometheus
308              
309             =head1 CONSTRUCTOR
310              
311             =head2 I<new()>
312              
313             my $object = Mail::Milter::Authentication::Metric->new();
314              
315             Create a new Mail::Milter::Authentication::Metric object
316             This object is used to store, modify, and report metrics.
317              
318             =head1 METHODS
319              
320             =head2 I<get_timeout()>
321              
322             Returns the current value of timeout for metrics operations.
323              
324             =head2 I<clean_label($text)>
325              
326             Given a string, return a version of that string which is safe to use as a metrics label.
327              
328             =head2 I<count($args)>
329              
330             Increment the metric for the given counter
331             Called from the base handler, do not call directly.
332             $server is the current handler object
333              
334             count_id - the name of the metric to act on
335              
336             labels - hashref of labels to apply
337              
338             server - the current server object
339              
340             count - number to increment by (defaults to 1)
341              
342             =head2 I<send( $server )>
343              
344             Send metrics to the parent server process.
345              
346             =head2 I<register_metrics( $hash )>
347              
348             Register a new set of metric types and help texts.
349             Called from the master process in the setup phase.
350              
351             Expects a hashref of metric description, keyed on metric name.
352              
353             =head2 I<master_metric_update( $server )>
354              
355             Called in the master process to periodically update some metrics
356              
357             =head2 I<child_handler( $server)>
358              
359             Handle a metrics or http request in the child process.
360              
361             =head1 AUTHOR
362              
363             Marc Bradshaw <marc@marcbradshaw.net>
364              
365             =head1 COPYRIGHT AND LICENSE
366              
367             This software is copyright (c) 2018 by Marc Bradshaw.
368              
369             This is free software; you can redistribute it and/or modify it under
370             the same terms as the Perl 5 programming language system itself.
371              
372             =cut