1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.goetz.domino.log4j;
18
19 import java.net.InetAddress;
20 import java.net.UnknownHostException;
21 import java.util.Date;
22
23 import lotus.domino.Database;
24 import lotus.domino.DateTime;
25 import lotus.domino.Document;
26 import lotus.domino.NotesException;
27 import lotus.domino.Session;
28
29 import org.apache.log4j.AppenderSkeleton;
30 import org.apache.log4j.Layout;
31 import org.apache.log4j.SimpleLayout;
32 import org.apache.log4j.helpers.LogLog;
33 import org.apache.log4j.helpers.PatternConverter;
34 import org.apache.log4j.helpers.PatternParser;
35 import org.apache.log4j.spi.ErrorCode;
36 import org.apache.log4j.spi.LoggingEvent;
37
38 /***
39 * This is the base appender for a log4j agent and servlet appender
40 * implementation that log to a Domino database using a memory buffer and
41 * delayed writing.
42 *
43 * @author Bernd G?tz
44 */
45 public abstract class AbstractAppender extends AppenderSkeleton {
46
47 /***
48 * The total number of records processed by this Appender.
49 */
50 private long totalLogLinesCount = 0;
51
52 /***
53 * If this appender has been initialized or not.
54 */
55 private boolean initialized = false;
56
57 protected LogDocument doc = new LogDocument();
58
59 private Date lastWrite = new Date();
60
61 private static int DEFAULT_FLUSHTIMEOUT = 20000;
62
63 private int flushTimeout = DEFAULT_FLUSHTIMEOUT;
64
65 /***
66 * Creates a new DominoAppender.
67 */
68 public AbstractAppender() {
69 super();
70 }
71
72 /***
73 * Reads the message property that contains a pattern
74 * converter layout including an additional variable: %u
75 * for agent user.
76 *
77 * @param message the pattern layout
78 */
79 public void setMessage(String message) {
80 doc.setMessage(message);
81 }
82
83 /***
84 * Returns the currently set message.
85 *
86 */
87 public String getMessage() {
88 return doc.getMessage();
89 }
90
91 /***
92 * Sets the name under which the log entries appear.
93 *
94 * @param appName application name
95 */
96 public void setApplicationName(String appName) {
97 doc.setApplicationName(appName);
98 }
99
100 /***
101 * Returns the application name under which the log entries appear.
102 *
103 * @return application name
104 */
105 public String getApplicationName() {
106 return doc.getApplicationName();
107 }
108
109 /***
110 * Sets the form from the log4j properties.
111 *
112 * @param form the form name.
113 */
114 public void setFormName(String formName) {
115 doc.setFormName(formName);
116 }
117
118 /***
119 * Returns the form name.
120 *
121 */
122 public String getFormName() {
123 return doc.getFormName();
124 }
125
126 /***
127 * Sets the internal buffer size.
128 *
129 * @param bufferSize the new buffer size.
130 */
131 public void setMaxLines(int maxLines) {
132 doc.setMaxLines(maxLines);
133 }
134
135 /***
136 * Sets the internal buffer size.
137 *
138 */
139 public int getMaxLines() {
140 return doc.getMaxLines();
141 }
142
143 /***
144 * Sets the Domino serverName to log to.
145 *
146 * @param serverName the server name where the databaseName to log to is on.
147 */
148 public void setServer(String serverName) {
149 doc.setServerName(serverName);
150 }
151
152 /***
153 * Returns the serverName name.
154 *
155 */
156 public String getServer() {
157 return doc.getServerName();
158 }
159
160 /***
161 * Returns the databaseName name.
162 *
163 */
164 public String getDatabase() {
165 return doc.getDatabaseName();
166 }
167
168 /***
169 * Sets the databaseName path to log to.
170 *
171 * @param databaseName the path to the databaseName to log to.
172 */
173 public void setDatabase(String databaseName) {
174 doc.setDatabaseName(databaseName);
175 }
176
177 public int getFlushTimeout() {
178 return flushTimeout;
179 }
180
181 public void setFlushTimeout(int flushTimeout) {
182 this.flushTimeout = flushTimeout;
183 }
184
185 /***
186 * Always returns false. This Appender creates itĄs own
187 * SimpleLayout if no Layout is supplied.
188 *
189 * @return always false
190 */
191 public boolean requiresLayout() {
192 return false;
193 }
194
195 /***
196 * Returns the Layout used by this Appender.
197 *
198 * @return the current Layout.
199 */
200 public Layout getLayout() {
201 return this.layout;
202 }
203
204 /***
205 * Test if this Appender can append LoggingEvents.
206 *
207 * @param event the LoggingEvent to process.
208 * @return false if the appender is not ready to get LoggingEvents,
209 * true otherwise.
210 */
211 protected boolean checkEntryConditions(LoggingEvent event)
212 throws NotesException {
213
214 if (closed) {
215 LogLog.warn("Appender [" + name + "] closed. CanĄt append.");
216 return false;
217 }
218
219 return true;
220 }
221
222 /***
223 * Writes the log entry to the notes document.
224 *
225 * A log4j pattern parser is used. In addition %u is supported for user
226 * name. Sample: %d %t %u - %m
227 *
228 * @param doc current log document
229 * @param event the LoggingEvent to act on.
230 * @throws NotesException if the Appender could not
231 * write to the current Document.
232 */
233 protected void addEvent(LoggingEvent event)
234 throws NotesException {
235
236 String p;
237 if (doc.messageContainsUserVariable()) {
238 p = replace(doc.getMessage(), "%u", retrieveUserName());
239 } else {
240 p = doc.getMessage();
241 }
242 PatternConverter pc = new PatternParser(p).parse();
243
244 StringBuffer buf = new StringBuffer();
245
246 while (pc != null) {
247 pc.format(buf, event);
248 pc = pc.next;
249 }
250 doc.add(buf.toString());
251 totalLogLinesCount++;
252
253
254 if (event.getThrowableInformation() != null) {
255 int l = event.getThrowableStrRep().length;
256 String[] t = event.getThrowableStrRep();
257 for (int i = 0; i < l; i++) {
258 doc.add(t[i]);
259 totalLogLinesCount++;
260 }
261 }
262 }
263
264 /***
265 * Initializes application name and path.
266 *
267 * This is being called only once per appender instance, in contrast to
268 * <code>initAppend()</code> and <code>releaseAppend()</code> which are
269 * being called each log <code>append</code> call.
270 *
271 */
272 protected abstract void initialize(LoggingEvent event)
273 throws Exception;
274
275 /***
276 * Returns the session for this log entry.
277 *
278 * Domino invalidates the session for each new thread, the private session
279 * object.
280 * <p>The method is not called <code>getSession</code> because bean
281 * naming conventions would then interpret "session" as a property of the
282 * agent.</p>
283 *
284 * @return current session.
285 * @throws NotesException
286 */
287 abstract Session retrieveSession() throws NotesException;
288
289 /***
290 * Gets the Domino Database to log to.
291 * Initializes communication with Domino, if this is the first call.
292 *
293 * @return the Domino Database object to log to.
294 */
295 abstract Database getDominoDatabase(Session session)
296 throws NotesException;
297
298 /***
299 * Initialises the appender, e.g. with thread initialisation.
300 *
301 * @throws NotesException
302 */
303 abstract void initAppend() throws NotesException;
304
305 /***
306 * Called to release resources on a per log statement level, e.g. thread
307 * cleanup.
308 *
309 * This method is always called, even in the case of an error. So be aware
310 * that some resources might not be valid anymore.
311 */
312 abstract void releaseAppend();
313
314 /***
315 * Return the user name.
316 *
317 * <p>The method is not called <code>getUserName</code> because bean
318 * naming conventions would then interpret "userName" as a property of the
319 * agent.</p>
320 *
321 * @throws NotesException
322 */
323 abstract String retrieveUserName() throws NotesException;
324
325 /***
326 * Appends the specified event to this DominoAppender. The
327 * Logger is responsible for this.
328 *
329 * @param event the LoggingEvent to append.
330 */
331 public void append(LoggingEvent event) {
332 try {
333 if (closed) {
334 LogLog.warn("Appender '" + getName() +
335 "' closed, canĄt append.");
336 return;
337 }
338
339 if (layout == null) {
340 layout = new SimpleLayout();
341 }
342 initAppend();
343 if (!initialized) {
344 LogLog.debug("Initialize appender '" + getName() +
345 "' now once.");
346 initialize(event);
347 initialized = true;
348 }
349 if (doc.getCurrentSize() == 0) {
350 doc.setStartTime(new Date());
351 }
352 addEvent(event);
353
354
355
356
357
358
359 if (doc.isFull()) {
360 LogLog.debug("Document is full now");
361 writeDocument();
362 totalLogLinesCount += doc.getCurrentLines();
363 doc.reset(new Date());
364 return;
365 }
366 if (((new Date()).getTime() - lastWrite.getTime()) >= flushTimeout) {
367
368 LogLog.debug("Flush timeout value reached");
369 writeDocument();
370 }
371 } catch (NotesException e) {
372 if (e.id == 4000) {
373 LogLog.debug("Field is too large, dropping current document");
374
375 doc.reset(new Date());
376 }
377 errorHandler.error("Received Notes exception", e,
378 ErrorCode.GENERIC_FAILURE);
379 } catch (Exception e) {
380 errorHandler.error("Received exception", e,
381 ErrorCode.GENERIC_FAILURE);
382 } finally {
383 releaseAppend();
384 }
385 }
386
387 /***
388 * Returns the Domino Document that the current LoggingEvents should be
389 * written to.
390 *
391 * @return the Domino Document to write logs to.
392 */
393 protected Document getLogDocument(Database db)
394 throws NotesException {
395
396 LogLog.debug("Return current log document");
397
398 Document document = null;
399
400 if (doc.getCurrentDocId() == null) {
401
402 LogLog.debug("Create new log document");
403 document = db.createDocument();
404 } else {
405
406 try {
407 LogLog.debug("Retrieve log document using id '" +
408 doc.getCurrentDocId() + "'");
409 document = db.getDocumentByID(doc.getCurrentDocId());
410 if ((document == null) || (!document.isValid()) ||
411 (document.isDeleted())) {
412
413 LogLog.debug("Create new log docunent anyway");
414 document = db.createDocument();
415 }
416 } catch (NotesException e) {
417
418 LogLog.debug("Invalid id, create new log document anyway");
419 document = db.createDocument();
420 }
421 }
422 return document;
423 }
424
425 /***
426 * Writes the document to the database without the finish date.
427 *
428 * @param session
429 * @throws NotesException
430 */
431 private void writeDocument()
432 throws NotesException {
433
434 if (!doc.isDirty()) {
435 LogLog.debug("No new entries in cache, don't write to database");
436 return;
437 }
438
439 LogLog.debug("Writing Notes document now...");
440 lastWrite = new Date();
441
442 Session session = null;
443 Database db = null;
444 Document document = null;
445
446 try {
447
448 session = retrieveSession();
449 if (session != null) {
450 db = getDominoDatabase(session);
451 document = getLogDocument(db);
452
453 boolean isNew = document.isNewNote();
454
455 if (isNew) {
456 LogLog.debug("Document is new, initializing...");
457
458 document.replaceItemValue("Form", doc.getFormName());
459 String serverName = session.getServerName();
460 if (serverName.length() == 0) {
461
462 try {
463 LogLog.debug("Trying to get the host name...");
464 serverName = InetAddress.getLocalHost().getHostName();
465 } catch (UnknownHostException e) {
466 LogLog.debug(
467 "Unknown host exception, setting server name to 'local'");
468 serverName = "local";
469 }
470 }
471
472 document.replaceItemValue("Server", serverName);
473 document.replaceItemValue("AppName", doc.getApplicationName());
474 document.replaceItemValue("AppPath", doc.getApplicationPath());
475 DateTime start = retrieveSession().createDateTime(doc.getStartTime());
476 document.replaceItemValue("StartTime", start);
477 start.recycle();
478 }
479
480 LogLog.debug("Setting events now...");
481 document.replaceItemValue("Events", doc.getEvents());
482
483 if (doc.getFinishTime() != null) {
484
485 LogLog.debug("Finish the document now...");
486 DateTime t = retrieveSession().createDateTime("Today");
487 t.setNow();
488 document.replaceItemValue("FinishTime", t);
489 document.replaceItemValue("EventCount",
490 new Long(doc.getCurrentLines()));
491 doc.setCurrentDocId(null);
492 t.recycle();
493 }
494 LogLog.debug("Saving Domino document now...");
495 document.save();
496 if (isNew && (doc.getFinishTime() == null)) {
497
498 doc.setCurrentDocId(document.getNoteID());
499 }
500 }
501 } finally {
502 if (document != null) {
503 document.recycle();
504 }
505 if (db != null) {
506 db.recycle();
507 }
508
509
510
511 }
512 }
513
514 /***
515 * Closes this appender.
516 *
517 */
518 public void close() {
519
520 LogLog.debug("Closing the appender '" + getName() + "'");
521 try {
522 writeDocument();
523 releaseAppend();
524 } catch (NotesException e) {
525 LogLog.error("Caught Notes exception", e);
526 }
527 if (!closed) {
528 closed = true;
529 }
530 }
531
532 /***
533 * Replace a pattern in a string with Java 1.3.
534 *
535 * @param s is the original String which may contain substring aOldPattern
536 * @param oldPattern is the non-empty substring which is to be replaced
537 * @param newPattern is the replacement for aOldPattern
538 */
539 protected String replace(final String s,
540 final String oldPattern, final String newPattern) {
541 if (oldPattern.equals("")) {
542 throw new IllegalArgumentException("Old pattern must have content.");
543 }
544
545 final StringBuffer result = new StringBuffer();
546
547
548 int startIdx = 0;
549 int idxOld = 0;
550 while ((idxOld = s.indexOf(oldPattern, startIdx)) >= 0) {
551
552 result.append(s.substring(startIdx, idxOld));
553
554 result.append(newPattern);
555
556
557
558 startIdx = idxOld + oldPattern.length();
559 }
560
561 result.append(s.substring(startIdx));
562 return result.toString();
563 }
564
565 }