File Coverage

blib/lib/Mail/Milter/Authentication/Metric.pm
Criterion Covered Total %
statement 196 271 72.3
branch 56 110 50.9
condition 2 11 18.1
subroutine 24 26 92.3
pod 14 15 93.3
total 292 433 67.4


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Metric;
2 126     126   3240 use 5.20.0;
  126         618  
3 126     126   813 use strict;
  126         261  
  126         2845  
4 126     126   640 use warnings;
  126         387  
  126         3349  
5 126     126   1697 use Mail::Milter::Authentication::Pragmas;
  126         314  
  126         1517  
6             # ABSTRACT: Class for metrics generation
7             our $VERSION = '3.20230629'; # VERSION
8 126     126   91681 use Mail::Milter::Authentication::HTDocs;
  126         409  
  126         4293  
9 126     126   61505 use Mail::Milter::Authentication::Metric::Grafana;
  126         439  
  126         4173  
10 126     126   934 use File::Temp;
  126         315  
  126         12400  
11 126     126   68507 use Prometheus::Tiny::Shared 0.020;
  126         963925  
  126         3886  
12 126     126   957 use TOML;
  126         328  
  126         522899  
13              
14              
15              
16              
17             sub new {
18 55     55 1 1463 my ( $class, $thischild ) = @_;
19 55         187 my $self = {};
20 55         632 $self->{counter} = {};
21 55         669 $self->{help} = {};
22 55         501 $self->{start_time} = time;
23 55         517 $self->{registered_metrics} = [];
24 55         281 $self->{thischild} = $thischild;
25 55         448 bless $self, $class;
26              
27 55         973 $self->set_handler( undef );
28              
29 55         314 my $config = get_config();
30              
31             $self->{enabled} = defined( $config->{metric_port} ) ? 1
32 55 100       887 : defined( $config->{metric_connection} ) ? 1
    50          
33             : 0;
34              
35 55         818 return $self;
36             }
37              
38              
39             sub set_handler {
40 22757     22757 1 46760 my ( $self, $handler ) = @_;
41 22757         63171 $self->{handler} = $handler;
42             }
43              
44              
45             sub handle_exception {
46 8918     8918 1 24373 my ( $self, $exception ) = @_;
47 8918 50       27463 return if ! defined $exception;
48 8918 100       26935 return if ! defined $self->{handler};
49 8647         35018 $self->{handler}->handle_exception($exception);
50             }
51              
52              
53             sub dbgout {
54 8710     8710 1 22266 my ( $self, $key, $value, $priority ) = @_;
55 8710 100 0     24204 if ( defined ( $self->{handler} ) ) {
    50          
    0          
56 8647         38056 $self->{handler}->dbgout($key,$value,$priority);
57             }
58             elsif ( $priority == LOG_DEBUG ) {
59 63         709 $self->{thischild}->logdebug( "$key: $value" );
60             }
61             elsif ( $priority == LOG_INFO || $priority == LOG_NOTICE ) {
62 0         0 $self->{thischild}->loginfo( "$key: $value" );
63             }
64             else {
65 0         0 $self->{thischild}->logerror( "$key: $value" );
66             }
67             }
68              
69              
70             sub prom {
71 22484     22484 1 47123 my ( $self ) = @_;
72 22484         75393 my $config = get_config();
73              
74 22484         43955 my $metric_tempfile;
75 22484 100       59848 if ( exists( $self->{metric_tempfile} ) ) {
76 22447         48262 $metric_tempfile = $self->{metric_tempfile};
77             }
78             else {
79 37 50       235 if ( defined( $config->{metric_tempfile} ) ) {
80 0         0 $metric_tempfile = $config->{metric_tempfile};
81             }
82 37 50       209 if ( ! $metric_tempfile ) {
83 37         241 $metric_tempfile = $config->{lib_dir}.'/metrics';
84             }
85 37         507 $self->{metric_tempfile} = $metric_tempfile;
86             }
87              
88 22484         42801 my $prom = $self->{prom};
89             # Invalidate if the file does not exist!
90 22484 100       351992 if ( ! -e $metric_tempfile ) {
91 4         20 $prom = undef;
92             }
93 22484 100       266070 if ( ! -d $metric_tempfile ) {
94             # If metric_tempfile is a regular file then we need to re-init with a directory
95             # this is likely a restart after upgrade.
96 4         16 $prom = undef;
97             }
98              
99 22484 100       79608 if ( ! $prom ) {
100 37 50       576 if ( -f $metric_tempfile ) {
101 0         0 unlink $metric_tempfile;
102             }
103 37 100       572 if ( ! -d $metric_tempfile ) {
104 4         400 mkdir $metric_tempfile, 0700;
105             }
106 37         667 $self->dbgout( 'Metrics', "Setup new metrics file $metric_tempfile", LOG_DEBUG );
107 37         198 $prom = eval{ Prometheus::Tiny::Shared->new(filename => $metric_tempfile.'/authmilter_metrics', init_file => 1) };
  37         814  
108 37         53529 $self->handle_exception($@);
109 37 50       211 if ( $prom ) {
110 37         172 $self->{metric_tempfile} = $metric_tempfile;
111 37         521 $prom->declare( 'authmilter_uptime_seconds_total', help => 'Number of seconds since server startup', type => 'counter' );
112 37         10766 $prom->declare( 'authmilter_processes_waiting', help => 'The number of authentication milter processes in a waiting state', type => 'gauge' );
113 37         946 $prom->declare( 'authmilter_processes_processing', help => 'The number of authentication milter processes currently processing data', type => 'gauge' );
114 37         908 $prom->declare( 'authmilter_version', help => 'Running versions', type => 'gauge' );
115             }
116             else {
117 0         0 $self->dbgout( 'Metrics', "Failed to setup new metrics file $metric_tempfile", LOG_ERR );
118             }
119             }
120 22484         55086 $self->{prom} = $prom;
121              
122 22484         122643 return $prom;
123             }
124              
125              
126             sub set_versions {
127 26     26 1 293 my ( $self, $server ) = @_;
128 26 50       361 return if ! $self->{enabled};
129 26 50       414 return if ! $self->prom;
130 26         1060 $self->dbgout( 'Metrics', "Setting up versioning metrics", LOG_DEBUG );
131 26         286 $self->prom->set( 'authmilter_version', 1, { version => $Mail::Milter::Authentication::VERSION, module => 'core', ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
132 26         5822 foreach my $Handler ( sort keys %{ $server->{handler} } ) {
  26         655  
133 260 100       1206 next if $Handler eq '_Handler';
134 234         611 eval{ $self->prom->set( 'authmilter_version', 1, { version => $server->{handler}->{ $Handler }->get_version(), module => $Handler, ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) }) };
  234         803  
135 234         13519 $self->handle_exception($@);
136             }
137             }
138              
139              
140              
141             sub get_timeout {
142 11     11 1 72 my ( $self ) = @_;
143 11         84 my $config = get_config();
144 11   50     310 return $config->{metric_timeout} || 5;
145             }
146              
147              
148             sub clean_label {
149 53777     53777 1 111505 my ( $self, $text ) = @_;
150 53777         110371 $text = lc $text;
151 53777         193289 $text =~ s/[^a-z0-9]/_/g;
152 53777 100       126418 if ( $text eq q{} ) {
153 71         307 $text = '-none-';
154             }
155 53777         153790 return $text;
156             }
157              
158              
159             sub count {
160 11322     11322 1 23518 my ( $self, $args ) = @_;
161 11322 100       30459 return if ! $self->{enabled};
162 8618 50       23258 return if ! $self->prom;
163              
164 8618         23436 my $count_id = $args->{count_id};
165 8618         16622 my $labels = $args->{labels};
166 8618         15920 my $server = $args->{server};
167 8618         17092 my $count = $args->{count};
168              
169 8618 50       20309 $count = 1 if ! defined $count;
170              
171 8618         23463 $count_id = $self->clean_label( $count_id );
172              
173 8618         19907 my $clean_labels = {};
174 8618 50       21005 if ( $labels ) {
175 8618         44999 foreach my $l ( sort keys %$labels ) {
176 17133         39836 $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} );
177             }
178             }
179              
180 8618         21764 $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT );
181              
182 8618         45322 $self->dbgout( 'Metrics', "Counting $count_id:$count:".join(',',map {"$_=".$clean_labels->{$_}} (sort keys %$clean_labels) ), LOG_DEBUG );
  25751         97281  
