Log.pm 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package Trog::Log;
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use POSIX qw{strftime};
  6. use Module::Runtime();
  7. use Log::Dispatch;
  8. use Log::Dispatch::DBI;
  9. use Log::Dispatch::Screen;
  10. use Log::Dispatch::FileRotate;
  11. use File::Basename qw{dirname};
  12. use Trog::SQLite;
  13. use Trog::Log::DBI;
  14. use Exporter 'import';
  15. our @EXPORT_OK = qw{log_init is_debug INFO DEBUG WARN FATAL};
  16. our %EXPORT_TAGS = ( 'all' => \@EXPORT_OK );
  17. my ( $LEVEL, $LOGDIR );
  18. our ( $log, $user );
  19. $Trog::Log::user = 'nobody';
  20. $Trog::Log::ip = '0.0.0.0';
  21. =head1 Trog::Log
  22. Logging for tCMS.
  23. Everything goes through one Log::Dispatch object with three outputs: a rotating
  24. file for the operator, the screen for anything at error level or above (which
  25. is what ends up in the webserver's own log), and Trog::Log::DBI, which is what
  26. Trog::Log::Metrics later reads its time series out of.
  27. Import the level subroutines you need, or C<:all>:
  28. use Trog::Log qw{:all};
  29. INFO("something happened");
  30. =head1 Termination Conditions
  31. log_init() dies if it isn't told where to log and at what level. FATAL() dies
  32. by design, that being the point of it. Note that the level subroutines will
  33. themselves die on an undefined $log, so log_init() has to have run first --
  34. which the server does per-worker, and CLI tools have to do for themselves.
  35. =head1 VARIABLES
  36. =over 4
  37. =item $Trog::Log::user
  38. Who the current request belongs to. Interpolated into every log line, so that
  39. the request log can be grepped by user. Defaults to 'nobody'.
  40. =item $Trog::Log::ip
  41. Where the current request came from, same deal. Defaults to '0.0.0.0'.
  42. =back
  43. =head1 FUNCTIONS
  44. =head2 log_init(STRING logname, STRING level, [ARRAYREF loggers]) = BOOL
  45. Build the logger. Must be called before anything tries to log.
  46. $logname is the path to the log file; its directory is also where the metrics
  47. database gets put. $level is a Log::Dispatch level, generally 'info' or
  48. 'debug'.
  49. $loggers names more Log::Dispatch outputs to add, by class, and each gets
  50. C<min_level> and C<log_dir> as tPSGI gives them. The server passes the
  51. C<loggers> of tpsgi.ini, so that one setting sends the lines of tPSGI and of
  52. tCMS to the same places. See Trog::Log::Syslog.
  53. Returns 1.
  54. =cut
  55. sub log_init {
  56. my ( $LOGNAME, $LEVEL, $loggers ) = @_;
  57. die "Cannot initialize logs without log name and log level" unless $LOGNAME && $LEVEL;
  58. $LOGNAME //= 'logs/tcms.log';
  59. $LEVEL //= 'info';
  60. $LOGDIR = dirname($LOGNAME);
  61. # By default only log requests & warnings.
  62. # Otherwise emit debug messages.
  63. my $rotate = Log::Dispatch::FileRotate->new(
  64. name => 'tcms',
  65. filename => $LOGNAME,
  66. min_level => $LEVEL,
  67. 'mode' => 'append',
  68. size => 10 * 1024 * 1024,
  69. max => 6,
  70. );
  71. # Only send fatal events/errors to prod-web.log
  72. my $screen = Log::Dispatch::Screen->new(
  73. name => 'screen',
  74. min_level => 'error',
  75. );
  76. # Send things like requests in to the stats log
  77. my $dblog = Trog::Log::DBI->new(
  78. name => 'dbi',
  79. min_level => $LEVEL,
  80. dbh => _dbh(),
  81. );
  82. $log = Log::Dispatch->new();
  83. $log->add($rotate);
  84. $log->add($screen);
  85. $log->add($dblog);
  86. foreach my $logger ( @{ $loggers // [] } ) {
  87. $log->add( Module::Runtime::use_module($logger)->new( min_level => $LEVEL, log_dir => $LOGDIR ) );
  88. }
  89. uuid("INIT");
  90. return 1;
  91. }
  92. #memoize
  93. my $rq;
  94. sub _dbh {
  95. return Trog::SQLite::dbh( 'schema/log.schema', "$LOGDIR/log.db" );
  96. }
  97. =head2 is_debug() = BOOL
  98. Whether we were initialized at debug level. Useful for skipping the
  99. construction of an expensive message which would only be thrown away.
  100. =cut
  101. sub is_debug {
  102. $LEVEL //= 'info';
  103. return $LEVEL eq 'debug';
  104. }
  105. =head2 uuid([STRING requestid]) = STRING
  106. Get, or set, the request ID stamped onto every log line.
  107. The server sets this once per request so that the lines belonging to it can be
  108. picked back out of an interleaved log, and so that Trog::Log::DBI can group a
  109. request's messages under the request itself.
  110. =cut
  111. sub uuid {
  112. my $requestid = shift;
  113. $rq = $requestid if $requestid;
  114. return $requestid // $rq;
  115. }
  116. sub _log {
  117. my ( $msg, $level ) = @_;
  118. $msg //= "No message passed. This is almost certainly a bug. ";
  119. #XXX Log lines must start as an ISO8601 date, anything else breaks fail2ban's beautiful mind
  120. my $tstamp = strftime "%Y-%m-%dT%H:%M:%SZ", gmtime;
  121. # Undef until the server stamps one, or log_init() sets INIT. Anything
  122. # logged before that still deserves a line rather than an uninitialized
  123. # value warning glued into the middle of it.
  124. my $uuid = uuid() // 'NONE';
  125. return "$tstamp [$level]: RequestId $uuid From $Trog::Log::ip |$Trog::Log::user| $msg\n";
  126. }
  127. =head2 DEBUG(STRING msg), INFO(STRING msg), WARN(STRING msg), FATAL(STRING msg)
  128. Log a message at the named level.
  129. Lines are formatted as an ISO8601 timestamp, the level, the request ID, the
  130. originating IP and the user, followed by the message. Don't change the leading
  131. timestamp -- fail2ban parses these.
  132. FATAL logs and then dies, so it never returns.
  133. All four work before log_init() has run, falling back to stderr. Logging is
  134. the thing you reach for when something has already gone wrong, so a logger that
  135. dies because it wasn't set up first turns a diagnostic into an outage -- and
  136. plenty of callers legitimately run uninitialized, such as the scripts in bin/
  137. and the test suite.
  138. =cut
  139. # One path for every level so the uninitialized fallback only exists once.
  140. sub _emit ( $level, $method, $msg ) {
  141. my $line = _log( $msg, $level );
  142. return $log->$method($line) if $log;
  143. warn $line;
  144. return 1;
  145. }
  146. sub DEBUG { return _emit( 'DEBUG', 'debug', shift ) }
  147. sub INFO { return _emit( 'INFO', 'info', shift ) }
  148. sub WARN { return _emit( 'WARN', 'warning', shift ) }
  149. sub FATAL {
  150. my $line = _log( shift, 'FATAL' );
  151. $log->log_and_die( level => 'error', message => $line ) if $log;
  152. # Still fatal with no logger, or the caller's error handling vanishes.
  153. die $line;
  154. }
  155. 1;