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 127     127   2978 use 5.20.0;
  127         589  
3 127     127   760 use strict;
  127         381  
  127         2678  
4 127     127   757 use warnings;
  127         322  
  127         3553  
5 127     127   739 use Mail::Milter::Authentication::Pragmas;
  127         264  
  127         1856  
6             # ABSTRACT: Class for metrics generation
7             our $VERSION = '3.20230911'; # VERSION
8 127     127   96204 use Mail::Milter::Authentication::HTDocs;
  127         392  
  127         4323  
9 127     127   62954 use Mail::Milter::Authentication::Metric::Grafana;
  127         390  
  127         3981  
10 127     127   1498 use File::Temp;
  127         346  
  127         12709  
11 127     127   69558 use Prometheus::Tiny::Shared 0.020;
  127         1000615  
  127         4333  
12 127     127   921 use TOML;
  127         333  
  127         518977  
13              
14              
15              
16              
17             sub new {
18 58     58 1 827 my ( $class, $thischild ) = @_;
19 58         480 my $self = {};
20 58         513 $self->{counter} = {};
21 58         609 $self->{help} = {};
22 58         548 $self->{start_time} = time;
23 58         620 $self->{registered_metrics} = [];
24 58         272 $self->{thischild} = $thischild;
25 58         573 bless $self, $class;
26              
27 58         622 $self->set_handler( undef );
28              
29 58         401 my $config = get_config();
30              
31             $self->{enabled} = defined( $config->{metric_port} ) ? 1
32 58 100       845 : defined( $config->{metric_connection} ) ? 1
    50          
33             : 0;
34              
35 58         588 return $self;
36             }
37              
38              
39             sub set_handler {
40 25686     25686 1 51157 my ( $self, $handler ) = @_;
41 25686         65964 $self->{handler} = $handler;
42             }
43              
44              
45             sub handle_exception {
46 9867     9867 1 25545 my ( $self, $exception ) = @_;
47 9867 50       29130 return if ! defined $exception;
48 9867 100       26891 return if ! defined $self->{handler};
49 9596         35113 $self->{handler}->handle_exception($exception);
50             }
51              
52              
53             sub dbgout {
54 9659     9659 1 24305 my ( $self, $key, $value, $priority ) = @_;
55 9659 100 0     27154 if ( defined ( $self->{handler} ) ) {
    50          
    0          
56 9596         40779 $self->{handler}->dbgout($key,$value,$priority);
57             }
58             elsif ( $priority == LOG_DEBUG ) {
59 63         690 $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 24385     24385 1 50454 my ( $self ) = @_;
72 24385         77585 my $config = get_config();
73              
74 24385         47304 my $metric_tempfile;
75 24385 100       65675 if ( exists( $self->{metric_tempfile} ) ) {
76 24348         50961 $metric_tempfile = $self->{metric_tempfile};
77             }
78             else {
79 37 50       178 if ( defined( $config->{metric_tempfile} ) ) {
80 0         0 $metric_tempfile = $config->{metric_tempfile};
81             }
82 37 50       222 if ( ! $metric_tempfile ) {
83 37         200 $metric_tempfile = $config->{lib_dir}.'/metrics';
84             }
85 37         449 $self->{metric_tempfile} = $metric_tempfile;
86             }
87              
88 24385         48885 my $prom = $self->{prom};
89             # Invalidate if the file does not exist!
90 24385 100       382245 if ( ! -e $metric_tempfile ) {
91 4         28 $prom = undef;
92             }
93 24385 100       283229 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         20 $prom = undef;
97             }
98              
99 24385 100       84272 if ( ! $prom ) {
100 37 50       487 if ( -f $metric_tempfile ) {
101 0         0 unlink $metric_tempfile;
102             }
103 37 100       535 if ( ! -d $metric_tempfile ) {
104 4         484 mkdir $metric_tempfile, 0700;
105             }
106 37         481 $self->dbgout( 'Metrics', "Setup new metrics file $metric_tempfile", LOG_DEBUG );
107 37         153 $prom = eval{ Prometheus::Tiny::Shared->new(filename => $metric_tempfile.'/authmilter_metrics', init_file => 1) };
  37         906  
108 37         68060 $self->handle_exception($@);
109 37 50       218 if ( $prom ) {
110 37         145 $self->{metric_tempfile} = $metric_tempfile;
111 37         271 $prom->declare( 'authmilter_uptime_seconds_total', help => 'Number of seconds since server startup', type => 'counter' );
112 37         10934 $prom->declare( 'authmilter_processes_waiting', help => 'The number of authentication milter processes in a waiting state', type => 'gauge' );
113 37         959 $prom->declare( 'authmilter_processes_processing', help => 'The number of authentication milter processes currently processing data', type => 'gauge' );
114 37         602 $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 24385         60135 $self->{prom} = $prom;
121              
122 24385         128120 return $prom;
123             }
124              
125              
126             sub set_versions {
127 26     26 1 521 my ( $self, $server ) = @_;
128 26 50       392 return if ! $self->{enabled};
129 26 50       508 return if ! $self->prom;
130 26         958 $self->dbgout( 'Metrics', "Setting up versioning metrics", LOG_DEBUG );
131 26         191 $self->prom->set( 'authmilter_version', 1, { version => $Mail::Milter::Authentication::VERSION, module => 'core', ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
132 26         5832 foreach my $Handler ( sort keys %{ $server->{handler} } ) {
  26         560  
133 260 100       876 next if $Handler eq '_Handler';
134 234         649 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         767  
135 234         13436 $self->handle_exception($@);
136             }
137             }
138              
139              
140              
141             sub get_timeout {
142 11     11 1 56 my ( $self ) = @_;
143 11         89 my $config = get_config();
144 11   50     395 return $config->{metric_timeout} || 5;
145             }
146              
147              
148             sub clean_label {
149 59473     59473 1 119324 my ( $self, $text ) = @_;
150 59473         117278 $text = lc $text;
151 59473         207908 $text =~ s/[^a-z0-9]/_/g;
152 59473 100       130253 if ( $text eq q{} ) {
153 71         241 $text = '-none-';
154             }
155 59473         161631 return $text;
156             }
157              
158              
159             sub count {
160 12785     12785 1 24789 my ( $self, $args ) = @_;
161 12785 100       35900 return if ! $self->{enabled};
162 9567 50       25179 return if ! $self->prom;
163              
164 9567         25685 my $count_id = $args->{count_id};
165 9567         18417 my $labels = $args->{labels};
166 9567         18402 my $server = $args->{server};
167 9567         18906 my $count = $args->{count};
168              
169 9567 50       22114 $count = 1 if ! defined $count;
170              
171 9567         25905 $count_id = $self->clean_label( $count_id );
172              
173 9567         22828 my $clean_labels = {};
174 9567 50       23773 if ( $labels ) {
175 9567         48148 foreach my $l ( sort keys %$labels ) {
176 19031         46491 $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} );
177             }
178             }
179              
180 9567         23438 $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT );
181              
182 9567         48868 $self->dbgout( 'Metrics', "Counting $count_id:$count:".join(',',map {"$_=".$clean_labels->{$_}} (sort keys %$clean_labels) ), LOG_DEBUG );
  28598         103799  
183              
184 9567         27701 eval{ $self->prom->add( 'authmilter_' . $count_id, $count, $clean_labels ); };
  9567         27753  
185 9567         774139 $self->handle_exception($@);
186             }
187              
188              
189             sub set {
190 29     29 1 99 my ( $self, $args ) = @_;
191 29 50       109 return if ! $self->{enabled};
192 29 50       130 return if ! $self->prom;
193              
194 29         102 my $gauge_id = $args->{gauge_id};
195 29         88 my $labels = $args->{labels};
196 29         91 my $server = $args->{server};
197 29         95 my $value = $args->{value};
198              
199 29 50       113 die 'metric set must define value' if ! defined $value;
200              
201 29         132 $gauge_id = $self->clean_label( $gauge_id );
202              
203 29         733 my $clean_labels = {};
204 29 50       118 if ( $labels ) {
205 29         240 foreach my $l ( sort keys %$labels ) {
206 38         133 $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} );
207             }
208             }
209              
210 29         122 $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT );
211              
212 29         265 $self->dbgout( 'Metrics', "Setting $gauge_id:$value:".join(',',map {"$_=".$clean_labels->{$_}} (sort keys %$clean_labels) ), LOG_DEBUG );
  67         1755  
