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