使用定制的 Web Service Appender for Log4j 将日志信息发送到某一集中位置。Log4j Appender 使您可以在面向服务架构 (SOA) 解决方案中调试和跟踪任意问题。
引言
你
可以使用 Web Service Appender 将日志集中到某一位置,同时,Web Service Appender
允许管理者监控、开发者调试面向服务架构(SOA)环境里可能存在的任何问题。Web Service Appender 是一种扩展 JAVA
类,它由 Log4j 的 Appender 类扩展而来。
从定义上看,SOA
是一种彼此可以互相通信的服务集合,但这些服务的内容是各自独立的,每一类服务均不受其它服务内容或服务状态的影响,并且这些服务都工作在分布式的系统架
构里。在 SOA 中,Web 服务通常被用来在给定事务中处理请求,这些请求可以是遗留代码、企业级 Java Beans(EJBs)
的封装,也可以是 Java
类的封装,使用一种可以将日志信息聚集在中心位置里的日志纪录方法,能帮助您隔离缺陷和问题,并能让你更好的理解逻辑流的处理。
将特定模块或服务的日志消息纪录到一个中心位置的机制,可以把可能潜在的问题和缺陷降低到最小。
本文对 Log4j 的功能进行了大体的概述,并介绍了如何编写自定义的 Log4j Appender,这类特殊的 Appender 将日志消息编到一种特定的 Web 服务。
Log4j 快速入门
Log4j
是一种开放源代码的日志库,它已被发展为 Apache Software Foundation 日志服务项目的子项目。该库是以 IBM 在 90
年代末开发的日志库为基础的,第一版发布于 1999 年。现在它在开放源代码团体得到了广泛使用,它的体系是围绕以下三个主要概念构建起来的:
这些概念可以让您根据消息类型、消息优先级来纪录消息,您可以控制消息在何处结束及消息如何格式化。
Logger 是应用程序首先调用以初始化消息纪录的对象。当把某一消息传递给日志时,logger 会生成 LoggingEvent
,对消息进行封装。之后,Logger 对象将 LoggingEvent
传递给与之关联的 Appender。
Appender 将 LoggingEvent
所包含的消息发送给指定的目标输出文件。所谓指定的文件,大多数情况下,是 Log4 属性文件。一些 Appender 存在于 Log4j 中。您也可以扩展 Appender,使之支持其它的目标文件,比如 XML 文件、控制台等等。
在 Log4j 里, LoggingEvent
被赋予某一级别,以表明它们的优先级。缺省的级别包括如下几种:
- OFF:可能是最高的级别,它是用来关闭日志纪录的
- FATAL:指出现了非常严重的错误事件,这些错误可能会导致应用程序异常中止
- ERROR:指虽有错误,但仍允许应用程序继续运行
- WARN:指运行环境潜藏着危害
- INFO:指报告信息,这些信息在粗粒度级别上突出显示应用程序的进程
- DEBUG:指细粒度信息事件,细粒度信息事件对于应用程序的调试是最有用的
- ALL:可能是最低的级别,其目的是打开所有日志记录
Logger 和 Appender 也被赋予上述的某一级别,并且仅执行等于或高于它们自身的级别的日志请求。比如,如果一个 Appender 属于 INFO 级别,而日志请求属于 DEBUG,那么 Appender 将不会为给定的日志事件写消息。
客户端组件
客户端 log4j.properties 文件
客户端 log4j.properties 文件是一种标准文件,它包含服务或模块使用的所有 Appender。Web Service Appender 要求有一个端点(endpoint) 属性以指定所使用的日志服务。
清单 1 描述了使用 WebServiceAppender
所必需的 Web 服务客户端 Log4j 属性。 黑体显示的文本指明了将访问 WebServiceAppender
服务器端的 Appender。属性文件是使用 Log4j 的基本需求,它可以让您配置应用程序以使用多个 Appender 以及 logging severity。一旦应用程序进入运行状态或潜在的问题得到解决,您就可以轻松地修改属性文件。
清单 1:客户端 Log4j 的属性文件 #set the level of the root logger log4j.rootLogger = INFO, CONSOLE #set own logger log4j.logger.com.carmelouria.logging.test=CONSOLE log4j.appender.CONSOLE=com.carmelouria.logging.WebServiceAppender log4j.appender.CONSOLE.endpoint= http://localhost:9080/log4j/services/LogAppenderService log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n
|
服务器的 Log4j.properties 文件
服
务器 Log4j.properties 文件被用来关联客户端 Log4j 属性文件,它指定了日志的级别及服务器将如何输出消息。对于支持
Log4j 的应用程序,您可以定义多个 appender。当然,这些 appender 既可以用于客户端服务,也可以用于服务模块。
清单 2 描述了一份典型的 Log4j 属性文件,服务器端的 WebServiceAppender
使用缺省的 Log4j Appenders。服务器端的 Appender 可以潜在的调用另一个 WebServiceAppender
,并将日志信息链接起来:
清单 2:服务器端的 Log4j 属性文件 #set the level of the root logger log4j.rootLogger = INFO, FILE #set own logger log4j.appender.FILE=org.apache.log4j.RollingFileAppender log4j.appender.FILE.file=c:/temp/log4j/server/server.log log4j.appender.FILE.layout=org.apache.log4j.PatternLayout log4j.appender.FILE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n
|
客户端程序测试示例:
这个客户端程序示例是无格式普通 Java 对象(POJO),它记录了一条消息,并被配置为使用 Web Service Appender 来处理消息。清单 3 显示了这个示例:
清单 3:客户端应用程序使用 WebServiceAppender 的示例 package com.carmelouria.logging.test; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PropertyConfigurator; /** * @author Carmelo Uria * */ public class LoggingSample { private static Logger logger = Logger.getLogger(LoggingSample.class.getName()); /** * */ public LoggingSample() { super(); PropertyConfigurator.configure("c:/temp/log4j.properties"); logger.log(Level.INFO, "LoggingSample instantiation..."); System.out.println("finished..."); } public static void main(String[] args) { LoggingSample sample = new LoggingSample(); } }
|
WebServiceAppender
WebServiceAppender
是必需的,它可以将消息发送到指定的 Web 服务。WebServiceAppender
继承了 org.log4j.Appender,它允许使用 log4.properties,并成为有效的 Log4j Appender。
WebServiceAppender
使用基于 XML 的远程过程调用 (JAX-RPC) 的 Java API,来将消息发送到服务器。JAX-RPC 是一种规范,它描述使用
RPC 和 XML 构建 Web 服务和 Web 服务客户端的应用编程接口 (API) 和约定。JAX-RPC 又被称为 JSR 101。
LoggingEvent
通过 SOAPElement
被分割并表示为 XML。javax.xml.soap.SOAPElement 接口意味着服务端点接口将包含一个参数,或返回 javax.xml.soap.SOAPElement 类型的值,以对应于 schema 中每个使用<xsd:any/>
的地方。从本质上看,它是 XML 参数的封装,且没有相应的序列化/反序列化 JAVA 类。例如,一旦客户请求记录一个消息,就会创建一个 LoggEvent
对象,然后传送给 Appender。在这种情况下,Appender 就是 WebServiceAppender
。Appender 检索事件,并在解析事件中的信息。一些额外的信息会被加入,如主机名称,这样您就知道这些消息来自哪个系统。同时,append 方法也将消息转换为 SOAPElement
,这样就可以通过 executeWebService
方法将消息传递给 Web 服务。使用 SOAPElement
充分考虑了 WebServiceAppender
未来版本的可扩展性问题。
清单4:执行 WebServiceAppender 服务的 Append 方法 protected void append(LoggingEvent event) { // create Web Service client using endpoint if (endpoint == null) { System.out.println("no endpoint set. Check configuration file"); System.out.println("[" + hostname + "] " + this.layout.format(event)); return; } executeWebService(event); } private void executeWebService(LoggingEvent event) { SoapClient client = new SoapClient(); URL endPoint = null; try { endPoint = new URL(getendpoint()); } catch (MalformedURLException e1) { e1.printStackTrace(); } String nameSpace = "http://ejb.logging.carmelouria.com"; QName serviceName = new QName(nameSpace, "LogAppenderServiceService"); QName operation = new QName(nameSpace, "log"); QName port = new QName(nameSpace, "LogAppenderService"); Parameter message = new Parameter("log", Constants.XSD_ANY, SOAPElement.class, ParameterMode.IN); try { /** *create SOAPElement from LoggingEvent need hostname */ Level level = event.getLevel(); String sysLog = "<syslog>" + new Integer(level.getSyslogEquivalent()).toString() + "</syslog>"; String startTime = new Long(LoggingEvent.getStartTime()).toString(); String timeTag = "<start_time>" + startTime + "</start_time>"; String hostName = "<hostname>" + InetAddress.getLocalHost() + "</hostname>"; String threadName = "<thread_name>" + event.getThreadName() +"</thread_name>"; String logger = "<logger>" + event.getLoggerName() + "</logger>"; String eventMessage = "<message>" + event.getRenderedMessage() + "</message>"; String log = hostName + threadName + logger + timeTag + sysLog + eventMessage; String throwableInformation[] = event.getThrowableStrRep(); if (throwableInformation != null) { for (int i = 0; i < throwableInformation.length; i++) { String throwable = "<throwable_information>" + throwableInformation[i] + "</throwable_information>"; log += throwable; } } String ndcString = event.getNDC(); if (throwableInformation != null) { String throwable = <ndc>" + ndcString + </ndc>"; log += throwable; } message.setValue(SOAPElementFactory.create(<log>" + log + </log>")); } catch (UnknownHostException unknownHostException) { unknownHostException.printStackTrace(); } catch (SOAPException e2) { e2.printStackTrace(); } Parameter resultType = newParameter("logResponse", Constants.WEBSERVICES_VOID, Object.class, ParameterMode.OUT); Parameter[] parameters = { message }; try { // execute client Object result = client.execute(endPoint, serviceName, operation, "wrapped", null, port, resultType, parameters); if ((result != null) && (result instanceof String)) System.out.println((String) result); } catch (ClientException e) { e.printStackTrace(); } }
|
Hostname
不幸的是,Log4j 的 LoggingEvent
没有包含 Hostname,而 Hostname 是 Web Service Appender 众多需求之一。在创建 SOAPElement
以前,您可以用下面的语句将 Hostname 添加到 XML 文件里:
String hostName = "<hostname>" + InetAddress.getLocalHost() + "</hostname>";
SoapElementFactory
SoapElementFactory
是主要用于创建 SOAPElement
的类。它同时支持创建 IBM 和 Java 的 SOAPElement
实现,如清单 5 所示:
清单 5:使用 SoapElementFactory 类的创建方法 public static javax.xml.soap.SOAPElement create(String xml) throws SOAPException { com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory factory = (com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory) com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory .newInstance(); SOAPElement element = (javax.xml.soap.SOAPElement)factory.createElementFromXMLString(xml); return(element); } public static SOAPElement create(String arg0, String arg1, String arg2, boolean ibmSoapElement) throws SOAPException { if (ibmSoapElement) { SOAPFactory soapFactory = (com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory) com.ibm.ws.webservices.engine.xmlsoap.SOAPFactory.newInstance(); return (soapFactory.createSOAPElement(arg0, arg1)); } javax.xml.soap.SOAPFactory soapFactory = javax.xml.soap.SOAPFactory.newInstance(); return (soapFactory.createElement(arg0, arg1, arg2)); }
|
SoapClient
SoapClient
类封装了 Call
接口的 JAX-RPC 实现,javax.xml.rpc.Call 接口提供了对服务端点动态调用的支持。javax.xml.rpc.Service 接口就好象是创建 Call
实例的工厂。
清单 6 说明了客户端如何动态调用服务。这允许对服务进行变更,而无需生成客户端代理来访问远程服务。
清单 6:使用 SoapClient 类的调用方法 private Object call(SoapService service, QName operation, QName portType, String operationStyleProperty, String encodingURIProperty, Parameter returnType, Parameter[] parameters) throws ClientException { QName portName; String response = null; Object results = null; Call call = null; try { // check to see if Service object exists if (service == null) throw new ClientException("Invalid Service object. It maybe null."); // retrieve call from Service object call = service.createCall(); call.setOperationName(operation); call.setPortTypeName(portType); // check call object if (call == null) throw new ClientException("invalid operation. Call object is null."); // set default values if (operationStyleProperty == null) call.setProperty(Call.OPERATION_STYLE_PROPERTY, OPERATION_STYLE_DOCUMENT_TYPE); else call.setProperty(Call.OPERATION_STYLE_PROPERTY, operationStyleProperty); if (encodingURIProperty == null) call.setProperty(Call.ENCODINGSTYLE_URI_PROPERTY, ENCODING_LITERAL); else call.setProperty(Call.ENCODINGSTYLE_URI_PROPERTY, encodingURIProperty); call.setTargetEndpointAddress(service.getServiceEndPoint()); //create Parameter class for SoapClient for (int i = 0; i < parameters.length; i++) { Class classObject = parameters[i].getClassObject(); if (classObject != null) call.addParameter(parameters[i].getName(), parameters[i].getXmlType(), parameters[i].getClassObject(), parameters[i].getMode()); else call.addParameter(parameters[i].getName(), parameters[i].getXmlType(), parameters[i].getMode()); } // pass parameter as ReturnType if (returnType != null) { if (returnType.getClassObject() != null) call.setReturnType(returnType.getXmlType(), returnType.getClassObject()); else call.setReturnType(returnType.getXmlType()); } Object[] request = new Object[parameters.length]; // add parameter values for (int i = 0; i < request.length; i++) { request[i] = parameters[i].getValue(); } results = call.invoke(request); } catch (SOAPFaultException e) { System.out.println(e.getFaultString()); e.getStackTrace(); throw new ClientException(e.getLocalizedMessage(), e); } catch (ServiceException serviceException) { serviceException.getStackTrace(); throw new ClientException(serviceException.getLocalizedMessage(), serviceException); } catch (RemoteException exception) { exception.printStackTrace(); throw new ClientException(exception.getLocalizedMessage(), exception); } return (results); }
|
|
|
服务组件
Log4j.server.properties
Log4j.server.properties 文件包含了一个基本的 Log4j 配置文件,该文件可以让您指定把哪些日志发送给 Web 服务系统。
清单 7:Log4j.server.properties 文件 #set the level of the root logger log4j.rootLogger = INFO, FILE #set own logger log4j.appender.FILE=org.apache.log4j.RollingFileAppender log4j.appender.FILE.file=c:/temp/log4j/server/server.log log4j.appender.FILE.layout=org.apache.log4j.PatternLayout log4j.appender.FILE.layout.ConversionPattern=%p [%t] %c{2} (%M:%L) :: %m%n
|
LogAppenderBean.java
LogAppenderBean.java 是 Web Service Appender 服务所要使用的 EJB。该服务启动 LogAppenderBean
以处理来自每个 Web Service Appender 客户端的每一个请求。
清单 8 显示了来自 WebServiceAppender
EJB 的 log 方法,该方法解析来自客户端的消息,并将客户端信息纪录到服务的服务器端。
清单 8:LogAppenderBean 的 log 方法 public void log(SOAPElement message) { try { InputSource source = ((IBMSOAPElement) message).toInputSource(false); Document document = Parser.parse(source); String log = null; String hostname = document.selectSingleNode("//hostname").getText(); String threadName = document.selectSingleNode("//thread_name").getText(); String syslog = document.selectSingleNode("//syslog").getText(); String startTime = new Long( document.selectSingleNode("//start_time"). getText()).toString(); log = '[' + startTime + ':' + hostname + ':' + threadName + "] " + document.selectSingleNode( "//message").getText(); // retrieve any throwable messages List throwableList = document.selectNodes( "//throwable_information"); if(throwableList != null) { Iterator throwables = throwableList.iterator(); while(throwables.hasNext()) { log += '\n' + ((Node)throwables.next()).getText(); }
log += '\n'; }
logger.log(Level.toLevel(new Integer(syslog).intValue()), log); logger.log(Level.INFO,log); } catch(ParserException parseException) { parseException.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } }
|
通过 IBM SOAPElement
的 InputSource,每一个 SOAPElement
的内容都会被检索。目前,只有 IBM WebSphere® Application Server (Application Server) 支持这些代码(请参阅参考资料)。 然而,如果您移除 IBM SOAPElement
,那么您就可以在任何应用服务器上使用这些代码。IBM
SOAPElement
内置的性能优化也适用于 Application Server。
每一个 SOAPElement
都使用 Dom4j 来读取、解析和转换。Dom4j 是一种在内存中表示 XML 树的对象模型。Dom4j 提供了一组易于使用的
API,从而为我们提供了一整套强大的功能来处理、操作或定位 XML,使用 XPath 和 XSLT 进行工作,以及与 SAX、
JAXP、DOM 集成。
除了可以使用任意的 XML 解析器外,DOM4J 还允许使用任意的 SAX 解析器,为实现更好的性能,还允许使用所有标准的 XSLT 转换器。
转换被用来析取发送给 Web Service Appender 的客户端 LoggingEvent
的元素。
如果您允许使用 SOAPElement
,那么就需要在代码中维持最大限度的灵活性。Web Service Appender 服务可以被修改,以支持所有发送给服务的 XML。
输出
下面的示例展示了 Web Service Appender 的可能的输出:
INFO [WebContainer : 0] ejb.LogAppenderBean (log:?) :: [1111513482641:OO7-64BIT/9.48.114.183:main]LoggingSample instantiation...
OO7-64BIT/9.48.114.183 是机器名和 IP 地址,而 main 是日志所在处的方法名。
结束语
Web
Service Appender 是将日志集中到某一位置的基本工具。由于 Web Service Appender 是 Log4j 的
Appender 类的子集,因而配置和使用 Appender 都非常简单易懂。您可以修改 Log4j 的属性文件,这样,使用 Log4j
的现有应用程序和服务就可以马上使用 Web Service Appender。
下载
描述 | 名字 | 大小 | 下载方法 |
---|
Foundation Class Library | foundation.zip | 47 KB | HTTP |
---|
Logging Web Service J2EE Application | LoggingWebService.ear | 1976 KB | HTTP |
---|
Unit Test Sample Code | SoapClientTest.java | 5 KB | HTTP |
---|