213              
214 29         90 eval{ $self->prom->set( 'authmilter_' . $gauge_id, $value, $clean_labels ); };
  29         131  
215 29         2776 $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 488     488 1 1956 my ( $self, $hash ) = @_;
226 488 100       2146 return if ! $self->{enabled};
227 444 50       1698 return if ! $self->prom;
228 444         1041 push @{$self->{registered_metrics}}, $hash;
  444         1659  
229 444         2193 $self->_register_metrics( $hash );
230             }
231              
232              
233             sub re_register_metrics {
234 39     39 0 173 my ( $self ) = @_;
235 39 50       191 return if ! $self->{enabled};
236 39 50       182 return if ! $self->prom;
237 39         192 foreach my $metric ( @{$self->{registered_metrics}} ) {
  39         357  
238 452         19475 $self->_register_metrics( $metric );
239             }
240             }
241              
242             sub _register_metrics {
243 896     896   2197 my ( $self, $hash ) = @_;
244 896 50       2424 return if ! $self->{enabled};
245 896 50       2268 return if ! $self->prom;
246              
247 896         4718 foreach my $metric ( keys %$hash ) {
248 1352         26147 my $data = $hash->{ $metric };
249 1352         2240 my $help;
250 1352         2441 my $type = 'counter';
251 1352 100       3533 if ( ref $data eq 'HASH' ) {
252 142         447 $help = $data->{help};
253 142         400 $type = $data->{type};
254             }
255             else {
256 1210         2071 $help = $data;
257             }
258 1352         3506 $self->prom->declare( 'authmilter_' . $metric, help => $help, type => $type);
259 1352         37957 $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 260     260 1 1663 my ( $self, $server ) = @_;
266 260 50       2050 return if ! $self->{enabled};
267 260 50       3748 return if ! $self->prom;
268              
269 260         1464 eval {
270 260         1611 foreach my $type ( qw { waiting processing } ) {
271 520         37925 $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 629 my ( $self, $server ) = @_;
279 11 50       195 return if ! $self->{enabled};
280              
281 11         134 my $config = get_config();
282              
283 11         62 eval {
284 11     0   935 local $SIG{ALRM} = sub{ die "Timeout\n" };
  0         0  
285 11         155 alarm( $self->get_timeout() );
286              
287 11         115 my $socket = $server->{server}->{client};
288 11         148 my $req;
289              
290 11         251 $PROGRAM_NAME = $Mail::Milter::Authentication::Config::IDENT . ':metrics';
291              
292 11         1429 $req = <$socket>;
293 11         367 $req =~ s/[\n\r]+$//;
294              
295 11 50 33     544 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         186 my $request_method = uc $1;
303 11         69 my $request_uri = $2;
304 11         105 my $server_protocol = $3;
305              
306 11 50       87 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         170 while ( $req = <$socket> ) {
315 11         322 $req =~ s/[\n\r]+$//;
316 11 50       118 last if $req eq q{};
317             }
318              
319 11 50 0     134 if ( $request_uri eq '/metrics' ) {
    0          
    0          
    0          
    0          
320 11 50       98 if ( $self->prom ) {
321 11         305 $server->{handler}->{_Handler}->top_metrics_callback();
322 11         77 $self->prom->set( 'authmilter_uptime_seconds_total', time - $self->{start_time}, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) });
323             }
324              
325 11         1898 print $socket "HTTP/1.0 200 OK\n";
326 11         204 print $socket "Content-Type: text/plain\n";
327 11         149 print $socket "\n";
328 11 50       86 if ( $self->prom ) {
329 11         72 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         28163 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.20230911
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