Log.pm 4.6 KB

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