DBI.pm 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package Trog::Log::DBI;
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use parent qw{Log::Dispatch::DBI};
  6. use Ref::Util qw{is_arrayref};
  7. use POSIX qw{mktime};
  8. use POSIX::strptime qw{strptime};
  9. our ( $referer, $ua, $urchin );
  10. =head1 Trog::Log::DBI
  11. A Log::Dispatch::DBI subclass which files tCMS's request log into SQLite, so
  12. that Trog::Log::Metrics has something to compute time series out of.
  13. Rather than storing log lines verbatim, log_message() picks apart the format
  14. Trog::Log emits and stores the pieces in columns. Lines which don't match the
  15. request format are treated as free text belonging to whichever request was in
  16. flight, and are buffered until that request's own line shows up to hang them
  17. off of.
  18. Anything too mangled to identify a request is dropped -- a metrics database is
  19. not the place to go looking for them, the log file has them either way.
  20. =head1 VARIABLES
  21. =over 4
  22. =item $referer, $ua, $urchin
  23. Per-request context which isn't in the log line itself. The routes set these
  24. before responding; they only exist for metrics, which is why they aren't in the
  25. text logs. $urchin is a hashref of utm_* parameters, and is only recorded when
  26. it has a utm_source.
  27. =back
  28. =head1 METHODS
  29. =head2 create_statement() = DBI::st
  30. Called by Log::Dispatch::DBI at construction. Prepares, and returns, the
  31. statement for the requests view, and prepares the two extra statements
  32. log_message() needs for messages and urchin data.
  33. =cut
  34. sub create_statement {
  35. my $self = shift;
  36. # This is a writable view. Consult schema for its behavior.
  37. my $sql = "INSERT INTO all_requests (uuid, date, ip_address, user, method, route, referer, ua, code) VALUES (?,?,?,?,?,?,?,?,?)";
  38. my $sql2 = "INSERT INTO messages (uuid, message) VALUES (?,?)";
  39. $self->{sth2} = $self->{dbh}->prepare($sql2);
  40. my $sql3 = "INSERT INTO urchin_requests (request_uuid, utm_source, utm_medium, utm_campaign, utm_term, utm_content) VALUES (?,?,?,?,?,?)";
  41. $self->{sth3} = $self->{dbh}->prepare($sql3);
  42. return $self->{dbh}->prepare($sql);
  43. }
  44. my %buffer;
  45. # A line as Trog::Log formats it: the time, the level, who asked, and what
  46. # happened.
  47. my $STAMP = qr/[\w:-]+/;
  48. my $REQUEST = qr/[\w-]+/;
  49. my $ADDRESS = qr/[\w.:]+/;
  50. my $WHO = qr/RequestId[ ]($REQUEST)[ ]From[ ]($ADDRESS)[ ]\|(\w+)\|/;
  51. my $LINE = qr/\A($STAMP)[ ]\[(\w+)\]:[ ]$WHO[ ](\N+)/;
  52. # What happened, when that is a request that was served: the method, the
  53. # code, the bytes, and the route.
  54. my $SERVED = qr/\A(\w+)[ ](\d+)[ ](\d+)[ ](\N+)/;
  55. =head2 log_message(HASH params) = MIXED
  56. Record one log line.
  57. Returns the request insert's result for a request line, 1 for a message that
  58. got buffered, and undef for a line we couldn't make sense of or could not write.
  59. A write that fails is a warning on stderr, and never dies.
  60. =cut
  61. sub log_message {
  62. my ( $self, %params ) = @_;
  63. my ( $date, $level, $uuid, $ip, $user, $message ) = $params{message} =~ $LINE;
  64. return unless $uuid;
  65. my ( $method, $code, undef, $route ) = $level eq 'INFO' ? $message =~ $SERVED : ();
  66. # Anything else goes in the "messages" table, once its request is written.
  67. if ( !defined $method ) {
  68. push( @{ $buffer{$uuid} }, $message );
  69. return 1;
  70. }
  71. # 2024-01-20T22:37:41Z
  72. # Transform the date into an epoch so we can do math on it
  73. my $fmt = "%Y-%m-%dT%H:%M:%SZ";
  74. my @cracked = strptime( $date, $fmt );
  75. #XXX get a dumb warning otherwise
  76. pop @cracked;
  77. my $epoch = mktime(@cracked);
  78. # Allow callers to set quasi-tracking parameters.
  79. # We only care about this in DB context, as it's only for metrics, which are irrelevant in text logs/debugging.
  80. $referer //= 'none';
  81. $ua //= 'none';
  82. $urchin //= {};
  83. # The handle raises, and this runs inside every log call, so a failed write warns rather than dies.
  84. my $messages = delete $buffer{$uuid};
  85. my $res;
  86. eval {
  87. # TODO track bytes in the DB
  88. $res = $self->{sth}->execute( $uuid, $epoch, $ip, $user, $method, $route, $referer, $ua, $code );
  89. # Dump in the accumulated messages
  90. if ( is_arrayref($messages) && @$messages ) {
  91. $self->{sth2}->bind_param_array( 1, $uuid );
  92. $self->{sth2}->bind_param_array( 2, $messages );
  93. $self->{sth2}->execute_array( {} );
  94. }
  95. # Record urchin data if there is any.
  96. if ( %$urchin && $urchin->{utm_source} ) {
  97. $self->{sth3}->execute( $uuid, $urchin->{utm_source}, $urchin->{utm_medium}, $urchin->{utm_campaign}, $urchin->{utm_term}, $urchin->{utm_content} );
  98. }
  99. 1;
  100. } or do {
  101. warn "Could not record request $uuid in the metrics database: $@";
  102. return;
  103. };
  104. return $res;
  105. }
  106. 1;