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