package Mail::Milter::Authentication::Metric; use 5.20.0; use strict; use warnings; use Mail::Milter::Authentication::Pragmas; # ABSTRACT: Class for metrics generation our $VERSION = '3.20250611'; # VERSION use Mail::Milter::Authentication::HTDocs; use Mail::Milter::Authentication::Metric::Grafana; use File::Temp; use Prometheus::Tiny::Shared 0.020; use TOML; sub new { my ( $class, $thischild ) = @_; my $self = {}; $self->{counter} = {}; $self->{help} = {}; $self->{start_time} = time; $self->{registered_metrics} = []; $self->{thischild} = $thischild; bless $self, $class; $self->set_handler( undef ); my $config = get_config(); $self->{enabled} = defined( $config->{metric_port} ) ? 1 : defined( $config->{metric_connection} ) ? 1 : 0; my $metric_tempfile; if ( defined( $config->{metric_tempfile} ) ) { $metric_tempfile = $config->{metric_tempfile}; } if ( ! $metric_tempfile ) { $metric_tempfile = $config->{lib_dir}.'/metrics'; } # If metric_tempfile is a regular file then we need to re-init with a directory # this is likely a restart after upgrade. if ( -f $metric_tempfile ) { unlink $metric_tempfile; } if ( ! -d $metric_tempfile ) { mkdir $metric_tempfile, 0700; } $self->dbgout( 'Metrics', "Setup new metrics file $metric_tempfile", LOG_DEBUG ); my $prom = eval{ Prometheus::Tiny::Shared->new(filename => $metric_tempfile.'/authmilter_metrics', init_file => 1) }; $self->handle_exception($@); if ( $prom ) { $self->{prom} = $prom; $self->{metric_tempfile} = $metric_tempfile; $prom->declare( 'authmilter_uptime_seconds_total', help => 'Number of seconds since server startup', type => 'counter' ); $prom->declare( 'authmilter_processes_waiting', help => 'The number of authentication milter processes in a waiting state', type => 'gauge' ); $prom->declare( 'authmilter_processes_processing', help => 'The number of authentication milter processes currently processing data', type => 'gauge' ); $prom->declare( 'authmilter_version', help => 'Running versions', type => 'gauge' ); } else { $self->dbgout( 'Metrics', "Failed to setup new metrics file $metric_tempfile", LOG_ERR ); } return $self; } sub set_handler { my ( $self, $handler ) = @_; $self->{handler} = $handler; } sub handle_exception { my ( $self, $exception ) = @_; return if ! defined $exception; return if ! defined $self->{handler}; $self->{handler}->handle_exception($exception); } sub dbgout { my ( $self, $key, $value, $priority ) = @_; if ( defined ( $self->{handler} ) ) { $self->{handler}->dbgout($key,$value,$priority); } elsif ( $priority == LOG_DEBUG ) { $self->{thischild}->logdebug( "$key: $value" ); } elsif ( $priority == LOG_INFO || $priority == LOG_NOTICE ) { $self->{thischild}->loginfo( "$key: $value" ); } else { $self->{thischild}->logerror( "$key: $value" ); } } sub prom { my ( $self ) = @_; return $self->{prom}; } sub set_versions { my ( $self, $server ) = @_; return if ! $self->{enabled}; return if ! $self->prom; $self->dbgout( 'Metrics', "Setting up versioning metrics", LOG_DEBUG ); $self->prom->set( 'authmilter_version', 1, { version => $Mail::Milter::Authentication::VERSION, module => 'core', ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) }); foreach my $Handler ( sort keys %{ $server->{handler} } ) { next if $Handler eq '_Handler'; eval{ $self->prom->set( 'authmilter_version', 1, { version => $server->{handler}->{ $Handler }->get_version(), module => $Handler, ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) }) }; $self->handle_exception($@); } } sub get_timeout { my ( $self ) = @_; my $config = get_config(); return $config->{metric_timeout} || 5; } sub clean_label { my ( $self, $text ) = @_; $text = lc $text; $text =~ s/[^a-z0-9]/_/g; if ( $text eq q{} ) { $text = '-none-'; } return $text; } sub count { my ( $self, $args ) = @_; return if ! $self->{enabled}; return if ! $self->prom; my $count_id = $args->{count_id}; my $labels = $args->{labels}; my $server = $args->{server}; my $count = $args->{count}; $count = 1 if ! defined $count; $count_id = $self->clean_label( $count_id ); my $clean_labels = {}; if ( $labels ) { foreach my $l ( sort keys %$labels ) { $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} ); } } $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ); $self->dbgout( 'Metrics', "Counting $count_id:$count:".join(',',map {"$_=".$clean_labels->{$_}} (sort keys %$clean_labels) ), LOG_DEBUG ); eval{ $self->prom->add( 'authmilter_' . $count_id, $count, $clean_labels ); }; $self->handle_exception($@); } sub set { my ( $self, $args ) = @_; return if ! $self->{enabled}; return if ! $self->prom; my $gauge_id = $args->{gauge_id}; my $labels = $args->{labels}; my $server = $args->{server}; my $value = $args->{value}; die 'metric set must define value' if ! defined $value; $gauge_id = $self->clean_label( $gauge_id ); my $clean_labels = {}; if ( $labels ) { foreach my $l ( sort keys %$labels ) { $clean_labels->{ $self->clean_label( $l ) } = $self->clean_label( $labels->{$l} ); } } $clean_labels->{ident} = $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ); $self->dbgout( 'Metrics', "Setting $gauge_id:$value:".join(',',map {"$_=".$clean_labels->{$_}} (sort keys %$clean_labels) ), LOG_DEBUG ); eval{ $self->prom->set( 'authmilter_' . $gauge_id, $value, $clean_labels ); }; $self->handle_exception($@); } sub send { ## no critic my ( $self, $server ) = @_; } sub register_metrics { my ( $self, $hash ) = @_; return if ! $self->{enabled}; return if ! $self->prom; push @{$self->{registered_metrics}}, $hash; $self->_register_metrics( $hash ); } sub re_register_metrics { my ( $self ) = @_; return if ! $self->{enabled}; return if ! $self->prom; foreach my $metric ( @{$self->{registered_metrics}} ) { $self->_register_metrics( $metric ); } } sub _register_metrics { my ( $self, $hash ) = @_; return if ! $self->{enabled}; return if ! $self->prom; foreach my $metric ( keys %$hash ) { my $data = $hash->{ $metric }; my $help; my $type = 'counter'; if ( ref $data eq 'HASH' ) { $help = $data->{help}; $type = $data->{type}; } else { $help = $data; } $self->prom->declare( 'authmilter_' . $metric, help => $help, type => $type); $self->prom->add( 'authmilter_' . $metric,0, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) }); } } sub parent_metric_update { my ( $self, $server ) = @_; return if ! $self->{enabled}; return if ! $self->prom; eval { foreach my $type ( qw { waiting processing } ) { $self->prom->set('authmilter_processes_' . $type, $server->{server}->{tally}->{$type}, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) }); } }; } sub child_handler { my ( $self, $server ) = @_; return if ! $self->{enabled}; my $config = get_config(); eval { local $SIG{ALRM} = sub{ die "Timeout\n" }; alarm( $self->get_timeout() ); my $socket = $server->{server}->{client}; my $req; $PROGRAM_NAME = $Mail::Milter::Authentication::Config::IDENT . ':metrics'; $req = <$socket>; $req =~ s/[\n\r]+$//; if (!defined($req) || $req !~ m{ ^\s*(GET|POST|PUT|DELETE|PUSH|HEAD|OPTIONS)\s+(.+)\s+(HTTP/1\.[01])\s*$ }ix) { print $socket "HTTP/1.0 500 Server Error\n"; print $socket "\n"; print $socket "Invalid Request Error\n"; return; } my $request_method = uc $1; my $request_uri = $2; my $server_protocol = $3; if ( $request_method ne 'GET' ) { print $socket "HTTP/1.0 500 Server Error\n"; print $socket "\n"; print $socket "Server Error\n"; return; } # Ignore the rest of the HTTP request while ( $req = <$socket> ) { $req =~ s/[\n\r]+$//; last if $req eq q{}; } if ( $request_uri eq '/metrics' ) { if ( $self->prom ) { $server->{handler}->{_Handler}->top_metrics_callback(); $self->prom->set( 'authmilter_uptime_seconds_total', time - $self->{start_time}, { ident => $self->clean_label( $Mail::Milter::Authentication::Config::IDENT ) }); } print $socket "HTTP/1.0 200 OK\n"; print $socket "Content-Type: text/plain\n"; print $socket "\n"; if ( $self->prom ) { print $socket $self->prom->format(); } else { print $socket '# Metrics unavailable'; } } elsif ( $request_uri eq '/' ){ my $config = get_config(); print $socket "HTTP/1.0 200 OK\n"; print $socket "Content-Type: text/html\n"; print $socket "\n"; print $socket qq{
$stage | " . join( ' ', map{ "$_" } @$callbacks ) . " |