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