      1: <?php
      2: /**
      3:  * Licensed to the Apache Software Foundation (ASF) under one or more
      4:  * contributor license agreements.  See the NOTICE file distributed with
      5:  * this work for additional information regarding copyright ownership.
      6:  * The ASF licenses this file to You under the Apache License, Version 2.0
      7:  * (the "License"); you may not use this file except in compliance with
      8:  * the License.  You may obtain a copy of the License at
      9:  *
     10:  *     http://www.apache.org/licenses/LICENSE-2.0
     11:  *
     12:  * Unless required by applicable law or agreed to in writing, software
     13:  * distributed under the License is distributed on an "AS IS" BASIS,
     14:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15:  * See the License for the specific language governing permissions and
     16:  * limitations under the License.
     17:  */
     18:
     19: /**
     20:  * LoggerAppenderPDO appender logs to a database using the PHP's PDO extension.
     21:  *
     22:  * ## Configurable parameters: ##
     23:  *
     24:  * - dsn             - The Data Source Name (DSN) used to connect to the database.
     25:  * - user            - Username used to connect to the database.
     26:  * - password        - Password used to connect to the database.
     27:  * - table           - Name of the table to which log entries are be inserted.
     28:  * - insertSQL       - Sets the insert statement for a logging event. Defaults
     29:  *                     to the correct one - change only if you are sure what you are doing.
     30:  * - insertPattern   - The conversion pattern to use in conjuction with insert
     31:  *                     SQL. Must contain the same number of comma separated
     32:  *                     conversion patterns as there are question marks in the
     33:  *                     insertSQL.
     34:  *
     35:  * @version $Revision: 1374546 $
     36:  * @package log4php
     37:  * @subpackage appenders
     38:  * @since 2.0
     39:  * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
     40:  * @link http://logging.apache.org/log4php/docs/appenders/pdo.html Appender documentation
     41:  */
     42: class LoggerAppenderPDO extends LoggerAppender {
     43:
     44:     // ******************************************
     45:     // *** Configurable parameters            ***
     46:     // ******************************************
     47:
     48:     /**
     49:      * DSN string used to connect to the database.
     50:      * @see http://www.php.net/manual/en/pdo.construct.php
     51:      */
     52:     protected $dsn;
     53:
     54:     /** Database user name. */
     55:     protected $user;
     56:
     57:     /** Database password. */
     58:     protected $password;
     59:
     60:     /**
     61:      * The insert query.
     62:      *
     63:      * The __TABLE__ placeholder will be replaced by the table name from
     64:      * {@link $table}.
     65:      *
     66:      * The questionmarks are part of the prepared statement, and they must
     67:      * match the number of conversion specifiers in {@link insertPattern}.
     68:      */
     69:     protected $insertSQL = "INSERT INTO __TABLE__ (timestamp, logger, level, message, thread, file, line) VALUES (?, ?, ?, ?, ?, ?, ?)";
     70:
     71:     /**
     72:      * A comma separated list of {@link LoggerPatternLayout} format strings
     73:      * which replace the "?" in {@link $insertSQL}.
     74:      *
     75:      * Must contain the same number of comma separated conversion patterns as
     76:      * there are question marks in {@link insertSQL}.
     77:      *
     78:      * @see LoggerPatternLayout For conversion patterns.
     79:      */
     80:     protected $insertPattern = "%date{Y-m-d H:i:s},%logger,%level,%message,%pid,%file,%line";
     81:
     82:     /** Name of the table to which to append log events. */
     83:     protected $table = 'log4php_log';
     84:
     85:     /** The number of recconect attempts to make on failed append. */
     86:     protected $reconnectAttempts = 3;
     87:
     88:
     89:     // ******************************************
     90:     // *** Private memebers                   ***
     91:     // ******************************************
     92:
     93:     /**
     94:      * The PDO instance.
     95:      * @var PDO
     96:      */
     97:     protected $db;
     98:
     99:     /**
    100:      * Prepared statement for the insert query.
    101:      * @var PDOStatement
    102:      */
    103:     protected $preparedInsert;
    104:
    105:     /** This appender does not require a layout. */
    106:     protected $requiresLayout = false;
    107:
    108:
    109:     // ******************************************
    110:     // *** Appender methods                   ***
    111:     // ******************************************
    112:
    113:     /**
    114:      * Acquires a database connection based on parameters.
    115:      * Parses the insert pattern to create a chain of converters which will be
    116:      * used in forming query parameters from logging events.
    117:      */
    118:     public function activateOptions() {
    119:         try {
    120:             $this->establishConnection();
    121:         } catch (PDOException $e) {
    122:             $this->warn("Failed connecting to database. Closing appender. Error: " . $e->getMessage());
    123:             $this->close();
    124:             return;
    125:         }
    126:
    127:         // Parse the insert patterns; pattern parts are comma delimited
    128:         $pieces = explode(',', $this->insertPattern);
    129:         $converterMap = LoggerLayoutPattern::getDefaultConverterMap();
    130:         foreach($pieces as $pattern) {
    131:             $parser = new LoggerPatternParser($pattern, $converterMap);
    132:             $this->converters[] = $parser->parse();
    133:         }
    134:
    135:         $this->closed = false;
    136:     }
    137:
    138:     /**
    139:      * Connects to the database, and prepares the insert query.
    140:      * @throws PDOException If connect or prepare fails.
    141:      */
    142:     protected function establishConnection() {
    143:         // Acquire database connection
    144:         $this->db = new PDO($this->dsn, $this->user, $this->password);
    145:         $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    146:
    147:         // Prepare the insert statement
    148:         $insertSQL = str_replace('__TABLE__', $this->table, $this->insertSQL);
    149:         $this->preparedInsert = $this->db->prepare($insertSQL);
    150:     }
    151:
    152:     /**
    153:      * Appends a new event to the database.
    154:      *
    155:      * If writing to database fails, it will retry by re-establishing the
    156:      * connection up to $reconnectAttempts times. If writing still fails,
    157:      * the appender will close.
    158:      */
    159:     public function append(LoggerLoggingEvent $event) {
    160:
    161:         for ($attempt = 1; $attempt <= $this->reconnectAttempts + 1; $attempt++) {
    162:             try {
    163:                 // Attempt to write to database
    164:                 $this->preparedInsert->execute($this->format($event));
    165:                 $this->preparedInsert->closeCursor();
    166:                 break;
    167:             } catch (PDOException $e) {
    168:                 $this->warn("Failed writing to database: ". $e->getMessage());
    169:
    170:                 // Close the appender if it's the last attempt
    171:                 if ($attempt > $this->reconnectAttempts) {
    172:                     $this->warn("Failed writing to database after {$this->reconnectAttempts} reconnect attempts. Closing appender.");
    173:                     $this->close();
    174:                 // Otherwise reconnect and try to write again
    175:                 } else {
    176:                     $this->warn("Attempting a reconnect (attempt $attempt of {$this->reconnectAttempts}).");
    177:                     $this->establishConnection();
    178:                 }
    179:             }
    180:         }
    181:     }
    182:
    183:     /**
    184:      * Converts the logging event to a series of database parameters by using
    185:      * the converter chain which was set up on activation.
    186:      */
    187:     protected function format(LoggerLoggingEvent $event) {
    188:         $params = array();
    189:         foreach($this->converters as $converter) {
    190:             $buffer = '';
    191:             while ($converter !== null) {
    192:                 $converter->format($buffer, $event);
    193:                 $converter = $converter->next;
    194:             }
    195:             $params[] = $buffer;
    196:         }
    197:         return $params;
    198:     }
    199:
    200:     /**
    201:      * Closes the connection to the logging database
    202:      */
    203:     public function close() {
    204:         // Close the connection (if any)
    205:         $this->db = null;
    206:
    207:         // Close the appender
    208:         $this->closed = true;
    209:     }
    210:
    211:     // ******************************************
    212:     // *** Accessor methods                   ***
    213:     // ******************************************
    214:
    215:     /**
    216:      * Returns the active database handle or null if not established.
    217:      * @return PDO
    218:      */
    219:     public function getDatabaseHandle() {
    220:         return $this->db;
    221:     }
    222:
    223:     /** Sets the username. */
    224:     public function setUser($user) {
    225:         $this->setString('user', $user);
    226:     }
    227:
    228:     /** Returns the username. */
    229:     public function getUser($user) {
    230:         return $this->user;
    231:     }
    232:
    233:     /** Sets the password. */
    234:     public function setPassword($password) {
    235:         $this->setString('password', $password);
    236:     }
    237:
    238:     /** Returns the password. */
    239:     public function getPassword($password) {
    240:         return $this->password;
    241:     }
    242:
    243:     /** Sets the insert SQL. */
    244:     public function setInsertSQL($sql) {
    245:         $this->setString('insertSQL', $sql);
    246:     }
    247:
    248:     /** Returns the insert SQL. */
    249:     public function getInsertSQL($sql) {
    250:         return $this->insertSQL;
    251:     }
    252:
    253:     /** Sets the insert pattern. */
    254:     public function setInsertPattern($pattern) {
    255:         $this->setString('insertPattern', $pattern);
    256:     }
    257:
    258:     /** Returns the insert pattern. */
    259:     public function getInsertPattern($pattern) {
    260:         return $this->insertPattern;
    261:     }
    262:
    263:     /** Sets the table name. */
    264:     public function setTable($table) {
    265:         $this->setString('table', $table);
    266:     }
    267:
    268:     /** Returns the table name. */
    269:     public function getTable($table) {
    270:         return $this->table;
    271:     }
    272:
    273:     /** Sets the DSN string. */
    274:     public function setDSN($dsn) {
    275:         $this->setString('dsn', $dsn);
    276:     }
    277:
    278:     /** Returns the DSN string. */
    279:     public function getDSN($dsn) {
    280:         return $this->setString('dsn', $dsn);
    281:     }
    282: }
    283: