1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.log4j.jdbc; 18 19 import java.sql.Connection; 20 import java.sql.DriverManager; 21 import java.sql.SQLException; 22 import java.sql.Statement; 23 import java.util.ArrayList; 24 import java.util.Iterator; 25 26 import org.apache.log4j.PatternLayout; 27 import org.apache.log4j.spi.ErrorCode; 28 import org.apache.log4j.spi.LoggingEvent; 29 30 31 /** 32 The JDBCAppender provides for sending log events to a database. 33 34 <p><b><font color="#FF2222">WARNING: This version of JDBCAppender 35 is very likely to be completely replaced in the future. Moreoever, 36 it does not log exceptions</font></b>. 37 38 <p>Each append call adds to an <code>ArrayList</code> buffer. When 39 the buffer is filled each log event is placed in a sql statement 40 (configurable) and executed. 41 42 <b>BufferSize</b>, <b>db URL</b>, <b>User</b>, & <b>Password</b> are 43 configurable options in the standard log4j ways. 44 45 <p>The <code>setSql(String sql)</code> sets the SQL statement to be 46 used for logging -- this statement is sent to a 47 <code>PatternLayout</code> (either created automaticly by the 48 appender or added by the user). Therefore by default all the 49 conversion patterns in <code>PatternLayout</code> can be used 50 inside of the statement. (see the test cases for examples) 51 52 <p>Overriding the {@link #getLogStatement} method allows more 53 explicit control of the statement used for logging. 54 55 <p>For use as a base class: 56 57 <ul> 58 59 <li>Override <code>getConnection()</code> to pass any connection 60 you want. Typically this is used to enable application wide 61 connection pooling. 62 63 <li>Override <code>closeConnection(Connection con)</code> -- if 64 you override getConnection make sure to implement 65 <code>closeConnection</code> to handle the connection you 66 generated. Typically this would return the connection to the 67 pool it came from. 68 69 <li>Override <code>getLogStatement(LoggingEvent event)</code> to 70 produce specialized or dynamic statements. The default uses the 71 sql option value. 72 73 </ul> 74 75 @author Kevin Steppe (<A HREF="mailto:ksteppe@pacbell.net">ksteppe@pacbell.net</A>) 76 77 */ 78 public class JDBCAppender extends org.apache.log4j.AppenderSkeleton 79 implements org.apache.log4j.Appender { 80 81 /** 82 * URL of the DB for default connection handling 83 */ 84 protected String databaseURL = "jdbc:odbc:myDB"; 85 86 /** 87 * User to connect as for default connection handling 88 */ 89 protected String databaseUser = "me"; 90 91 /** 92 * User to use for default connection handling 93 */ 94 protected String databasePassword = "mypassword"; 95 96 /** 97 * Connection used by default. The connection is opened the first time it 98 * is needed and then held open until the appender is closed (usually at 99 * garbage collection). This behavior is best modified by creating a 100 * sub-class and overriding the <code>getConnection</code> and 101 * <code>closeConnection</code> methods. 102 */ 103 protected Connection connection = null; 104 105 /** 106 * Stores the string given to the pattern layout for conversion into a SQL 107 * statement, eg: insert into LogTable (Thread, Class, Message) values 108 * ("%t", "%c", "%m"). 109 * 110 * Be careful of quotes in your messages! 111 * 112 * Also see PatternLayout. 113 */ 114 protected String sqlStatement = ""; 115 116 /** 117 * size of LoggingEvent buffer before writting to the database. 118 * Default is 1. 119 */ 120 protected int bufferSize = 1; 121 122 /** 123 * ArrayList holding the buffer of Logging Events. 124 */ 125 protected ArrayList buffer; 126 127 /** 128 * Helper object for clearing out the buffer 129 */ 130 protected ArrayList removes; 131 132 private boolean locationInfo = false; 133 134 public JDBCAppender() { 135 super(); 136 buffer = new ArrayList(bufferSize); 137 removes = new ArrayList(bufferSize); 138 } 139 140 /** 141 * Gets whether the location of the logging request call 142 * should be captured. 143 * 144 * @since 1.2.16 145 * @return the current value of the <b>LocationInfo</b> option. 146 */ 147 public boolean getLocationInfo() { 148 return locationInfo; 149 } 150 151 /** 152 * The <b>LocationInfo</b> option takes a boolean value. By default, it is 153 * set to false which means there will be no effort to extract the location 154 * information related to the event. As a result, the event that will be 155 * ultimately logged will likely to contain the wrong location information 156 * (if present in the log format). 157 * <p/> 158 * <p/> 159 * Location information extraction is comparatively very slow and should be 160 * avoided unless performance is not a concern. 161 * </p> 162 * @since 1.2.16 163 * @param flag true if location information should be extracted. 164 */ 165 public void setLocationInfo(final boolean flag) { 166 locationInfo = flag; 167 } 168 169 170 /** 171 * Adds the event to the buffer. When full the buffer is flushed. 172 */ 173 public void append(LoggingEvent event) { 174 event.getNDC(); 175 event.getThreadName(); 176 // Get a copy of this thread's MDC. 177 event.getMDCCopy(); 178 if (locationInfo) { 179 event.getLocationInformation(); 180 } 181 event.getRenderedMessage(); 182 event.getThrowableStrRep(); 183 buffer.add(event); 184 185 if (buffer.size() >= bufferSize) 186 flushBuffer(); 187 } 188 189 /** 190 * By default getLogStatement sends the event to the required Layout object. 191 * The layout will format the given pattern into a workable SQL string. 192 * 193 * Overriding this provides direct access to the LoggingEvent 194 * when constructing the logging statement. 195 * 196 */ 197 protected String getLogStatement(LoggingEvent event) { 198 return getLayout().format(event); 199 } 200 201 /** 202 * 203 * Override this to provide an alertnate method of getting 204 * connections (such as caching). One method to fix this is to open 205 * connections at the start of flushBuffer() and close them at the 206 * end. I use a connection pool outside of JDBCAppender which is 207 * accessed in an override of this method. 208 * */ 209 protected void execute(String sql) throws SQLException { 210 211 Connection con = null; 212 Statement stmt = null; 213 214 try { 215 con = getConnection(); 216 217 stmt = con.createStatement(); 218 stmt.executeUpdate(sql); 219 } finally { 220 if(stmt != null) { 221 stmt.close(); 222 } 223 closeConnection(con); 224 } 225 226 //System.out.println("Execute: " + sql); 227 } 228 229 230 /** 231 * Override this to return the connection to a pool, or to clean up the 232 * resource. 233 * 234 * The default behavior holds a single connection open until the appender 235 * is closed (typically when garbage collected). 236 */ 237 protected void closeConnection(Connection con) { 238 } 239 240 /** 241 * Override this to link with your connection pooling system. 242 * 243 * By default this creates a single connection which is held open 244 * until the object is garbage collected. 245 */ 246 protected Connection getConnection() throws SQLException { 247 if (!DriverManager.getDrivers().hasMoreElements()) 248 setDriver("sun.jdbc.odbc.JdbcOdbcDriver"); 249 250 if (connection == null) { 251 connection = DriverManager.getConnection(databaseURL, databaseUser, 252 databasePassword); 253 } 254 255 return connection; 256 } 257 258 /** 259 * Closes the appender, flushing the buffer first then closing the default 260 * connection if it is open. 261 */ 262 public void close() 263 { 264 flushBuffer(); 265 266 try { 267 if (connection != null && !connection.isClosed()) 268 connection.close(); 269 } catch (SQLException e) { 270 errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE); 271 } 272 this.closed = true; 273 } 274 275 /** 276 * loops through the buffer of LoggingEvents, gets a 277 * sql string from getLogStatement() and sends it to execute(). 278 * Errors are sent to the errorHandler. 279 * 280 * If a statement fails the LoggingEvent stays in the buffer! 281 */ 282 public void flushBuffer() { 283 //Do the actual logging 284 removes.ensureCapacity(buffer.size()); 285 for (Iterator i = buffer.iterator(); i.hasNext();) { 286 LoggingEvent logEvent = (LoggingEvent)i.next(); 287 try { 288 String sql = getLogStatement(logEvent); 289 execute(sql); 290 } 291 catch (SQLException e) { 292 errorHandler.error("Failed to excute sql", e, 293 ErrorCode.FLUSH_FAILURE); 294 } finally { 295 removes.add(logEvent); 296 } 297 } 298 299 // remove from the buffer any events that were reported 300 buffer.removeAll(removes); 301 302 // clear the buffer of reported events 303 removes.clear(); 304 } 305 306 307 /** closes the appender before disposal */ 308 public void finalize() { 309 close(); 310 } 311 312 313 /** 314 * JDBCAppender requires a layout. 315 * */ 316 public boolean requiresLayout() { 317 return true; 318 } 319 320 321 /** 322 * 323 */ 324 public void setSql(String s) { 325 sqlStatement = s; 326 if (getLayout() == null) { 327 this.setLayout(new PatternLayout(s)); 328 } 329 else { 330 ((PatternLayout)getLayout()).setConversionPattern(s); 331 } 332 } 333 334 335 /** 336 * Returns pre-formated statement eg: insert into LogTable (msg) values ("%m") 337 */ 338 public String getSql() { 339 return sqlStatement; 340 } 341 342 343 public void setUser(String user) { 344 databaseUser = user; 345 } 346 347 348 public void setURL(String url) { 349 databaseURL = url; 350 } 351 352 353 public void setPassword(String password) { 354 databasePassword = password; 355 } 356 357 358 public void setBufferSize(int newBufferSize) { 359 bufferSize = newBufferSize; 360 buffer.ensureCapacity(bufferSize); 361 removes.ensureCapacity(bufferSize); 362 } 363 364 365 public String getUser() { 366 return databaseUser; 367 } 368 369 370 public String getURL() { 371 return databaseURL; 372 } 373 374 375 public String getPassword() { 376 return databasePassword; 377 } 378 379 380 public int getBufferSize() { 381 return bufferSize; 382 } 383 384 385 /** 386 * Ensures that the given driver class has been loaded for sql connection 387 * creation. 388 */ 389 public void setDriver(String driverClass) { 390 try { 391 Class.forName(driverClass); 392 } catch (Exception e) { 393 errorHandler.error("Failed to load driver", e, 394 ErrorCode.GENERIC_FAILURE); 395 } 396 } 397 } 398