View Javadoc
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  
18  package org.apache.log4j.db;
19  
20  import org.apache.log4j.AppenderSkeleton;
21  import org.apache.log4j.db.dialect.SQLDialect;
22  import org.apache.log4j.db.dialect.Util;
23  import org.apache.log4j.helpers.LogLog;
24  import org.apache.log4j.spi.LocationInfo;
25  import org.apache.log4j.spi.LoggingEvent;
26  import org.apache.log4j.xml.DOMConfigurator;
27  import org.apache.log4j.xml.UnrecognizedElementHandler;
28  import org.w3c.dom.Element;
29  
30  import java.lang.reflect.InvocationTargetException;
31  import java.lang.reflect.Method;
32  import java.sql.Connection;
33  import java.sql.PreparedStatement;
34  import java.sql.ResultSet;
35  import java.sql.SQLException;
36  import java.sql.Statement;
37  import java.util.Iterator;
38  import java.util.Properties;
39  import java.util.Set;
40  
41  
42  /**
43   * The DBAppender inserts loggin events into three database tables in a format
44   * independent of the Java programming language. The three tables that
45   * DBAppender inserts to must exists before DBAppender can be used. These tables
46   * may be created with the help of SQL scripts found in the
47   * <em>src/java/org/apache/log4j/db/dialect</em> directory. There is a
48   * specific script for each of the most popular database systems. If the script
49   * for your particular type of database system is missing, it should be quite
50   * easy to write one, taking example on the already existing scripts. If you
51   * send them to us, we will gladly include missing scripts in future releases.
52   *
53   * <p>
54   * If the JDBC driver you are using supports the
55   * {@link java.sql.Statement#getGeneratedKeys}method introduced in JDBC 3.0
56   * specification, then you are all set. Otherwise, there must be an
57   * {@link SQLDialect}appropriate for your database system. Currently, we have
58   * dialects for PostgreSQL, MySQL, Oracle and MsSQL. As mentioed previously, an
59   * SQLDialect is required only if the JDBC driver for your database system does
60   * not support the {@link java.sql.Statement#getGeneratedKeys getGeneratedKeys}
61   * method.
62   * </p>
63   *
64   * <table border="1" cellpadding="4">
65   * <caption>supported dialects</caption>
66   * <tr>
67   * <th>RDBMS</th>
68   * <th>supports <br><code>getGeneratedKeys()</code> method</th>
69   * <th>specific <br>SQLDialect support</th>
70   * <tr>
71   * <tr>
72   * <td>PostgreSQL</td>
73   * <td align="center">NO</td>
74   * <td>present and used</td>
75   * <tr>
76   * <tr>
77   * <td>MySQL</td>
78   * <td align="center">YES</td>
79   * <td>present, but not actually needed or used</td>
80   * <tr>
81   * <tr>
82   * <td>Oracle</td>
83   * <td align="center">YES</td>
84   * <td>present, but not actually needed or used</td>
85   * <tr>
86   * <tr>
87   * <td>DB2</td>
88   * <td align="center">YES</td>
89   * <td>not present, and not needed or used</td>
90   * <tr>
91   * <tr>
92   * <td>MsSQL</td>
93   * <td align="center">YES</td>
94   * <td>not present, and not needed or used</td>
95   * <tr>
96   * <tr>
97   *   <td>HSQL</td>
98   *    <td align="center">NO</td>
99   *    <td>present and used</td>
100  * <tr>
101  *
102  * </table>
103  * <p>
104  * <b>Performance: </b> Experiments show that writing a single event into the
105  * database takes approximately 50 milliseconds, on a "standard" PC. If pooled
106  * connections are used, this figure drops to under 10 milliseconds. Note that
107  * most JDBC drivers already ship with connection pooling support.
108  * </p>
109  *
110  *
111  *
112  * <p>
113  * <b>Configuration </b> DBAppender can be configured programmatically, or using
114  * {@link org.apache.log4j.xml.DOMConfigurator JoranConfigurator}. Example
115  * scripts can be found in the <em>tests/input/db</em> directory.
116  *
117  * @author Ceki G&uuml;lc&uuml;
118  * @author Ray DeCampo
119  */
120 public class DBAppender extends AppenderSkeleton implements UnrecognizedElementHandler {
121   static final String insertPropertiesSQL =
122     "INSERT INTO  logging_event_property (event_id, mapped_key, mapped_value) VALUES (?, ?, ?)";
123   static final String insertExceptionSQL =
124     "INSERT INTO  logging_event_exception (event_id, i, trace_line) VALUES (?, ?, ?)";
125   static final String insertSQL;
126   private static final Method GET_GENERATED_KEYS_METHOD;
127 
128 
129   static {
130     StringBuffer sql = new StringBuffer();
131     sql.append("INSERT INTO logging_event (");
132     sql.append("sequence_number, ");
133     sql.append("timestamp, ");
134     sql.append("rendered_message, ");
135     sql.append("logger_name, ");
136     sql.append("level_string, ");
137     sql.append("ndc, ");
138     sql.append("thread_name, ");
139     sql.append("reference_flag, ");
140     sql.append("caller_filename, ");
141     sql.append("caller_class, ");
142     sql.append("caller_method, ");
143     sql.append("caller_line) ");
144     sql.append(" VALUES (?, ?, ? ,?, ?, ?, ?, ?, ?, ?, ?, ?)");
145     insertSQL = sql.toString();
146     //
147     //   PreparedStatement.getGeneratedKeys added in JDK 1.4
148     //
149     Method getGeneratedKeysMethod;
150     try {
151         getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", null);
152     } catch(Exception ex) {
153         getGeneratedKeysMethod = null;
154     }
155     GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
156   }
157 
158   ConnectionSource connectionSource;
159   boolean cnxSupportsGetGeneratedKeys = false;
160   boolean cnxSupportsBatchUpdates = false;
161   SQLDialect sqlDialect;
162   boolean locationInfo = false;
163   
164 
165   public DBAppender() {
166       super(false);
167   }
168 
169   public void activateOptions() {
170     LogLog.debug("DBAppender.activateOptions called");
171 
172     if (connectionSource == null) {
173       throw new IllegalStateException(
174         "DBAppender cannot function without a connection source");
175     }
176 
177     sqlDialect = Util.getDialectFromCode(connectionSource.getSQLDialectCode());
178     if (GET_GENERATED_KEYS_METHOD != null) {
179         cnxSupportsGetGeneratedKeys = connectionSource.supportsGetGeneratedKeys();
180     } else {
181         cnxSupportsGetGeneratedKeys = false;
182     }
183     cnxSupportsBatchUpdates = connectionSource.supportsBatchUpdates();
184     if (!cnxSupportsGetGeneratedKeys && (sqlDialect == null)) {
185       throw new IllegalStateException(
186         "DBAppender cannot function if the JDBC driver does not support getGeneratedKeys method *and* without a specific SQL dialect");
187     }
188     
189     // all nice and dandy on the eastern front
190     super.activateOptions();
191   }
192 
193   /**
194    * @return Returns the connectionSource.
195    */
196   public ConnectionSource getConnectionSource() {
197     return connectionSource;
198   }
199 
200   /**
201    * @param connectionSource
202    *          The connectionSource to set.
203    */
204   public void setConnectionSource(ConnectionSource connectionSource) {
205     LogLog.debug("setConnectionSource called for DBAppender");
206     this.connectionSource = connectionSource;
207   }
208 
209   protected void append(LoggingEvent event) {
210       Connection connection = null;
211       try {
212           connection = connectionSource.getConnection();
213           connection.setAutoCommit(false);
214           
215           PreparedStatement insertStatement;
216           if (cnxSupportsGetGeneratedKeys) {
217         	  insertStatement = connection.prepareStatement(insertSQL, Statement.RETURN_GENERATED_KEYS);
218           } else {
219               insertStatement = connection.prepareStatement(insertSQL);
220           }
221 
222 /*          insertStatement.setLong(1, event.getSequenceNumber());*/
223 		  insertStatement.setLong(1, 0);
224 		
225           insertStatement.setLong(2, event.getTimeStamp());
226           insertStatement.setString(3, event.getRenderedMessage());
227           insertStatement.setString(4, event.getLoggerName());
228           insertStatement.setString(5, event.getLevel().toString());
229           insertStatement.setString(6, event.getNDC());
230           insertStatement.setString(7, event.getThreadName());
231           insertStatement.setShort(8, DBHelper.computeReferenceMask(event));
232           
233           LocationInfo li;
234           
235           if (event.locationInformationExists() || locationInfo) {
236               li = event.getLocationInformation();
237           } else {
238               li = LocationInfo.NA_LOCATION_INFO;
239           }
240           
241           insertStatement.setString(9, li.getFileName());
242           insertStatement.setString(10, li.getClassName());
243           insertStatement.setString(11, li.getMethodName());
244           insertStatement.setString(12, li.getLineNumber());
245           
246           int updateCount = insertStatement.executeUpdate();
247           if (updateCount != 1) {
248               LogLog.warn("Failed to insert loggingEvent");
249           }
250           
251           ResultSet rs = null;
252           Statement idStatement = null;
253           boolean gotGeneratedKeys = false;
254           if (cnxSupportsGetGeneratedKeys) {
255               try {
256                   rs = (ResultSet) GET_GENERATED_KEYS_METHOD.invoke(insertStatement, null);
257                   gotGeneratedKeys = true;
258               } catch(InvocationTargetException ex) {
259                   Throwable target = ex.getTargetException();
260                   if (target instanceof SQLException) {
261                       throw (SQLException) target;
262                   }
263                   throw ex; 
264               } catch(IllegalAccessException ex) {
265                   LogLog.warn("IllegalAccessException invoking PreparedStatement.getGeneratedKeys", ex);
266               }
267           }
268           
269           if (!gotGeneratedKeys) {
270               insertStatement.close();
271               insertStatement = null;
272               
273               idStatement = connection.createStatement();
274               idStatement.setMaxRows(1);
275               rs = idStatement.executeQuery(sqlDialect.getSelectInsertId());
276           }
277           
278           // A ResultSet cursor is initially positioned before the first row; the 
279           // first call to the method next makes the first row the current row
280           rs.next();
281           int eventId = rs.getInt(1);
282           
283           rs.close();
284 
285           // we no longer need the insertStatement
286           if(insertStatement != null) {
287               insertStatement.close();
288               insertStatement = null;
289           }
290 
291           if(idStatement != null) {
292               idStatement.close();
293               idStatement = null;
294           }
295 
296           Set propertiesKeys = event.getPropertyKeySet();
297           
298           if (propertiesKeys.size() > 0) {
299               PreparedStatement insertPropertiesStatement =
300                   connection.prepareStatement(insertPropertiesSQL);
301               
302               for (Iterator i = propertiesKeys.iterator(); i.hasNext();) {
303                   String key = (String) i.next();
304                   String value = (String) event.getProperty(key);
305                   
306                   //LogLog.info("id " + eventId + ", key " + key + ", value " + value);
307                   insertPropertiesStatement.setInt(1, eventId);
308                   insertPropertiesStatement.setString(2, key);
309                   insertPropertiesStatement.setString(3, value);
310                   
311                   if (cnxSupportsBatchUpdates) {
312                       insertPropertiesStatement.addBatch();
313                   } else {
314                       insertPropertiesStatement.execute();
315                   }
316               }
317               
318               if (cnxSupportsBatchUpdates) {
319                   insertPropertiesStatement.executeBatch();
320               }
321               
322               insertPropertiesStatement.close();
323               insertPropertiesStatement = null;
324           }
325           
326           String[] strRep = event.getThrowableStrRep();
327           
328           if (strRep != null) {
329               LogLog.debug("Logging an exception");
330               
331               PreparedStatement insertExceptionStatement =
332                   connection.prepareStatement(insertExceptionSQL);
333               
334               for (short i = 0; i < strRep.length; i++) {
335                   insertExceptionStatement.setInt(1, eventId);
336                   insertExceptionStatement.setShort(2, i);
337                   insertExceptionStatement.setString(3, strRep[i]);
338                   if (cnxSupportsBatchUpdates) {
339                       insertExceptionStatement.addBatch();
340                   } else {
341                       insertExceptionStatement.execute();
342                   }
343               }
344               if (cnxSupportsBatchUpdates) {
345                   insertExceptionStatement.executeBatch();
346               }
347               insertExceptionStatement.close();
348               insertExceptionStatement = null;
349           }
350           
351           connection.commit();
352       } catch (Throwable sqle) {
353           LogLog.error("problem appending event", sqle);
354       } finally {
355           DBHelper.closeConnection(connection);
356       }
357   }
358 
359   public void close() {
360     closed = true;
361   }
362 
363   /**
364    * Returns value of the <b>LocationInfo </b> property which determines whether
365    * caller's location info is written to the database.
366    */
367   public boolean getLocationInfo() {
368     return locationInfo;
369   }
370 
371   /**
372    * If true, the information written to the database will include caller's
373    * location information. Due to performance concerns, by default no location
374    * information is written to the database.
375    */
376   public void setLocationInfo(boolean locationInfo) {
377     this.locationInfo = locationInfo;
378   }
379 
380     /**
381      * Gets whether appender requires a layout.
382      * @return false
383      */
384   public boolean requiresLayout() {
385       return false;
386   }
387 
388     /**
389      * {@inheritDoc}
390      */
391   public boolean parseUnrecognizedElement(Element element, Properties props) throws Exception {
392         if ("connectionSource".equals(element.getNodeName())) {
393             Object instance =
394                     DOMConfigurator.parseElement(element, props, ConnectionSource.class);
395             if (instance instanceof ConnectionSource) {
396                ConnectionSource source = (ConnectionSource) instance;
397                source.activateOptions();
398                setConnectionSource(source);
399             }
400             return true;
401         }
402         return false;
403   }
404 }