PageRenderTime 38ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Log/Log4perl/Appender/DBI.pm

http://github.com/mschilli/log4perl
Perl | 646 lines | 460 code | 135 blank | 51 comment | 49 complexity | 81f84d410963b68f741aea383d3687c2 MD5 | raw file
  1. package Log::Log4perl::Appender::DBI;
  2. our @ISA = qw(Log::Log4perl::Appender);
  3. use Carp;
  4. use strict;
  5. use DBI;
  6. sub new {
  7. my($proto, %p) = @_;
  8. my $class = ref $proto || $proto;
  9. my $self = bless {}, $class;
  10. $self->_init(%p);
  11. my %defaults = (
  12. reconnect_attempts => 1,
  13. reconnect_sleep => 0,
  14. );
  15. for (keys %defaults) {
  16. if(exists $p{$_}) {
  17. $self->{$_} = $p{$_};
  18. } else {
  19. $self->{$_} = $defaults{$_};
  20. }
  21. }
  22. #e.g.
  23. #log4j.appender.DBAppndr.params.1 = %p
  24. #log4j.appender.DBAppndr.params.2 = %5.5m
  25. foreach my $pnum (keys %{$p{params}}){
  26. $self->{bind_value_layouts}{$pnum} =
  27. Log::Log4perl::Layout::PatternLayout->new({
  28. ConversionPattern => {value => $p{params}->{$pnum}},
  29. undef_column_value => undef,
  30. });
  31. }
  32. #'bind_value_layouts' now contains a PatternLayout
  33. #for each parameter heading for the Sql engine
  34. $self->{SQL} = $p{sql}; #save for error msg later on
  35. $self->{MAX_COL_SIZE} = $p{max_col_size};
  36. $self->{BUFFERSIZE} = $p{bufferSize} || 1;
  37. if ($p{usePreparedStmt}) {
  38. $self->{sth} = $self->create_statement($p{sql});
  39. $self->{usePreparedStmt} = 1;
  40. }else{
  41. $self->{layout} = Log::Log4perl::Layout::PatternLayout->new({
  42. ConversionPattern => {value => $p{sql}},
  43. undef_column_value => undef,
  44. });
  45. }
  46. if ($self->{usePreparedStmt} && $self->{bufferSize}){
  47. warn "Log4perl: you've defined both usePreparedStmt and bufferSize \n".
  48. "in your appender '$p{name}'--\n".
  49. "I'm going to ignore bufferSize and just use a prepared stmt\n";
  50. }
  51. return $self;
  52. }
  53. sub _init {
  54. my $self = shift;
  55. my %params = @_;
  56. if ($params{dbh}) {
  57. $self->{dbh} = $params{dbh};
  58. } else {
  59. $self->{connect} = sub {
  60. DBI->connect(@params{qw(datasource username password)},
  61. {PrintError => 0, $params{attrs} ? %{$params{attrs}} : ()})
  62. or croak "Log4perl: $DBI::errstr";
  63. };
  64. $self->{dbh} = $self->{connect}->();
  65. $self->{_mine} = 1;
  66. }
  67. }
  68. sub create_statement {
  69. my ($self, $stmt) = @_;
  70. $stmt || croak "Log4perl: sql not set in Log4perl::Appender::DBI";
  71. return $self->{dbh}->prepare($stmt) || croak "Log4perl: DBI->prepare failed $DBI::errstr\n$stmt";
  72. }
  73. sub log {
  74. my $self = shift;
  75. my %p = @_;
  76. #%p is
  77. # { name => $appender_name,
  78. # level => loglevel
  79. # message => $message,
  80. # log4p_category => $category,
  81. # log4p_level => $level,);
  82. # },
  83. #getting log4j behavior with no specified ConversionPattern
  84. chomp $p{message} unless ref $p{message};
  85. my $qmarks = $self->calculate_bind_values(\%p);
  86. if ($self->{usePreparedStmt}) {
  87. $self->query_execute($self->{sth}, @$qmarks);
  88. }else{
  89. #first expand any %x's in the statement
  90. my $stmt = $self->{layout}->render(
  91. $p{message},
  92. $p{log4p_category},
  93. $p{log4p_level},
  94. 5 + $Log::Log4perl::caller_depth,
  95. );
  96. push @{$self->{BUFFER}}, $stmt, $qmarks;
  97. $self->check_buffer();
  98. }
  99. }
  100. sub query_execute {
  101. my($self, $sth, @qmarks) = @_;
  102. my $errstr = "[no error]";
  103. for my $attempt (0..$self->{reconnect_attempts}) {
  104. #warn "Exe: @qmarks"; # TODO
  105. if(! $sth->execute(@qmarks)) {
  106. # save errstr because ping() would override it [RT 56145]
  107. $errstr = $self->{dbh}->errstr();
  108. # Exe failed -- was it because we lost the DB
  109. # connection?
  110. if($self->{dbh}->ping()) {
  111. # No, the connection is ok, we failed because there's
  112. # something wrong with the execute(): Bad SQL or
  113. # missing parameters or some such). Abort.
  114. croak "Log4perl: DBI appender error: '$errstr'";
  115. }
  116. if($attempt == $self->{reconnect_attempts}) {
  117. croak "Log4perl: DBI appender failed to " .
  118. ($self->{reconnect_attempts} == 1 ? "" : "re") .
  119. "connect " .
  120. "to database after " .
  121. "$self->{reconnect_attempts} attempt" .
  122. ($self->{reconnect_attempts} == 1 ? "" : "s") .
  123. " (last error error was [$errstr]";
  124. }
  125. if(! $self->{dbh}->ping()) {
  126. # Ping failed, try to reconnect
  127. if($attempt) {
  128. #warn "Sleeping"; # TODO
  129. sleep($self->{reconnect_sleep}) if $self->{reconnect_sleep};
  130. }
  131. eval {
  132. #warn "Reconnecting to DB"; # TODO
  133. $self->{dbh} = $self->{connect}->();
  134. };
  135. }
  136. if ($self->{usePreparedStmt}) {
  137. $sth = $self->create_statement($self->{SQL});
  138. $self->{sth} = $sth if $self->{sth};
  139. } else {
  140. #warn "Pending stmt: $self->{pending_stmt}"; #TODO
  141. $sth = $self->create_statement($self->{pending_stmt});
  142. }
  143. next;
  144. }
  145. return 1;
  146. }
  147. croak "Log4perl: DBI->execute failed $errstr, \n".
  148. "on $self->{SQL}\n @qmarks";
  149. }
  150. sub calculate_bind_values {
  151. my ($self, $p) = @_;
  152. my @qmarks;
  153. my $user_ph_idx = 0;
  154. my $i=0;
  155. if ($self->{bind_value_layouts}) {
  156. my $prev_pnum = 0;
  157. my $max_pnum = 0;
  158. my @pnums = sort {$a <=> $b} keys %{$self->{bind_value_layouts}};
  159. $max_pnum = $pnums[-1];
  160. #Walk through the integers for each possible bind value.
  161. #If it doesn't have a layout assigned from the config file
  162. #then shift it off the array from the $log call
  163. #This needs to be reworked now that we always get an arrayref? --kg 1/2003
  164. foreach my $pnum (1..$max_pnum){
  165. my $msg;
  166. #we've got a bind_value_layout to fill the spot
  167. if ($self->{bind_value_layouts}{$pnum}){
  168. $msg = $self->{bind_value_layouts}{$pnum}->render(
  169. $p->{message},
  170. $p->{log4p_category},
  171. $p->{log4p_level},
  172. 5 + $Log::Log4perl::caller_depth,
  173. );
  174. #we don't have a bind_value_layout, so get
  175. #a message bit
  176. }elsif (ref $p->{message} eq 'ARRAY' && @{$p->{message}}){
  177. #$msg = shift @{$p->{message}};
  178. $msg = $p->{message}->[$i++];
  179. #here handle cases where we ran out of message bits
  180. #before we ran out of bind_value_layouts, just keep going
  181. }elsif (ref $p->{message} eq 'ARRAY'){
  182. $msg = undef;
  183. $p->{message} = undef;
  184. #here handle cases where we didn't get an arrayref
  185. #log the message in the first placeholder and nothing in the rest
  186. }elsif (! ref $p->{message} ){
  187. $msg = $p->{message};
  188. $p->{message} = undef;
  189. }
  190. if ($self->{MAX_COL_SIZE} &&
  191. length($msg) > $self->{MAX_COL_SIZE}){
  192. substr($msg, $self->{MAX_COL_SIZE}) = '';
  193. }
  194. push @qmarks, $msg;
  195. }
  196. }
  197. #handle leftovers
  198. if (ref $p->{message} eq 'ARRAY' && @{$p->{message}} ) {
  199. #push @qmarks, @{$p->{message}};
  200. push @qmarks, @{$p->{message}}[$i..@{$p->{message}}-1];
  201. }
  202. return \@qmarks;
  203. }
  204. sub check_buffer {
  205. my $self = shift;
  206. return unless ($self->{BUFFER} && ref $self->{BUFFER} eq 'ARRAY');
  207. if (scalar @{$self->{BUFFER}} >= $self->{BUFFERSIZE} * 2) {
  208. my ($sth, $stmt, $prev_stmt);
  209. $prev_stmt = ""; # Init to avoid warning (ms 5/10/03)
  210. while (@{$self->{BUFFER}}) {
  211. my ($stmt, $qmarks) = splice (@{$self->{BUFFER}},0,2);
  212. $self->{pending_stmt} = $stmt;
  213. #reuse the sth if the stmt doesn't change
  214. if ($stmt ne $prev_stmt) {
  215. $sth->finish if $sth;
  216. $sth = $self->create_statement($stmt);
  217. }
  218. $self->query_execute($sth, @$qmarks);
  219. $prev_stmt = $stmt;
  220. }
  221. $sth->finish;
  222. my $dbh = $self->{dbh};
  223. if ($dbh && ! $dbh->{AutoCommit}) {
  224. $dbh->commit;
  225. }
  226. }
  227. }
  228. sub DESTROY {
  229. my $self = shift;
  230. $self->{BUFFERSIZE} = 1;
  231. $self->check_buffer();
  232. if ($self->{_mine} && $self->{dbh}) {
  233. $self->{dbh}->disconnect;
  234. }
  235. }
  236. 1;
  237. __END__
  238. =encoding utf8
  239. =head1 NAME
  240. Log::Log4perl::Appender::DBI - implements appending to a DB
  241. =head1 SYNOPSIS
  242. my $config = q{
  243. log4j.category = WARN, DBAppndr
  244. log4j.appender.DBAppndr = Log::Log4perl::Appender::DBI
  245. log4j.appender.DBAppndr.datasource = DBI:CSV:f_dir=t/tmp
  246. log4j.appender.DBAppndr.username = bobjones
  247. log4j.appender.DBAppndr.password = 12345
  248. log4j.appender.DBAppndr.sql = \
  249. insert into log4perltest \
  250. (loglevel, custid, category, message, ipaddr) \
  251. values (?,?,?,?,?)
  252. log4j.appender.DBAppndr.params.1 = %p
  253. #2 is custid from the log() call
  254. log4j.appender.DBAppndr.params.3 = %c
  255. #4 is the message from log()
  256. #5 is ipaddr from log()
  257. log4j.appender.DBAppndr.usePreparedStmt = 1
  258. #--or--
  259. log4j.appender.DBAppndr.bufferSize = 2
  260. #just pass through the array of message items in the log statement
  261. log4j.appender.DBAppndr.layout = Log::Log4perl::Layout::NoopLayout
  262. log4j.appender.DBAppndr.warp_message = 0
  263. #driver attributes support
  264. log4j.appender.DBAppndr.attrs.f_encoding = utf8
  265. };
  266.   Log::Log4perl::init ( \$config ) ;
  267.   my $logger = Log::Log4perl->get_logger () ;
  268. $logger->warn( $custid, 'big problem!!', $ip_addr );
  269. =head1 CAVEAT
  270. This is a very young module and there are a lot of variations
  271. in setups with different databases and connection methods,
  272. so make sure you test thoroughly! Any feedback is welcome!
  273. =head1 DESCRIPTION
  274. This is a specialized Log::Dispatch object customized to work with
  275. log4perl and its abilities, originally based on Log::Dispatch::DBI
  276. by Tatsuhiko Miyagawa but with heavy modifications.
  277. It is an attempted compromise between what Log::Dispatch::DBI was
  278. doing and what log4j's JDBCAppender does. Note the log4j docs say
  279. the JDBCAppender "is very likely to be completely replaced in the future."
  280. The simplest usage is this:
  281. log4j.category = WARN, DBAppndr
  282. log4j.appender.DBAppndr = Log::Log4perl::Appender::DBI
  283. log4j.appender.DBAppndr.datasource = DBI:CSV:f_dir=t/tmp
  284. log4j.appender.DBAppndr.username = bobjones
  285. log4j.appender.DBAppndr.password = 12345
  286. log4j.appender.DBAppndr.sql = \
  287. INSERT INTO logtbl \
  288. (loglevel, message) \
  289. VALUES ('%c','%m')
  290. log4j.appender.DBAppndr.layout = Log::Log4perl::Layout::PatternLayout
  291. $logger->fatal('fatal message');
  292. $logger->warn('warning message');
  293. ===============================
  294. |FATAL|fatal message |
  295. |WARN |warning message |
  296. ===============================
  297. But the downsides to that usage are:
  298. =over 4
  299. =item *
  300. You'd better be darn sure there are not quotes in your log message, or your
  301. insert could have unforeseen consequences! This is a very insecure way to
  302. handle database inserts, using place holders and bind values is much better,
  303. keep reading. (Note that the log4j docs warn "Be careful of quotes in your
  304. messages!") B<*>.
  305. =item *
  306. It's not terribly high-performance, a statement is created and executed
  307. for each log call.
  308. =item *
  309. The only run-time parameter you get is the %m message, in reality
  310. you probably want to log specific data in specific table columns.
  311. =back
  312. So let's try using placeholders, and tell the logger to create a
  313. prepared statement handle at the beginning and just reuse it
  314. (just like Log::Dispatch::DBI does)
  315. log4j.appender.DBAppndr.sql = \
  316. INSERT INTO logtbl \
  317. (custid, loglevel, message) \
  318. VALUES (?,?,?)
  319. #---------------------------------------------------
  320. #now the bind values:
  321. #1 is the custid
  322. log4j.appender.DBAppndr.params.2 = %p
  323. #3 is the message
  324. #---------------------------------------------------
  325. log4j.appender.DBAppndr.layout = Log::Log4perl::Layout::NoopLayout
  326. log4j.appender.DBAppndr.warp_message = 0
  327. log4j.appender.DBAppndr.usePreparedStmt = 1
  328. $logger->warn( 1234, 'warning message' );
  329. Now see how we're using the '?' placeholders in our statement? This
  330. means we don't have to worry about messages that look like
  331. invalid input: 1234';drop table custid;
  332. fubaring our database!
  333. Normally a list of things in the logging statement gets concatenated into
  334. a single string, but setting C<warp_message> to 0 and using the
  335. NoopLayout means that in
  336. $logger->warn( 1234, 'warning message', 'bgates' );
  337. the individual list values will still be available for the DBI appender later
  338. on. (If C<warp_message> is not set to 0, the default behavior is to
  339. join the list elements into a single string. If PatternLayout or SimpleLayout
  340. are used, their attempt to C<render()> your layout will result in something
  341. like "ARRAY(0x841d8dc)" in your logs. More information on C<warp_message>
  342. is in Log::Log4perl::Appender.)
  343. In your insert SQL you can mix up '?' placeholders with conversion specifiers
  344. (%c, %p, etc) as you see fit--the logger will match the question marks to
  345. params you've defined in the config file and populate the rest with values
  346. from your list. If there are more '?' placeholders than there are values in
  347. your message, it will use undef for the rest. For instance,
  348. log4j.appender.DBAppndr.sql = \
  349. insert into log4perltest \
  350. (loglevel, message, datestr, subpoena_id)\
  351. values (?,?,?,?)
  352. log4j.appender.DBAppndr.params.1 = %p
  353. log4j.appender.DBAppndr.params.3 = %d
  354. log4j.appender.DBAppndr.warp_message=0
  355. $logger->info('arrest him!', $subpoena_id);
  356. results in the first '?' placeholder being bound to %p, the second to
  357. "arrest him!", the third to the date from "%d", and the fourth to your
  358. $subpoenaid. If you forget the $subpoena_id and just log
  359. $logger->info('arrest him!');
  360. then you just get undef in the fourth column.
  361. If the logger statement is also being handled by other non-DBI appenders,
  362. they will just join the list into a string, joined with
  363. C<$Log::Log4perl::JOIN_MSG_ARRAY_CHAR> (default is an empty string).
  364. And see the C<usePreparedStmt>? That creates a statement handle when
  365. the logger object is created and just reuses it. That, however, may
  366. be problematic for long-running processes like webservers, in which case
  367. you can use this parameter instead
  368. log4j.appender.DBAppndr.bufferSize=2
  369. This copies log4j's JDBCAppender's behavior, it saves up that many
  370. log statements and writes them all out at once. If your INSERT
  371. statement uses only ? placeholders and no %x conversion specifiers
  372. it should be quite efficient because the logger can re-use the
  373. same statement handle for the inserts.
  374. If the program ends while the buffer is only partly full, the DESTROY
  375. block should flush the remaining statements, if the DESTROY block
  376. runs of course.
  377. * I<As I was writing this, Danko Mannhaupt was coming out with his
  378. improved log4j JDBCAppender (http://www.mannhaupt.com/danko/projects/)
  379. which overcomes many of the drawbacks of the original JDBCAppender.>
  380. =head1 DESCRIPTION 2
  381. Or another way to say the same thing:
  382. The idea is that if you're logging to a database table, you probably
  383. want specific parts of your log information in certain columns. To this
  384. end, you pass an list to the log statement, like
  385. $logger->warn('big problem!!',$userid,$subpoena_nr,$ip_addr);
  386. and the array members drop into the positions defined by the placeholders
  387. in your SQL statement. You can also define information in the config
  388. file like
  389. log4j.appender.DBAppndr.params.2 = %p
  390. in which case those numbered placeholders will be filled in with
  391. the specified values, and the rest of the placeholders will be
  392. filled in with the values from your log statement's array.
  393. =head1 MISC PARAMETERS
  394. =over 4
  395. =item usePreparedStmt
  396. See above.
  397. =item warp_message
  398. see Log::Log4perl::Appender
  399. =item max_col_size
  400. If you're used to just throwing debugging messages like huge stacktraces
  401. into your logger, some databases (Sybase's DBD!!) may surprise you
  402. by choking on data size limitations. Normally, the data would
  403. just be truncated to fit in the column, but Sybases's DBD it turns out
  404. maxes out at 255 characters. Use this parameter in such a situation
  405. to truncate long messages before they get to the INSERT statement.
  406. =back
  407. =head1 CHANGING DBH CONNECTIONS (POOLING)
  408. If you want to get your dbh from some place in particular, like
  409. maybe a pool, subclass and override _init() and/or create_statement(),
  410. for instance
  411. sub _init {
  412. ; #no-op, no pooling at this level
  413. }
  414. sub create_statement {
  415. my ($self, $stmt) = @_;
  416. $stmt || croak "Log4perl: sql not set in ".__PACKAGE__;
  417. return My::Connections->getConnection->prepare($stmt)
  418. || croak "Log4perl: DBI->prepare failed $DBI::errstr\n$stmt";
  419. }
  420. =head1 LIFE OF CONNECTIONS
  421. If you're using C<log4j.appender.DBAppndr.usePreparedStmt>
  422. this module creates an sth when it starts and keeps it for the life
  423. of the program. For long-running processes (e.g. mod_perl), connections
  424. might go stale, but if C<Log::Log4perl::Appender::DBI> tries to write
  425. a message and figures out that the DB connection is no longer working
  426. (using DBI's ping method), it will reconnect.
  427. The reconnection process can be controlled by two parameters,
  428. C<reconnect_attempts> and C<reconnect_sleep>. C<reconnect_attempts>
  429. specifies the number of reconnections attempts the DBI appender
  430. performs until it gives up and dies. C<reconnect_sleep> is the
  431. time between reconnection attempts, measured in seconds.
  432. C<reconnect_attempts> defaults to 1, C<reconnect_sleep> to 0.
  433. Alternatively, use C<Apache::DBI> or C<Apache::DBI::Cache> and read
  434. CHANGING DB CONNECTIONS above.
  435. Note that C<Log::Log4perl::Appender::DBI> holds one connection open
  436. for every appender, which might be too many.
  437. =head1 SEE ALSO
  438. L<Log::Dispatch::DBI>
  439. L<Log::Log4perl::JavaMap::JDBCAppender>
  440. =head1 LICENSE
  441. Copyright 2002-2013 by Mike Schilli E<lt>m@perlmeister.comE<gt>
  442. and Kevin Goess E<lt>cpan@goess.orgE<gt>.
  443. This library is free software; you can redistribute it and/or modify
  444. it under the same terms as Perl itself.
  445. =head1 AUTHOR
  446. Please contribute patches to the project on Github:
  447. http://github.com/mschilli/log4perl
  448. Send bug reports or requests for enhancements to the authors via our
  449. MAILING LIST (questions, bug reports, suggestions/patches):
  450. log4perl-devel@lists.sourceforge.net
  451. Authors (please contact them via the list above, not directly):
  452. Mike Schilli <m@perlmeister.com>,
  453. Kevin Goess <cpan@goess.org>
  454. Contributors (in alphabetical order):
  455. Ateeq Altaf, Cory Bennett, Jens Berthold, Jeremy Bopp, Hutton
  456. Davidson, Chris R. Donnelly, Matisse Enzer, Hugh Esco, Anthony
  457. Foiani, James FitzGibbon, Carl Franks, Dennis Gregorovic, Andy
  458. Grundman, Paul Harrington, Alexander Hartmaier David Hull,
  459. Robert Jacobson, Jason Kohles, Jeff Macdonald, Markus Peter,
  460. Brett Rann, Peter Rabbitson, Erik Selberg, Aaron Straup Cope,
  461. Lars Thegler, David Viner, Mac Yang.