DBI.pm 4.7 KB

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