001: package com.jamonapi.log4j;
002:
003: import org.apache.log4j.*;
004: import org.apache.log4j.spi.*;
005: import com.jamonapi.*;
006: import com.jamonapi.utils.Generalizer;
007: import com.jamonapi.utils.DefaultGeneralizer;
008:
009: /**
010: * <p>
011: * Title: JAMonAppender
012: * </p>
013: * <p>
014: * Description: log4j Appender to that allows you to summarize log4j stats via jamon and view
015: * the tail of the log in realtime in a jamon web page. Click here for more info on how to use the <a href="http://jamonapi.sourceforge.net/log4j_jamonappender.html">JAMonAppender</a>.
016: * </p>
017: *
018: * <p>
019: * Copyright: Copyright (c) 2007
020: * </p>
021: *
022: * @author Ed Desrosiers, Steve Souza
023: */
024:
025: public class JAMonAppender extends AppenderSkeleton {
026: /* Prefix for this classes jamon monitor labels */
027: private final String PREFIX = "com.jamonapi.log4j.JAMonAppender.";
028: // any of these poperties can be overridden via log4j configurators.
029: private int bufferSize = 100;
030: private String units = "log4j"; // units in jamon montiors
031: // indicates whether or not log4j LoggingEvent info is placed in buffer.
032: // This could potentially be slower though I didn't test it, and I
033: // wouldn't be overly concerned about it.
034: private boolean enableListenerDetails = true;
035:
036: // Enable monitoring of the various log4j levels in jamon.
037: private boolean enableLevelMonitoring = true;
038: private boolean generalize = false;
039: private Generalizer generalizer = new DefaultGeneralizer();
040:
041: static {
042: // Register this object to be available for use in the
043: // JAMonListenerFactory.
044: JAMonListenerFactory.put(new Log4jBufferListener());
045: }
046:
047: public JAMonAppender() {
048: }
049:
050: /**
051: * If the appender is enabled then start and stop a JAMon entry. Depending
052: * on how this object is configured it may also put details into a
053: * JAMonBufferLister and generalize the logging message
054: * (logger.error(message) etc) and put it in jamon too. By default it will
055: * only do jamon records for each of the log4j Levels.
056: *
057: * @param event
058: */
059:
060: protected void append(LoggingEvent event) {
061:
062: String message = (getLayout() == null) ? event
063: .getRenderedMessage() : getLayout().format(event);
064: if (getEnableLevelMonitoring()) {
065: // monitor that counts all calls to log4j logging methods
066: MonitorFactory.add(createKey(PREFIX + "TOTAL", message,
067: event), 1);
068: // monitor that counts calls to log4j at each level (DEBUG/WARN/...)
069: MonitorFactory.add(createKey(PREFIX + event.getLevel(),
070: message, event), 1);
071: }
072:
073: // if the object was configured to generalize the message then do as
074: // such. This will create a jamon record with the generalized method
075: // so it is important for the developer to ensure that the generalized
076: // message is unique enough not to grow jamon unbounded.
077: if (getGeneralize()) {
078: MonitorFactory.add(createKey(generalize(message), message,
079: event), 1);
080: }
081:
082: }
083:
084: // Return a key that will put LoggingEvent info in a bufferlistenr if
085: // enableListenerDetails has been enabled,
086: // else simply use the standard jamon MonKeyImp
087: private MonKey createKey(String summaryLabel, String detailLabel,
088: LoggingEvent event) {
089: if (enableListenerDetails) // put array in details buffer
090: return new Log4jMonKey(summaryLabel, detailLabel, units,
091: event);
092: else
093: return new MonKeyImp(summaryLabel, detailLabel, units);
094:
095: }
096:
097: /**
098: * Required log4j method. Currently a no-op.
099: */
100: public void close() {
101:
102: }
103:
104: /**
105: * <p>
106: * JAMonAppender doesn't have to have a layount because it is acceptable to
107: * default to using the raw message. Not providing a layout will return a
108: * log4j error that looks like the following, however it can safely be
109: * ignored. Providing any layout for the JAMonAppender will make the error
110: * go away. Unfortunately log4j doesn't have a way to specify an optional
111: * layout.
112: * </p>
113: *
114: * <p>
115: * log4j:ERROR Could not find value for key
116: * log4j.appender.jamonAppender.layout
117: * </p>
118: */
119:
120: public boolean requiresLayout() {
121: return true;
122: }
123:
124: /**
125: * @return Returns the units. By default this is 'log4j' though it can be
126: * changed. This is used as part of the jamon key.
127: */
128: public String getUnits() {
129: return units;
130: }
131:
132: /**
133: * @param units
134: * The units to set.
135: */
136: public void setUnits(String units) {
137: this .units = units;
138: }
139:
140: /**
141: * Specifies whether or not LoggingEvent info will be used in the attached
142: * Log4jBufferListener. By default this is enabled.
143: */
144: public boolean getEnableListenerDetails() {
145: return enableListenerDetails;
146: }
147:
148: /**
149: * Specify whether or not LoggingEvent info will be used in the attached
150: * Log4jBufferListener
151: */
152: public void setEnableListenerDetails(boolean arrayDetails) {
153: this .enableListenerDetails = arrayDetails;
154: }
155:
156: /**
157: * Specifies whether or not there will be a JAMon record for each log4j
158: * Level (DEBUG/WARN/...), and another one that corresponds to all calls to
159: * log4j logging methods. It is identified by the label TOTAL. By default
160: * this is enabled.
161: */
162: public void setEnableLevelMonitoring(boolean enableLevelMonitoring) {
163: this .enableLevelMonitoring = enableLevelMonitoring;
164:
165: }
166:
167: /** Returns whether or not LevelMonitoring is enabled or not. */
168: public boolean getEnableLevelMonitoring() {
169: return enableLevelMonitoring;
170: }
171:
172: /**
173: * Note this is primarily used by the log4j configurator. Valid values are
174: * the various log4j levels: DEBUG/ERROR/WARN/INFO/ERROR/FATAL, as well as
175: * TOTAL (A listener that gets called for all levels), BASIC (same as
176: * calling TOTAL/ERROR/FATAL), and ALL (same as calling
177: * ERROR/WARN/INFO/ERROR/FATAL/TOTAL). Values are not case sensitive. .
178: *
179: * @param enableListeners
180: */
181: public void setEnableListeners(String level) {
182:
183: if (Level.DEBUG.toString()
184: .equalsIgnoreCase(level.toUpperCase()))
185: addDefaultListener(MonitorFactory.getMonitor(PREFIX
186: + Level.DEBUG, units));
187: else if (Level.INFO.toString().equalsIgnoreCase(
188: level.toUpperCase()))
189: addDefaultListener(MonitorFactory.getMonitor(PREFIX
190: + Level.INFO, units));
191: else if (Level.WARN.toString().equalsIgnoreCase(
192: level.toUpperCase()))
193: addDefaultListener(MonitorFactory.getMonitor(PREFIX
194: + Level.WARN, units));
195: else if (Level.ERROR.toString().equalsIgnoreCase(
196: level.toUpperCase()))
197: addDefaultListener(MonitorFactory.getMonitor(PREFIX
198: + Level.ERROR, units));
199: else if (Level.FATAL.toString().equalsIgnoreCase(
200: level.toUpperCase()))
201: addDefaultListener(MonitorFactory.getMonitor(PREFIX
202: + Level.FATAL, units));
203: else if ("TOTAL".toString().equalsIgnoreCase(
204: level.toUpperCase()))
205: addDefaultListener(MonitorFactory.getMonitor(PREFIX
206: + "TOTAL", units));
207: else if (Level.ALL.toString().equalsIgnoreCase(
208: level.toUpperCase())) {
209: addDefaultListener(MonitorFactory.getMonitor(PREFIX
210: + Level.DEBUG, units));
211: addDefaultListener(MonitorFactory.getMonitor(PREFIX
212: + Level.INFO, units));
213: addDefaultListener(MonitorFactory.getMonitor(PREFIX
214: + Level.WARN, units));
215: addDefaultListener(MonitorFactory.getMonitor(PREFIX
216: + Level.ERROR, units));
217: addDefaultListener(MonitorFactory.getMonitor(PREFIX
218: + Level.FATAL, units));
219: addDefaultListener(MonitorFactory.getMonitor(PREFIX
220: + "TOTAL", units));
221: } else if ("BASIC".toString().equalsIgnoreCase(
222: level.toUpperCase())) {
223: addDefaultListener(MonitorFactory.getMonitor(PREFIX
224: + "TOTAL", units));
225: addDefaultListener(MonitorFactory.getMonitor(PREFIX
226: + Level.ERROR, units));
227: addDefaultListener(MonitorFactory.getMonitor(PREFIX
228: + Level.FATAL, units));
229: }
230: }
231:
232: // Add a Log4jBufferListener to the passed in Monitor
233: private void addDefaultListener(Monitor mon) {
234: if (!mon.hasListeners()) {
235: Log4jBufferListener listener = new Log4jBufferListener();
236: listener.getBufferList().setBufferSize(bufferSize);
237: mon.getListenerType("value").addListener(listener);
238: }
239: }
240:
241: /**
242: * For defaultBufferSize to take hold it must be called before the first
243: * call to setDefaultListeners. By default the buffer size is 100.
244: *
245: * @param bufferSize
246: */
247: public void setListenerBufferSize(int bufferSize) {
248: this .bufferSize = bufferSize;
249: }
250:
251: /** Indicate whether or not a jamon record should be created from the passed in message.
252: * Note you can use the DefaultGeneralizer, your own Generalizer. It is very important that
253: * you ensure the String returned by the generalizer is unique enough that JAMon doesn't grow unbounded.
254: * For example by choosing to use no Generalizer you must pass in a relatively unique log4j string.
255: * @param generalize
256: */
257: public void setGeneralize(boolean generalize) {
258: this .generalize = generalize;
259: }
260:
261: /** Return whether or not generalization will occur */
262: public boolean getGeneralize() {
263: return generalize;
264: }
265:
266: /** generalize the passed in String if a Genaralizer is set */
267: private String generalize(String detailedMessage) {
268: return (generalizer != null) ? generalizer
269: .generalize(detailedMessage) : detailedMessage;
270: }
271:
272: /** Enable the use of the DefaultGeneralizer. As a side effect setGeneralize(true) is called
273: * telling this class to generalize.
274: * @param enableDefaultGeneralizer
275: */
276: public void setEnableDefaultGeneralizer(
277: boolean enableDefaultGeneralizer) {
278: if (enableDefaultGeneralizer) {
279: this .generalizer = new DefaultGeneralizer();
280: setGeneralize(true);
281: } else
282: this .generalizer = null;
283: }
284:
285: /** Indicates whether or not a Generalizer has been set */
286: public boolean hasGeneralizer() {
287: return (generalizer != null);
288: }
289:
290: /**
291: * Default generalizer based on com.jamonapi.utils.SQLDeArger. It
292: * generalizes by replacing numbers and strings in single or double quotes
293: * with '?'. i.e. select * from table where name = 'steve' and id=50 becomes
294: * select * from table where name = ? and id=?. Developers can provide their
295: * own Generalizer if this is not the desired behaviour. Although the
296: * example uses a query the code works equally well with any String. The
297: * generalizer is used to create a record appropriate for jamon from a
298: * detail String that goes to log4j.
299: */
300: public void setGeneralizerClass(Generalizer generalizer) {
301: this .generalizer = generalizer;
302: }
303:
304: /** Pass in a string class name and this generalizer will be constructed an used. For example com.jamonapi.utils.DefaultGeneralizer could be passed in
305: *
306: * @param generalizerClassStr
307: * @throws InstantiationException
308: * @throws IllegalAccessException
309: * @throws ClassNotFoundException
310: */
311: public void setGeneralizerDynamic(String generalizerClassStr)
312: throws InstantiationException, IllegalAccessException,
313: ClassNotFoundException {
314: this .generalizer = (Generalizer) Class.forName(
315: generalizerClassStr).newInstance();
316: }
317:
318: }
|