All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.log4j.jdbc.JDBCAppender Maven / Gradle / Ivy

There is a newer version: 6.1.4
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.log4j.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;

import org.apache.log4j.PatternLayout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.spi.LoggingEvent;

/**
 * The JDBCAppender provides for sending log events to a database. It does not log exceptions.
 *
 * 

JDBCAppender shipping in 1.2.18.2 produces * {@link java.sql.PreparedStatement PreparedStatement} instances. Thanks to the remarkable work of Vladimir * Sitnikov JDBCAppender now interprets the SQL expression on the fly and inserts new events using PreparedStartement * instances. Note that the table column types are restricted to those types compatible with Java's String. *

* *

Versions 1.2.18.0 and prior * are vulnerable to SQL injection attacks.. *

* *

* Each append call adds to an {@link ArrayList} buffer. When the buffer is filled each log event is placed in a sql * statement (configurable) and executed. *

* *

BufferSize, db URL, User, & Password are * configurable options in the standard log4j fashion. *

* *

* The setSql(String sql) sets the SQL statement to be used for logging -- this statement is sent to a * PatternLayout (either created automatically by the appender or added by the user). Therefore by default * all the conversion patterns in PatternLayout can be used inside of the statement. (see the test cases * for examples). *

*

As mentioned earlier, the produced * SQL is translated in an additional phase to use {@link java.sql.PreparedStatement PreparedStatement}. *

* * *

* For use as a base class: * *

    * *
  • Override getConnection() to pass any connection you want. * Typically this is used to enable application wide connection pooling. * *
  • Override closeConnection(Connection con) -- if you override * getConnection make sure to implement closeConnection to handle * the connection you generated. Typically this would return the connection to * the pool it came from. * *
  • As of 1.2.18.1 {@link #getLogStatement} is no longer in use. * *
* * @author Kevin Steppe ([email protected]) */ public class JDBCAppender extends org.apache.log4j.AppenderSkeleton implements org.apache.log4j.Appender { // private final Boolean secureSqlReplacement = Boolean // .parseBoolean(System.getProperty("org.apache.log4j.jdbc.JDBCAppender.secure_jdbc_replacement", "true")); private static final IllegalArgumentException ILLEGAL_PATTERN_FOR_SECURE_EXEC = new IllegalArgumentException( "Only org.apache.log4j.PatternLayout is supported for now due to CVE-2022-23305"); /** * URL of the DB for default connection handling */ protected String databaseURL = "jdbc:odbc:myDB"; /** * User to connect as for default connection handling */ protected String databaseUser = "me"; /** * User to use for default connection handling */ protected String databasePassword = "mypassword"; /** * Connection used by default. The connection is opened the first time it is needed and then held open until the * appender is closed (usually at garbage collection). This behavior is best modified by creating a sub-class and * overriding the getConnection and closeConnection methods. */ protected Connection connection = null; /** * Stores the string given to the pattern layout for conversion into a SQL statement, eg: insert into LogTable * (Thread, Class, Message) values ("%t", "%c", "%m"). *

* Be careful of quotes in your messages! *

* Also see PatternLayout. */ protected String sqlStatement = ""; private JdbcPatternParser preparedStatementParser; /** * size of LoggingEvent buffer before writting to the database. Default is 1. */ protected int bufferSize = 1; /** * ArrayList holding the buffer of Logging Events. */ protected ArrayList buffer; private boolean locationInfo = false; private boolean isActive = false; public JDBCAppender() { super(); buffer = new ArrayList(bufferSize); } @Override public void activateOptions() { if (getSql() == null || getSql().trim().length() == 0) { LogLog.error("JDBCAppender.sql parameter is mandatory. Skipping all futher processing"); isActive = false; return; } LogLog.debug("JDBCAppender constructing internal pattern parser"); preparedStatementParser = new JdbcPatternParser(getSql()); isActive = true; } /** * Gets whether the location of the logging request call should be captured. * * @return the current value of the LocationInfo option. * @since 1.2.16 */ public boolean getLocationInfo() { return locationInfo; } /** * The LocationInfo option takes a boolean value. By default, it is set to false which means there will be no * effort to extract the location information related to the event. As a result, the event that will be ultimately * logged will likely to contain the wrong location information (if present in the log format). *

*

* Location information extraction is comparatively very slow and should be avoided unless performance is not a * concern. *

* * @param flag true if location information should be extracted. * @since 1.2.16 */ public void setLocationInfo(final boolean flag) { locationInfo = flag; } /** * Adds the event to the buffer. When full the buffer is flushed. */ public void append(LoggingEvent event) { if (!isActive) { return; } event.getNDC(); event.getThreadName(); // Get a copy of this thread's MDC. event.getMDCCopy(); if (locationInfo) { event.getLocationInformation(); } event.getRenderedMessage(); event.getThrowableStrRep(); buffer.add(event); if (buffer.size() >= bufferSize) flushBuffer(); } /** *

* As of 1.2.18.1 {@link #getLogStatement} is no longer in use. *

* * @param event */ protected String getLogStatement(LoggingEvent event) { return ""; } /** * Override this to provide an alertnate method of getting connections (such as caching). One method to fix this is * to open connections at the start of flushBuffer() and close them at the end. I use a connection pool outside of * JDBCAppender which is accessed in an override of this method. */ protected void execute(String sql) throws SQLException { Connection con = null; Statement stmt = null; try { con = getConnection(); stmt = con.createStatement(); stmt.executeUpdate(sql); } finally { if (stmt != null) { stmt.close(); } closeConnection(con); } // System.out.println("Execute: " + sql); } /** * Override this to return the connection to a pool, or to clean up the resource. *

* The default behavior holds a single connection open until the appender is closed (typically when garbage * collected). */ protected void closeConnection(Connection con) { } /** * Override this to link with your connection pooling system. *

* By default this creates a single connection which is held open until the object is garbage collected. */ protected Connection getConnection() throws SQLException { if (!DriverManager.getDrivers().hasMoreElements()) setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); if (connection == null) { connection = DriverManager.getConnection(databaseURL, databaseUser, databasePassword); } return connection; } /** * Closes the appender, flushing the buffer first then closing the default connection if it is open. */ public void close() { flushBuffer(); try { if (connection != null && !connection.isClosed()) connection.close(); } catch (SQLException e) { errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); } this.closed = true; } /** * loops through the buffer of LoggingEvents, gets a sql string from getLogStatement() and sends it to execute(). * Errors are sent to the errorHandler. *

* If a statement fails the LoggingEvent stays in the buffer! */ public void flushBuffer() { if (buffer.isEmpty()) { return; } flushBufferSecure(); } private void flushBufferSecure() { // Prepare events that we will store to the DB ArrayList removes = new ArrayList(buffer); buffer.removeAll(removes); if (layout.getClass() != PatternLayout.class) { errorHandler.error("Failed to convert pattern " + layout + " to SQL", ILLEGAL_PATTERN_FOR_SECURE_EXEC, ErrorCode.MISSING_LAYOUT); return; } Connection con = null; boolean useBatch = removes.size() > 1; try { con = getConnection(); PreparedStatement ps = con.prepareStatement(preparedStatementParser.getParameterizedSql()); safelyInsertIntoDB(removes, useBatch, ps); } catch (SQLException e) { errorHandler.error("Failed to append messages sql", e, ErrorCode.FLUSH_FAILURE); } finally { if (con != null) { closeConnection(con); } } } private void safelyInsertIntoDB(ArrayList removes, boolean useBatch, PreparedStatement ps) throws SQLException { try { for (LoggingEvent logEvent : removes) { try { preparedStatementParser.setParameters(ps, logEvent); if (useBatch) { ps.addBatch(); } } catch (SQLException e) { errorHandler.error("Failed to append parameters", e, ErrorCode.FLUSH_FAILURE); } } if (useBatch) { ps.executeBatch(); } else { ps.execute(); } } finally { try { ps.close(); } catch (SQLException ignored) { } } } /** * closes the appender before disposal */ public void finalize() { close(); } /** * JDBCAppender requires a layout. */ public boolean requiresLayout() { return true; } /** * */ public void setSql(String s) { sqlStatement = s; if (getLayout() == null) { this.setLayout(new PatternLayout(s)); } else { ((PatternLayout) getLayout()).setConversionPattern(s); } } /** * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m") */ public String getSql() { return sqlStatement; } public void setUser(String user) { databaseUser = user; } public void setURL(String url) { databaseURL = url; } public void setPassword(String password) { databasePassword = password; } public void setBufferSize(int newBufferSize) { bufferSize = newBufferSize; buffer.ensureCapacity(bufferSize); } public String getUser() { return databaseUser; } public String getURL() { return databaseURL; } public String getPassword() { return databasePassword; } public int getBufferSize() { return bufferSize; } /** * Ensures that the given driver class has been loaded for sql connection creation. */ public void setDriver(String driverClass) { try { Class.forName(driverClass); } catch (Exception e) { errorHandler.error("Failed to load driver", e, ErrorCode.GENERIC_FAILURE); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy