1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 package org.archive.jmx;
27
28 import java.io.IOException;
29 import java.io.PrintWriter;
30 import java.io.StringWriter;
31 import java.text.FieldPosition;
32 import java.text.ParseException;
33 import java.text.SimpleDateFormat;
34 import java.util.Date;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.logging.ConsoleHandler;
40 import java.util.logging.Handler;
41 import java.util.logging.LogRecord;
42 import java.util.logging.Logger;
43 import java.util.logging.SimpleFormatter;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46
47 import javax.management.Attribute;
48 import javax.management.AttributeList;
49 import javax.management.InstanceNotFoundException;
50 import javax.management.IntrospectionException;
51 import javax.management.MBeanAttributeInfo;
52 import javax.management.MBeanFeatureInfo;
53 import javax.management.MBeanInfo;
54 import javax.management.MBeanOperationInfo;
55 import javax.management.MBeanParameterInfo;
56 import javax.management.MBeanServerConnection;
57 import javax.management.MalformedObjectNameException;
58 import javax.management.ObjectInstance;
59 import javax.management.ObjectName;
60 import javax.management.ReflectionException;
61 import javax.management.openmbean.CompositeData;
62 import javax.management.openmbean.TabularData;
63 import javax.management.remote.JMXConnector;
64 import javax.management.remote.JMXConnectorFactory;
65 import javax.management.remote.JMXServiceURL;
66
67
68 /***
69 * A Simple Command-Line JMX Client.
70 * Tested against the JDK 1.5.0 JMX Agent.
71 * See <a href="http://java.sun.com/j2se/1.5.0/docs/guide/management/agent.html">Monitoring
72 * and Management Using JMX</a>.
73 * <p>Can supply credentials and do primitive string representation of tabular
74 * and composite openmbeans.
75 * @author stack
76 */
77 public class Client {
78 private static final Logger logger =
79 Logger.getLogger(Client.class.getName());
80
81 /***
82 * Usage string.
83 */
84 private static final String USAGE = "Usage: java -jar" +
85 " cmdline-jmxclient.jar USER:PASS HOST:PORT [BEAN] [COMMAND]\n" +
86 "Options:\n" +
87 " USER:PASS Username and password. Required. If none, pass '-'.\n" +
88 " E.g. 'controlRole:secret'\n" +
89 " HOST:PORT Hostname and port to connect to. Required." +
90 " E.g. localhost:8081.\n" +
91 " Lists registered beans if only USER:PASS and this" +
92 " argument.\n" +
93 " BEAN Optional target bean name. If present we list" +
94 " available operations\n" +
95 " and attributes.\n" +
96 " COMMAND Optional operation to run or attribute to fetch. If" +
97 " none supplied,\n" +
98 " all operations and attributes are listed. Attributes" +
99 " begin with a\n" +
100 " capital letter: e.g. 'Status' or 'Started'." +
101 " Operations do not.\n" +
102 " Operations can take arguments by adding an '=' " +
103 "followed by\n" +
104 " comma-delimited params. Pass multiple " +
105 "attributes/operations to run\n" +
106 " more than one per invocation. Use commands 'create' and " +
107 "'destroy'\n" +
108 " to instantiate and unregister beans ('create' takes name " +
109 "of class).\n" +
110 " Pass 'Attributes' to get listing of all attributes and " +
111 "and their\n" +
112 " values.\n" +
113 "Requirements:\n" +
114 " JDK1.5.0. If connecting to a SUN 1.5.0 JDK JMX Agent, remote side" +
115 " must be\n" +
116 " started with system properties such as the following:\n" +
117 " -Dcom.sun.management.jmxremote.port=PORT\n" +
118 " -Dcom.sun.management.jmxremote.authenticate=false\n" +
119 " -Dcom.sun.management.jmxremote.ssl=false\n" +
120 " The above will start the remote server with no password. See\n" +
121 " http://java.sun.com/j2se/1.5.0/docs/guide/management/agent.html" +
122 " for more on\n" +
123 " 'Monitoring and Management via JMX'.\n" +
124 "Client Use Examples:\n" +
125 " To list MBeans on a non-password protected remote agent:\n" +
126 " % java -jar cmdline-jmxclient-X.X.jar - localhost:8081 //\n" +
127 " org.archive.crawler:name=Heritrix,type=Service\n" +
128 " To list attributes and attributes of the Heritrix MBean:\n" +
129 " % java -jar cmdline-jmxclient-X.X.jar - localhost:8081 //\n" +
130 " org.archive.crawler:name=Heritrix,type=Service //\n" +
131 " schedule=http://www.archive.org\n" +
132 " To set set logging level to FINE on a password protected JVM:\n" +
133 " % java -jar cmdline-jmxclient-X.X.jar controlRole:secret" +
134 " localhost:8081 //\n" +
135 " java.util.logging:type=Logging //\n" +
136 " setLoggerLevel=org.archive.crawler.Heritrix,FINE";
137
138 /***
139 * Pattern that matches a command name followed by
140 * an optional equals and optional comma-delimited list
141 * of arguments.
142 */
143 protected static final Pattern CMD_LINE_ARGS_PATTERN =
144 Pattern.compile("^([^=]+)(?:(?://=)(.+))?$");
145
146 private static final String CREATE_CMD_PREFIX = "create=";
147
148 public static void main(String[] args) throws Exception {
149 Client client = new Client();
150
151 Logger l = Logger.getLogger("");
152 Handler [] hs = l.getHandlers();
153 for (int i = 0; i < hs.length; i++) {
154 Handler h = hs[0];
155 if (h instanceof ConsoleHandler) {
156 h.setFormatter(client.new OneLineSimpleLogger());
157 }
158 }
159 client.execute(args);
160 }
161
162 protected static void usage() {
163 usage(0, null);
164 }
165
166 protected static void usage(int exitCode, String message) {
167 if (message != null && message.length() > 0) {
168 System.out.println(message);
169 }
170 System.out.println(USAGE);
171 System.exit(exitCode);
172 }
173
174 /***
175 * Constructor.
176 */
177 public Client() {
178 super();
179 }
180
181 /***
182 * Parse a 'login:password' string. Assumption is that no
183 * colon in the login name.
184 * @param userpass
185 * @return Array of strings with login in first position.
186 */
187 protected String [] parseUserpass(final String userpass) {
188 if (userpass == null || userpass.equals("-")) {
189 return null;
190 }
191 int index = userpass.indexOf(':');
192 if (index <= 0) {
193 throw new RuntimeException("Unable to parse: " +userpass);
194 }
195 return new String [] {userpass.substring(0, index),
196 userpass.substring(index + 1)};
197 }
198
199 /***
200 * @param login
201 * @param password
202 * @return Credentials as map for RMI.
203 */
204 protected Map formatCredentials(final String login,
205 final String password) {
206 Map env = null;
207 String[] creds = new String[] {login, password};
208 env = new HashMap(1);
209 env.put(JMXConnector.CREDENTIALS, creds);
210 return env;
211 }
212
213 protected JMXConnector getJMXConnector(final String hostport,
214 final String login, final String password)
215 throws IOException {
216
217 JMXServiceURL rmiurl = new JMXServiceURL("service:jmx:rmi://"
218 + hostport + "/jndi/rmi://" + hostport + "/jmxrmi");
219 return JMXConnectorFactory.connect(rmiurl,
220 formatCredentials(login, password));
221 }
222
223 protected ObjectName getObjectName(final String beanname)
224 throws MalformedObjectNameException, NullPointerException {
225 return notEmpty(beanname)? new ObjectName(beanname): null;
226 }
227
228 /***
229 * Version of execute called from the cmdline.
230 * Prints out result of execution on stdout.
231 * Parses cmdline args. Then calls {@link #execute(String, String,
232 * String, String, String[], boolean)}.
233 * @param args Cmdline args.
234 * @throws Exception
235 */
236 protected void execute(final String [] args)
237 throws Exception {
238
239 if (args.length == 0 || args.length == 1) {
240 usage();
241 }
242 String userpass = args[0];
243 String hostport = args[1];
244 String beanname = null;
245 String [] command = null;
246 if (args.length > 2) {
247 beanname = args[2];
248 }
249 if (args.length > 3) {
250 command = new String [args.length - 3];
251 for (int i = 3; i < args.length; i++) {
252 command[i - 3] = args[i];
253 }
254 }
255 String [] loginPassword = parseUserpass(userpass);
256 Object [] result = execute(hostport,
257 ((loginPassword == null)? null: loginPassword[0]),
258 ((loginPassword == null)? null: loginPassword[1]), beanname,
259 command);
260
261 if (result != null) {
262 for (int i = 0; i < result.length; i++) {
263 if (result[i] != null && result[i].toString().length() > 0) {
264 if (command != null) {
265 logger.info(command[i] + ": " + result[i]);
266 } else {
267 logger.info("\n" + result[i].toString());
268 }
269 }
270 }
271 }
272 }
273
274 protected Object [] execute(final String hostport, final String login,
275 final String password, final String beanname,
276 final String [] command)
277 throws Exception {
278 return execute(hostport, login, password, beanname, command, false);
279 }
280
281 public Object [] executeOneCmd(final String hostport, final String login,
282 final String password, final String beanname,
283 final String command)
284 throws Exception {
285 return execute(hostport, login, password, beanname,
286 new String[] {command}, true);
287 }
288
289 /***
290 * Execute command against remote JMX agent.
291 * @param hostport 'host:port' combination.
292 * @param login RMI login to use.
293 * @param password RMI password to use.
294 * @param beanname Name of remote bean to run command against.
295 * @param command Array of commands to run.
296 * @param oneBeanOnly Set true if passed <code>beanname</code> is
297 * an exact name and the query for a bean is only supposed to return
298 * one bean instance. If not, we raise an exception (Otherwise, if false,
299 * then we deal with possibility of multiple bean instances coming back
300 * from query). Set to true when want to get an attribute or run an
301 * operation.
302 * @return Array of results -- one per command.
303 * @throws Exception
304 */
305 protected Object [] execute(final String hostport, final String login,
306 final String password, final String beanname,
307 final String [] command, final boolean oneBeanOnly)
308 throws Exception {
309 JMXConnector jmxc = getJMXConnector(hostport, login, password);
310 Object [] result = null;
311 try {
312 result = doBeans(jmxc.getMBeanServerConnection(),
313 getObjectName(beanname), command, oneBeanOnly);
314 } finally {
315 jmxc.close();
316 }
317 return result;
318 }
319
320 protected boolean notEmpty(String s) {
321 return s != null && s.length() > 0;
322 }
323
324 protected Object [] doBeans(final MBeanServerConnection mbsc,
325 final ObjectName objName, final String[] command,
326 final boolean oneBeanOnly)
327 throws Exception {
328 Object [] result = null;
329 Set beans = mbsc.queryMBeans(objName, null);
330 if (beans.size() == 0) {
331
332 if (command.length == 1 && notEmpty(command[0])
333 && command[0].startsWith(CREATE_CMD_PREFIX)) {
334 String className =
335 command[0].substring(CREATE_CMD_PREFIX.length());
336 mbsc.createMBean(className, objName);
337 } else {
338
339
340 throw new RuntimeException(objName.getCanonicalName() +
341 " not registered.");
342 }
343 } else if (beans.size() == 1) {
344 result = doBean(mbsc, (ObjectInstance) beans.iterator().next(),
345 command);
346 } else {
347 if (oneBeanOnly) {
348 throw new RuntimeException("Only supposed to be one bean " +
349 "query result");
350 }
351
352
353
354 StringBuffer buffer = new StringBuffer();
355 for (Iterator i = beans.iterator(); i.hasNext();) {
356 Object obj = i.next();
357 if (obj instanceof ObjectName) {
358 buffer.append((((ObjectName) obj).getCanonicalName()));
359 } else if (obj instanceof ObjectInstance) {
360 buffer.append((((ObjectInstance) obj).getObjectName()
361 .getCanonicalName()));
362 } else {
363 throw new RuntimeException("Unexpected object type: " + obj);
364 }
365 buffer.append("\n");
366 }
367 result = new String [] {buffer.toString()};
368 }
369 return result;
370 }
371
372 /***
373 * Get attribute or run operation against passed bean <code>instance</code>.
374 *
375 * @param mbsc Server connection.
376 * @param instance Bean instance we're to get attributes from or run
377 * operation against.
378 * @param command Command to run (May be null).
379 * @return Result. If multiple commands, multiple results.
380 * @throws Exception
381 */
382 protected Object [] doBean(MBeanServerConnection mbsc,
383 ObjectInstance instance, String [] command)
384 throws Exception {
385
386 if (command == null || command.length <= 0) {
387 return new String [] {listOptions(mbsc, instance)};
388 }
389
390
391 Object [] result = new Object[command.length];
392 for (int i = 0; i < command.length; i++) {
393 result[i] = doSubCommand(mbsc, instance, command[i]);
394 }
395 return result;
396 }
397
398 public Object doSubCommand(MBeanServerConnection mbsc,
399 ObjectInstance instance, String subCommand)
400 throws Exception {
401
402 if (subCommand.equals("destroy")) {
403 mbsc.unregisterMBean(instance.getObjectName());
404 return null;
405 } else if (subCommand.startsWith(CREATE_CMD_PREFIX)) {
406 throw new IllegalArgumentException("You cannot call create " +
407 "on an already existing bean.");
408 }
409
410
411 MBeanAttributeInfo [] attributeInfo =
412 mbsc.getMBeanInfo(instance.getObjectName()).getAttributes();
413 MBeanOperationInfo [] operationInfo =
414 mbsc.getMBeanInfo(instance.getObjectName()).getOperations();
415
416
417
418
419 Object result = null;
420 if (Character.isUpperCase(subCommand.charAt(0))) {
421
422 if (!isFeatureInfo(attributeInfo, subCommand) &&
423 isFeatureInfo(operationInfo, subCommand)) {
424
425
426 result =
427 doBeanOperation(mbsc, instance, subCommand, operationInfo);
428 } else {
429
430
431
432 result = doAttributeOperation(mbsc, instance, subCommand,
433 attributeInfo);
434 }
435 } else {
436
437 if (!isFeatureInfo(operationInfo, subCommand) &&
438 isFeatureInfo(attributeInfo, subCommand)) {
439
440
441 result = doAttributeOperation(mbsc, instance, subCommand,
442 attributeInfo);
443 } else {
444
445
446 result =
447 doBeanOperation(mbsc, instance, subCommand, operationInfo);
448 }
449 }
450
451
452
453 if (result instanceof CompositeData) {
454 result = recurseCompositeData(new StringBuffer("\n"), "", "",
455 (CompositeData)result);
456 } else if (result instanceof TabularData) {
457 result = recurseTabularData(new StringBuffer("\n"), "", "",
458 (TabularData)result);
459 } else if (result instanceof String []) {
460 String [] strs = (String [])result;
461 StringBuffer buffer = new StringBuffer("\n");
462 for (int i = 0; i < strs.length; i++) {
463 buffer.append(strs[i]);
464 buffer.append("\n");
465 }
466 result = buffer;
467 } else if (result instanceof AttributeList) {
468 AttributeList list = (AttributeList)result;
469 if (list.size() <= 0) {
470 result = null;
471 } else {
472 StringBuffer buffer = new StringBuffer("\n");
473 for (Iterator ii = list.iterator(); ii.hasNext();) {
474 Attribute a = (Attribute)ii.next();
475 buffer.append(a.getName());
476 buffer.append(": ");
477 buffer.append(a.getValue());
478 buffer.append("\n");
479 }
480 result = buffer;
481 }
482 }
483 return result;
484 }
485
486 protected boolean isFeatureInfo(MBeanFeatureInfo [] infos, String cmd) {
487 return getFeatureInfo(infos, cmd) != null;
488 }
489
490 protected MBeanFeatureInfo getFeatureInfo(MBeanFeatureInfo [] infos,
491 String cmd) {
492
493 int index = cmd.indexOf('=');
494 String name = (index > 0)? cmd.substring(0, index): cmd;
495 for (int i = 0; i < infos.length; i++) {
496 if (infos[i].getName().equals(name)) {
497 return infos[i];
498 }
499 }
500 return null;
501 }
502
503 protected StringBuffer recurseTabularData(StringBuffer buffer,
504 String indent, String name, TabularData data) {
505 addNameToBuffer(buffer, indent, name);
506 java.util.Collection c = data.values();
507 for (Iterator i = c.iterator(); i.hasNext();) {
508 Object obj = i.next();
509 if (obj instanceof CompositeData) {
510 recurseCompositeData(buffer, indent + " ", "",
511 (CompositeData)obj);
512 } else if (obj instanceof TabularData) {
513 recurseTabularData(buffer, indent, "",
514 (TabularData)obj);
515 } else {
516 buffer.append(obj);
517 }
518 }
519 return buffer;
520 }
521
522 protected StringBuffer recurseCompositeData(StringBuffer buffer,
523 String indent, String name, CompositeData data) {
524 indent = addNameToBuffer(buffer, indent, name);
525 for (Iterator i = data.getCompositeType().keySet().iterator();
526 i.hasNext();) {
527 String key = (String)i.next();
528 Object o = data.get(key);
529 if (o instanceof CompositeData) {
530 recurseCompositeData(buffer, indent + " ", key,
531 (CompositeData)o);
532 } else if (o instanceof TabularData) {
533 recurseTabularData(buffer, indent, key, (TabularData)o);
534 } else {
535 buffer.append(indent);
536 buffer.append(key);
537 buffer.append(": ");
538 buffer.append(o);
539 buffer.append("\n");
540 }
541 }
542 return buffer;
543 }
544
545 protected String addNameToBuffer(StringBuffer buffer, String indent,
546 String name) {
547 if (name == null || name.length() == 0) {
548 return indent;
549 }
550 buffer.append(indent);
551 buffer.append(name);
552 buffer.append(":\n");
553
554 return indent + " ";
555 }
556
557 /***
558 * Class that parses commandline arguments.
559 * Expected format is 'operationName=arg0,arg1,arg2...'. We are assuming no
560 * spaces nor comma's in argument values.
561 */
562 protected class CommandParse {
563 private String cmd;
564 private String [] args;
565
566 protected CommandParse(String command) throws ParseException {
567 parse(command);
568 }
569
570 private void parse(String command) throws ParseException {
571 Matcher m = CMD_LINE_ARGS_PATTERN.matcher(command);
572 if (m == null || !m.matches()) {
573 throw new ParseException("Failed parse of " + command, 0);
574 }
575
576 this.cmd = m.group(1);
577 if (m.group(2) != null && m.group(2).length() > 0) {
578 this.args = m.group(2).split(",");
579 } else {
580 this.args = null;
581 }
582 }
583
584 protected String getCmd() {
585 return this.cmd;
586 }
587
588 protected String [] getArgs() {
589 return this.args;
590 }
591 }
592
593 protected Object doAttributeOperation(MBeanServerConnection mbsc,
594 ObjectInstance instance, String command, MBeanAttributeInfo [] infos)
595 throws Exception {
596
597
598 CommandParse parse = new CommandParse(command);
599 if (parse.getArgs() == null || parse.getArgs().length == 0) {
600
601
602 if (command.equals("Attributes")) {
603 String [] names = new String[infos.length];
604 for (int i = 0; i < infos.length; i++) {
605 names[i] = infos[i].getName();
606 }
607 return mbsc.getAttributes(instance.getObjectName(), names);
608 }
609 return mbsc.getAttribute(instance.getObjectName(), parse.getCmd());
610 }
611 if (parse.getArgs().length != 1) {
612 throw new IllegalArgumentException("One only argument setting " +
613 "attribute values: " + parse.getArgs());
614 }
615
616
617 MBeanAttributeInfo info =
618 (MBeanAttributeInfo)getFeatureInfo(infos, parse.getCmd());
619 java.lang.reflect.Constructor c = Class.forName(
620 info.getType()).getConstructor(new Class[] {String.class});
621 Attribute a = new Attribute(parse.getCmd(),
622 c.newInstance(new Object[] {parse.getArgs()[0]}));
623 mbsc.setAttribute(instance.getObjectName(), a);
624 return null;
625 }
626
627 protected Object doBeanOperation(MBeanServerConnection mbsc,
628 ObjectInstance instance, String command, MBeanOperationInfo [] infos)
629 throws Exception {
630
631 CommandParse parse = new CommandParse(command);
632
633
634
635
636 MBeanOperationInfo op =
637 (MBeanOperationInfo)getFeatureInfo(infos, parse.getCmd());
638 Object result = null;
639 if (op == null) {
640 result = "Operation " + parse.getCmd() + " not found.";
641 } else {
642 MBeanParameterInfo [] paraminfos = op.getSignature();
643 int paraminfosLength = (paraminfos == null)? 0: paraminfos.length;
644 int objsLength = (parse.getArgs() == null)?
645 0: parse.getArgs().length;
646 if (paraminfosLength != objsLength) {
647 result = "Passed param count does not match signature count";
648 } else {
649 String [] signature = new String[paraminfosLength];
650 Object [] params = (paraminfosLength == 0)? null
651 : new Object[paraminfosLength];
652 for (int i = 0; i < paraminfosLength; i++) {
653 MBeanParameterInfo paraminfo = paraminfos[i];
654 java.lang.reflect.Constructor c = Class.forName(
655 paraminfo.getType()).getConstructor(
656 new Class[] {String.class});
657 params[i] =
658 c.newInstance(new Object[] {parse.getArgs()[i]});
659 signature[i] = paraminfo.getType();
660 }
661 result = mbsc.invoke(instance.getObjectName(), parse.getCmd(),
662 params, signature);
663 }
664 }
665 return result;
666 }
667
668 protected String listOptions(MBeanServerConnection mbsc,
669 ObjectInstance instance)
670 throws InstanceNotFoundException, IntrospectionException,
671 ReflectionException, IOException {
672 StringBuffer result = new StringBuffer();
673 MBeanInfo info = mbsc.getMBeanInfo(instance.getObjectName());
674 MBeanAttributeInfo [] attributes = info.getAttributes();
675 if (attributes.length > 0) {
676 result.append("Attributes:");
677 result.append("\n");
678 for (int i = 0; i < attributes.length; i++) {
679 result.append(' ' + attributes[i].getName() +
680 ": " + attributes[i].getDescription() +
681 " (type=" + attributes[i].getType() +
682 ")");
683 result.append("\n");
684 }
685 }
686 MBeanOperationInfo [] operations = info.getOperations();
687 if (operations.length > 0) {
688 result.append("Operations:");
689 result.append("\n");
690 for (int i = 0; i < operations.length; i++) {
691 MBeanParameterInfo [] params = operations[i].getSignature();
692 StringBuffer paramsStrBuffer = new StringBuffer();
693 if (params != null) {
694 for (int j = 0; j < params.length; j++) {
695 paramsStrBuffer.append("\n name=");
696 paramsStrBuffer.append(params[j].getName());
697 paramsStrBuffer.append(" type=");
698 paramsStrBuffer.append(params[j].getType());
699 paramsStrBuffer.append(" ");
700 paramsStrBuffer.append(params[j].getDescription());
701 }
702 }
703 result.append(' ' + operations[i].getName() +
704 ": " + operations[i].getDescription() +
705 "\n Parameters " + params.length +
706 ", return type=" + operations[i].getReturnType() +
707 paramsStrBuffer.toString());
708 result.append("\n");
709 }
710 }
711 return result.toString();
712 }
713
714 /***
715 * Logger that writes entry on one line with less verbose date.
716 * Modelled on the OneLineSimpleLogger from Heritrix.
717 *
718 * @author stack
719 * @version $Revision: 1.15 $, $Date: 2005/10/03 20:07:45 $
720 */
721 private class OneLineSimpleLogger extends SimpleFormatter {
722 /***
723 * Date instance.
724 *
725 * Keep around instance of date.
726 */
727 private Date date = new Date();
728
729 /***
730 * Field position instance.
731 *
732 * Keep around this instance.
733 */
734 private FieldPosition position = new FieldPosition(0);
735
736 /***
737 * MessageFormatter for date.
738 */
739 private SimpleDateFormat formatter =
740 new SimpleDateFormat("MM/dd/yyyy HH:mm:ss Z");
741
742 /***
743 * Persistent buffer in which we conjure the log.
744 */
745 private StringBuffer buffer = new StringBuffer();
746
747
748 public OneLineSimpleLogger() {
749 super();
750 }
751
752 public synchronized String format(LogRecord record) {
753 this.buffer.setLength(0);
754 this.date.setTime(record.getMillis());
755 this.position.setBeginIndex(0);
756 this.formatter.format(this.date, this.buffer, this.position);
757 this.buffer.append(' ');
758 if (record.getSourceClassName() != null) {
759 this.buffer.append(record.getSourceClassName());
760 } else {
761 this.buffer.append(record.getLoggerName());
762 }
763 this.buffer.append(' ');
764 this.buffer.append(formatMessage(record));
765 this.buffer.append(System.getProperty("line.separator"));
766 if (record.getThrown() != null) {
767 try {
768 StringWriter writer = new StringWriter();
769 PrintWriter printer = new PrintWriter(writer);
770 record.getThrown().printStackTrace(printer);
771 writer.close();
772 this.buffer.append(writer.toString());
773 } catch (Exception e) {
774 this.buffer.append("Failed to get stack trace: " +
775 e.getMessage());
776 }
777 }
778 return this.buffer.toString();
779 }
780 }
781 }