183              
184 8618         25820 eval{ $self->prom->add( 'authmilter_' . $count_id, $count, $clean_labels ); };
  8618         24215  
185 8618         663200 $self->handle_exception($@);
186             }
187              
188              
189             sub set {
190 29     29 1 109 my ( $self, $args ) = @_;
191 29 50       264 return if ! $self->{enabled};
192 29 50       139 return if ! $self->prom;
193              
194 29         111 my $gauge_id = $args->{gauge_id};
195 29         84 my $labels = $args->{labels};
196 29         85 my $server = $args->{server};
197 29         84 my $value = $args->{value};
198              
199 29 50       130 die 'metric set must define value' if ! defined $value;
200              
201 29         153 $gauge_id = $self->clean_label( $gauge_id );
202              
203 29         127 my $clean_labels = {};
204 29 50       120 if ( $labels ) {
205 29         262 foreach my $l ( sort keys %$labels ) {
206 38         140 $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} );
207             }
208             }
209              
210 29         146 $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT );
211              
212 29         279 $self->dbgout( 'Metrics', "Setting $gauge_id:$value:".join(',',map {"$_=".$clean_labels->{$_}} (sort keys %$clean_labels) ), LOG_DEBUG );
  67         597  
213              
214 29         768 eval{ $self->prom->set( 'authmilter_' . $gauge_id, $value, $clean_labels ); };
  29         723  
