Trog-Log-Syslog.t 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. #!/usr/bin/env perl
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use Test::More;
  6. use Test::NoWarnings;
  7. use Test::Fatal qw{exception};
  8. use File::Temp qw{tempdir};
  9. use IO::Socket::UNIX;
  10. use Socket qw{SOCK_DGRAM};
  11. use Log::Dispatch;
  12. use FindBin::libs;
  13. require_ok('Trog::Log::Syslog') or BAIL_OUT("Can't find SUT");
  14. # A datagram socket in a temporary directory stands in for /dev/log, which is
  15. # a datagram socket too.
  16. my $dir = tempdir( CLEANUP => 1 );
  17. my $path = "$dir/log";
  18. my $sink = IO::Socket::UNIX->new( Type => SOCK_DGRAM, Local => $path ) or die "Cannot listen on $path: $!";
  19. sub received {
  20. my $got = '';
  21. $sink->recv( $got, 65536 );
  22. return $got;
  23. }
  24. # What tPSGI passes to every logger that its configuration names.
  25. my $output = Trog::Log::Syslog->new( min_level => 'info', log_dir => $dir, socket => $path );
  26. my $log = Log::Dispatch->new();
  27. $log->add($output);
  28. subtest 'a line arrives as the syslog of this machine reads it' => sub {
  29. $log->info("2026-09-29T12:00:00Z [INFO]: RequestId x From 192.0.2.1 |nobody| Failed login for user\n");
  30. my $got = received();
  31. # daemon is facility 3 and info is severity 6, so the priority is 3*8+6.
  32. like( $got, qr/^<30>/, 'as daemon.info' );
  33. like( $got, qr/\btcms\[$$\]:[ ]/, 'tagged with the program name and the pid' );
  34. my $line = ': 2026-09-29T12:00:00Z [INFO]: RequestId x From 192.0.2.1 |nobody| Failed login for user';
  35. like( $got, qr/\Q$line\E\z/, 'with the line that the file gets, and no newline' );
  36. };
  37. subtest 'each level keeps its severity' => sub {
  38. my %severity = ( info => 6, notice => 5, warning => 4, error => 3, critical => 2, alert => 1, emergency => 0 );
  39. foreach my $level ( sort keys %severity ) {
  40. $log->log( level => $level, message => "at $level" );
  41. my $priority = 24 + $severity{$level}; # the daemon facility, 3, times 8
  42. like( received(), qr/^<$priority>\N*:[ ]at[ ]$level$/, "$level is severity $severity{$level}" );
  43. }
  44. };
  45. subtest 'nothing below the level goes out' => sub {
  46. $log->debug('too quiet');
  47. $log->info('loud enough');
  48. like( received(), qr/loud[ ]enough$/, 'the debug line was not sent' );
  49. };
  50. subtest 'a syslog that went away does not take the application with it' => sub {
  51. close($sink) or die $!;
  52. unlink($path);
  53. # The file output still has the line, and a request that fails because
  54. # its log line did not arrive is worse than a line that did not arrive.
  55. is( exception { $log->warning('into the void') }, undef, 'logging to a missing socket does not die' );
  56. };
  57. subtest 'log_init adds the loggers that tpsgi.ini names' => sub {
  58. require Trog::Log;
  59. # A stand-in, so that this test does not write to the syslog of the
  60. # machine that runs it.
  61. {
  62. package Test::Capture;
  63. use parent -norequire, qw{Log::Dispatch::Output};
  64. our ( @lines, %given );
  65. sub new {
  66. my ( $class, %p ) = @_;
  67. %given = %p;
  68. my $self = bless( {}, $class );
  69. $self->_basic_init( min_level => $p{min_level} );
  70. return $self;
  71. }
  72. sub log_message { my ( $self, %p ) = @_; push( @lines, $p{message} ); return }
  73. }
  74. local $INC{'Test/Capture.pm'} = __FILE__;
  75. Trog::Log::log_init( "$dir/tcms.log", 'info', ['Test::Capture'] );
  76. Trog::Log::INFO('through every output');
  77. no warnings 'once';
  78. is( $Test::Capture::given{log_dir}, $dir, 'handed the log directory, as tPSGI hands it' );
  79. like( $Test::Capture::lines[-1], qr/through[ ]every[ ]output/, 'and given the lines' );
  80. like( exception { Trog::Log::log_init( "$dir/tcms.log", 'info', ['../../etc/passwd'] ) }, qr/not[ ]a[ ]module[ ]name/, 'a logger that is not a module name is refused' );
  81. };
  82. Test::NoWarnings::had_no_warnings();
  83. done_testing();