215 29         2574 $self->handle_exception($@);
216             }
217              
218              
219             sub send { ## no critic
220 0     0 1 0 my ( $self, $server ) = @_;
221             }
222              
223              
224             sub register_metrics {
225 479     479 1 1792 my ( $self, $hash ) = @_;
226 479 100       2247 return if ! $self->{enabled};
227 444 50       1913 return if ! $self->prom;
228 444         990 push @{$self->{registered_metrics}}, $hash;
  444         1616  
229 444         2294 $self->_register_metrics( $hash );
230             }
231              
232              
233             sub re_register_metrics {
234 39     39 0 187 my ( $self ) = @_;
235 39 50       196 return if ! $self->{enabled};
236 39 50       183 return if ! $self->prom;
237 39         272 foreach my $metric ( @{$self->{registered_metrics}} ) {
  39         557  
238 452         19305 $self->_register_metrics( $metric );
239             }
240             }
241              
242             sub _register_metrics {
243 896     896   2325 my ( $self, $hash ) = @_;
244 896 50       2676 return if ! $self->{enabled};
245 896 50       2053 return if ! $self->prom;
246              
247 896         5008 foreach my $metric ( keys %$hash ) {
248 1352         25035 my $data = $hash->{ $metric };
249 1352         2353 my $help;
250 1352         2528 my $type = 'counter';
251 1352 100       3594 if ( ref $data eq 'HASH' ) {
252 142         399 $help = $data->{help};
253 142         391 $type = $data->{type};
254             }
255             else {
256 1210         2252 $help = $data;
257             }
258 1352         3615 $self->prom->declare( 'authmilter_' . $metric, help => $help, type => $type);
259 1352         38986 $self->prom->add( 'authmilter_' . $metric,0, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
260             }
261             }
262              
263              
264             sub parent_metric_update {
265 259     259 1 3311 my ( $self, $server ) = @_;
266 259 50       2700 return if ! $self->{enabled};
267 259 50       2472 return if ! $self->prom;
268              
269 259         1749 eval {
270 259         1439 foreach my $type ( qw { waiting processing } ) {
271 518         36315 $self->prom->set('authmilter_processes_' . $type, $server->{server}->{tally}->{$type}, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
272             }
273             };
274             }
275              
276              
277             sub child_handler {
278 11     11 1 139 my ( $self, $server ) = @_;
279 11 50       143 return if ! $self->{enabled};
280              
281 11         194 my $config = get_config();
282              
283 11         305 eval {
284 11     0   855 local $SIG{ALRM} = sub{ die "Timeout\n" };
  0         0  
285 11         140 alarm( $self->get_timeout() );
286              
287 11         96 my $socket = $server->{server}->{client};
288 11         47 my $req;
289              
290 11         223 $PROGRAM_NAME = $Mail::Milter::Authentication::Config::IDENT . ':metrics';
291              
292 11         1309 $req = <$socket>;
293 11         316 $req =~ s/[\n\r]+$//;
294              
295 11 50 33     455 if (!defined($req) || $req !~ m{ ^\s*(GET|POST|PUT|DELETE|PUSH|HEAD|OPTIONS)\s+(.+)\s+(HTTP/1\.[01])\s*$ }ix) {
296 0         0 print $socket "HTTP/1.0 500 Server Error\n";
297 0         0 print $socket "\n";
298 0         0 print $socket "Invalid Request Error\n";
299 0         0 return;
300             }
301              
302 11         125 my $request_method = uc $1;
303 11         63 my $request_uri = $2;
304 11         79 my $server_protocol = $3;
305              
306 11 50       114 if ( $request_method ne 'GET' ) {
307 0         0 print $socket "HTTP/1.0 500 Server Error\n";
308 0         0 print $socket "\n";
309 0         0 print $socket "Server Error\n";
310 0         0 return;
311             }
312              
313             # Ignore the rest of the HTTP request
314 11         140 while ( $req = <$socket> ) {
315 11         276 $req =~ s/[\n\r]+$//;
316 11 50       127 last if $req eq q{};
317             }
318              
319 11 50 0     102 if ( $request_uri eq '/metrics' ) {
    0          
    0          
    0          
    0          
320 11 50       103 if ( $self->prom ) {
321 11         340 $server->{handler}->{_Handler}->top_metrics_callback();
322 11         91 $self->prom->set( 'authmilter_uptime_seconds_total', time - $self->{start_time}, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
323             }
324              
325 11         1816 print $socket "HTTP/1.0 200 OK\n";
326 11         201 print $socket "Content-Type: text/plain\n";
327 11         126 print $socket "\n";
328 11 50       110 if ( $self->prom ) {
329 11         68 print $socket $self->prom->format();
330             }
331             else {
332 0         0 print $socket '# Metrics unavailable';
333             }
334              
335             }
336             elsif ( $request_uri eq '/' ){
337 0         0 my $config = get_config();
338 0         0 print $socket "HTTP/1.0 200 OK\n";
339 0         0 print $socket "Content-Type: text/html\n";
340 0         0 print $socket "\n";
341 0         0 print $socket qq{
342             <html>
343             <head>
344             <title>Authentication Milter</title>
345             <link rel="stylesheet" href="/css/normalize.css" />
346             <link rel="stylesheet" href="/css/skeleton.css" />
347             <link rel="stylesheet" href="/css/authmilter.css" />
348             </head>
349             <body>
350              
351             <div class="container">
352              
353             <h1>Authentication Milter</h1>
354              
355             <span class="versionBlock">Version: } . $Mail::Milter::Authentication::VERSION . qq{<br />Ident: } . $Mail::Milter::Authentication::Config::IDENT . qq{</span>
356              
357             <h2>Installed Handlers</h2>
358             <div class="spaceAfter">};
359              
360 0         0 foreach my $Handler ( sort keys %{ $server->{handler} } ) {
  0         0  
361 0 0       0 next if $Handler eq '_Handler';
362 0         0 print $socket ' <span class="handler">' . $Handler . ' (' . $server->{handler}->{ $Handler }->get_version(). ')</span> ';
363             }
364              
365 0         0 print $socket qq{
366             </div>
367              
368             <h2>Registered Callbacks</h2>
369             <table class="callbacksTable">};
370              
371 0         0 foreach my $stage ( qw{ setup connect helo envfrom envrcpt header eoh body eom abort close addheader } ) {
372 0         0 my $callbacks = $server->{handler}->{_Handler}->get_callbacks( $stage );
373 0         0 print $socket "<tr><td>$stage</td><td>" . join( ' ', map{ "<span class=\"handler\">$_</span>" } @$callbacks ) . "</td></tr>";
  0         0  
374             }
375              
376 0         0 print $socket qq{</table>
377              
378             <h2>Details</h2>
379             <ul>};
380 0         0 print $socket '<li>Protocol: ' . $config->{protocol} . '</li>';
381 0         0 my $connections = $config->{connections};
382 0         0 $connections->{default} = { connection => $config->{connection} };
383 0         0 foreach my $connection ( sort keys %$connections ) {
384 0         0 print $socket '<li>' . $connection . ': ' . $connections->{ $connection }->{connection} . '</li>'
385             }
386 0 0       0 print $socket qq{<li>Effective config (<a href="/config/toml">toml</a>/<a href="/config/json">json</a>)</li>} if !$config->{'metric_basic_http'};
387 0         0 print $socket qq{
388             </ul>
389              
390             <h2>Metrics</h2>
391             <ul>
392             <li><a href="/metrics">Prometheus metrics endpoint</a></li>
393             };
394 0 0       0 print $socket qq{<li>Example <a href="/grafana">Grafana dashboard</a> for this setup</li>} if !$config->{'metric_basic_http'};
395 0         0 print $socket qq{
396             </ul>
397              
398             <hr />
399              
400             </div>
401             </body>
402             };
403             }
404             elsif ( $request_uri eq '/config/json' || $request_uri eq '/config' ) {
405 0 0       0 if ( $config->{'metric_basic_http'} ) {
406 0         0 print $socket "HTTP/1.0 403 Denied\n";
407 0         0 print $socket "Content-Type: text/plain\n\nDenied by config\n\n";
408             }
409             else {
410 0         0 print $socket "HTTP/1.0 200 OK\n";
411 0         0 print $socket "Content-Type: text/plain\n";
412 0         0 print $socket "\n";
413 0         0 my $json = JSON::XS->new();
414 0         0 $json->canonical();
415 0         0 $json->pretty();
416 0         0 print $socket $json->encode( $config );
417             }
418             }
419             elsif ( $request_uri eq '/config/toml' ) {
420 0 0       0 if ( $config->{'metric_basic_http'} ) {
421 0         0 print $socket "HTTP/1.0 403 Denied\n";
422 0         0 print $socket "Content-Type: text/plain\n\nDenied by config\n\n";
423             }
424             else {
425 0         0 print $socket "HTTP/1.0 200 OK\n";
426 0         0 print $socket "Content-Type: text/plain\n";
427 0         0 print $socket "\n";
428 0         0 my $toml = TOML::to_toml( $config );
429 0         0 $toml =~ s/\n\[/\n\n\[/g;
430 0         0 print $socket $toml;
431             }
432             }
433             elsif ( $request_uri eq '/grafana' ) {
434 0 0       0 if ( $config->{'metric_basic_http'} ) {
435 0         0 print $socket "HTTP/1.0 403 Denied\n";
436 0         0 print $socket "Content-Type: text/plain\n\nDenied by config\n\n";
437             }
438             else {
439 0         0 print $socket "HTTP/1.0 200 OK\n";
440 0         0 print $socket "Content-Type: application/json\n";
441 0         0 print $socket "\n";
442              
443 0         0 my $Grafana = Mail::Milter::Authentication::Metric::Grafana->new();
444 0         0 print $socket $Grafana->get_dashboard( $server );
445             }
446             }
447             else {
448 0         0 my $htdocs = Mail::Milter::Authentication::HTDocs->new();
449 0         0 my $result = $htdocs->get_file( $request_uri );
450 0 0       0 if ( $result ) {
451 0         0 print $socket $result;
452             }
453             else {
454 0         0 print $socket "HTTP/1.0 404 Not Found\n";
455 0         0 print $socket "Content-Type: text/plain\n";
456 0         0 print $socket "\n";
457 0         0 print $socket "Not Found\n";
458             }
459             }
460              
461 11         27288 alarm( 0 );
462             };
463             }
464              
465             1;
466              
467             __END__
468              
469             =pod
470              
471             =encoding UTF-8
472              
473             =head1 NAME
474              
475             Mail::Milter::Authentication::Metric - Class for metrics generation
476              
477             =head1 VERSION
478              
479             version 3.20230629
480              
481             =head1 DESCRIPTION
482              
483             Handle metrics collection and production for prometheus
484              
485             =head1 CONSTRUCTOR
486              
487             =head2 I<new()>
488              
489             my $object = Mail::Milter::Authentication::Metric->new();
490              
491             Create a new Mail::Milter::Authentication::Metric object
492             This object is used to store, modify, and report metrics.
493              
494             =head1 METHODS
495              
496             =head2 I<set_handler($handler)>
497              
498             Set a reference to the current handler
499              
500             =head2 I<handle_exception($exception)>
501              
502             If we have a handler, then pass any exception to that handlers exception handling
503              
504             =head2 I<prom()>
505              
506             Return the prom object if available
507              
508             =head2 I<set_versions( $server )>
509              
510             Setup version metrics
511              
512             =head2 I<get_timeout()>
513              
514             Returns the current value of timeout for metrics operations.
515              
516             =head2 I<clean_label($text)>
517              
518             Given a string, return a version of that string which is safe to use as a metrics label.
519              
520             =head2 I<count($args)>
521              
522             Increment the metric for the given counter
523             Called from the base handler, do not call directly.
524             $server is the current handler object
525              
526             count_id - the name of the metric to act on
527              
528             labels - hashref of labels to apply
529              
530             server - the current server object
531              
532             count - number to increment by (defaults to 1)
533              
534             =head2 I<set($args)>
535              
536             Set the metric for the given counter
537             Called from the base handler, do not call directly.
538             $server is the current handler object
539              
540             count_id - the name of the metric to act on
541              
542             labels - hashref of labels to apply
543              
544             server - the current server object
545              
546             count - number to increment by (defaults to 1)
547              
548             =head2 I<send( $server )>
549              
550             Send metrics to the parent server process.
551              
552             =head2 I<register_metrics( $hash )>
553              
554             Register a new set of metric types and help texts.
555             Called from the parent process in the setup phase.
556              
557             Expects a hashref of metric description, keyed on metric name.
558              
559             =head2 I<re_register_metric()>
560              
561             Re-register currently registered metrics to ensure backend
562             metadata is correct
563              
564             =head2 I<parent_metric_update( $server )>
565              
566             Called in the parent process to periodically update some metrics
567              
568             =head2 I<child_handler( $server )>
569              
570             Handle a metrics or http request in the child process.
571              
572             =head1 LOGGING METHODS
573              
574             =head2 I<dbgout( $key, $value, $priority )>
575              
576             Pass arguments along to the dbgout method of the handler if we have one
577             or log via the Mail::Milter::Authentication object if we do not.
578              
579             =head1 AUTHOR
580              
581             Marc Bradshaw <marc@marcbradshaw.net>
582              
583             =head1 COPYRIGHT AND LICENSE
584              
585             This software is copyright (c) 2020 by Marc Bradshaw.
586              
587             This is free software; you can redistribute it and/or modify it under
588             the same terms as the Perl 5 programming language system itself.
589              
590             =cut