使用定制的 Web Service Appender for Log4j 將日志信息發(fā)送到某一集中位置。Log4j Appender 使您可以在面向服務(wù)架構(gòu) (SOA) 解決方案中調(diào)試和跟蹤任意問(wèn)題。
引言
你
可以使用 Web Service Appender 將日志集中到某一位置,同時(shí),Web Service Appender
允許管理者監(jiān)控、開(kāi)發(fā)者調(diào)試面向服務(wù)架構(gòu)(SOA)環(huán)境里可能存在的任何問(wèn)題。Web Service Appender 是一種擴(kuò)展 JAVA
類(lèi),它由 Log4j 的 Appender 類(lèi)擴(kuò)展而來(lái)。
從定義上看,SOA
是一種彼此可以互相通信的服務(wù)集合,但這些服務(wù)的內(nèi)容是各自獨(dú)立的,每一類(lèi)服務(wù)均不受其它服務(wù)內(nèi)容或服務(wù)狀態(tài)的影響,并且這些服務(wù)都工作在分布式的系統(tǒng)架
構(gòu)里。在 SOA 中,Web 服務(wù)通常被用來(lái)在給定事務(wù)中處理請(qǐng)求,這些請(qǐng)求可以是遺留代碼、企業(yè)級(jí) Java Beans(EJBs)
的封裝,也可以是 Java
類(lèi)的封裝,使用一種可以將日志信息聚集在中心位置里的日志紀(jì)錄方法,能幫助您隔離缺陷和問(wèn)題,并能讓你更好的理解邏輯流的處理。
將特定模塊或服務(wù)的日志消息紀(jì)錄到一個(gè)中心位置的機(jī)制,可以把可能潛在的問(wèn)題和缺陷降低到最小。
本文對(duì) Log4j 的功能進(jìn)行了大體的概述,并介紹了如何編寫(xiě)自定義的 Log4j Appender,這類(lèi)特殊的 Appender 將日志消息編到一種特定的 Web 服務(wù)。
Log4j 快速入門(mén)
Log4j
是一種開(kāi)放源代碼的日志庫(kù),它已被發(fā)展為 Apache Software Foundation 日志服務(wù)項(xiàng)目的子項(xiàng)目。該庫(kù)是以 IBM 在 90
年代末開(kāi)發(fā)的日志庫(kù)為基礎(chǔ)的,第一版發(fā)布于 1999 年。現(xiàn)在它在開(kāi)放源代碼團(tuán)體得到了廣泛使用,它的體系是圍繞以下三個(gè)主要概念構(gòu)建起來(lái)的:
這些概念可以讓您根據(jù)消息類(lèi)型、消息優(yōu)先級(jí)來(lái)紀(jì)錄消息,您可以控制消息在何處結(jié)束及消息如何格式化。
Logger 是應(yīng)用程序首先調(diào)用以初始化消息紀(jì)錄的對(duì)象。當(dāng)把某一消息傳遞給日志時(shí),logger 會(huì)生成 LoggingEvent
,對(duì)消息進(jìn)行封裝。之后,Logger 對(duì)象將 LoggingEvent
傳遞給與之關(guān)聯(lián)的 Appender。
Appender 將 LoggingEvent
所包含的消息發(fā)送給指定的目標(biāo)輸出文件。所謂指定的文件,大多數(shù)情況下,是 Log4 屬性文件。一些 Appender 存在于 Log4j 中。您也可以擴(kuò)展 Appender,使之支持其它的目標(biāo)文件,比如 XML 文件、控制臺(tái)等等。
在 Log4j 里, LoggingEvent
被賦予某一級(jí)別,以表明它們的優(yōu)先級(jí)。缺省的級(jí)別包括如下幾種:
- OFF:可能是最高的級(jí)別,它是用來(lái)關(guān)閉日志紀(jì)錄的
- FATAL:指出現(xiàn)了非常嚴(yán)重的錯(cuò)誤事件,這些錯(cuò)誤可能會(huì)導(dǎo)致應(yīng)用程序異常中止
- ERROR:指雖有錯(cuò)誤,但仍允許應(yīng)用程序繼續(xù)運(yùn)行
- WARN:指運(yùn)行環(huán)境潛藏著危害
- INFO:指報(bào)告信息,這些信息在粗粒度級(jí)別上突出顯示應(yīng)用程序的進(jìn)程
- DEBUG:指細(xì)粒度信息事件,細(xì)粒度信息事件對(duì)于應(yīng)用程序的調(diào)試是最有用的
- ALL:可能是最低的級(jí)別,其目的是打開(kāi)所有日志記錄
Logger 和 Appender 也被賦予上述的某一級(jí)別,并且僅執(zhí)行等于或高于它們自身的級(jí)別的日志請(qǐng)求。比如,如果一個(gè) Appender 屬于 INFO 級(jí)別,而日志請(qǐng)求屬于 DEBUG,那么 Appender 將不會(huì)為給定的日志事件寫(xiě)消息。
客戶(hù)端組件
客戶(hù)端 log4j.properties 文件
客戶(hù)端 log4j.properties 文件是一種標(biāo)準(zhǔn)文件,它包含服務(wù)或模塊使用的所有 Appender。Web Service Appender 要求有一個(gè)端點(diǎn)(endpoint) 屬性以指定所使用的日志服務(wù)。
清單 1 描述了使用 WebServiceAppender
所必需的 Web 服務(wù)客戶(hù)端 Log4j 屬性。 黑體顯示的文本指明了將訪問(wèn) WebServiceAppender
服務(wù)器端的 Appender。屬性文件是使用 Log4j 的基本需求,它可以讓您配置應(yīng)用程序以使用多個(gè) Appender 以及 logging severity。一旦應(yīng)用程序進(jìn)入運(yùn)行狀態(tài)或潛在的問(wèn)題得到解決,您就可以輕松地修改屬性文件。
清單 1:客戶(hù)端 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
|
服務(wù)器的 Log4j.properties 文件
服
務(wù)器 Log4j.properties 文件被用來(lái)關(guān)聯(lián)客戶(hù)端 Log4j 屬性文件,它指定了日志的級(jí)別及服務(wù)器將如何輸出消息。對(duì)于支持
Log4j 的應(yīng)用程序,您可以定義多個(gè) appender。當(dāng)然,這些 appender 既可以用于客戶(hù)端服務(wù),也可以用于服務(wù)模塊。
清單 2 描述了一份典型的 Log4j 屬性文件,服務(wù)器端的 WebServiceAppender
使用缺省的 Log4j Appenders。服務(wù)器端的 Appender 可以潛在的調(diào)用另一個(gè) WebServiceAppender
,并將日志信息鏈接起來(lái):
清單 2:服務(wù)器端的 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
|
客戶(hù)端程序測(cè)試示例:
這個(gè)客戶(hù)端程序示例是無(wú)格式普通 Java 對(duì)象(POJO),它記錄了一條消息,并被配置為使用 Web Service Appender 來(lái)處理消息。清單 3 顯示了這個(gè)示例:
清單 3:客戶(hù)端應(yīng)用程序使用 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
是必需的,它可以將消息發(fā)送到指定的 Web 服務(wù)。WebServiceAppender
繼承了 org.log4j.Appender,它允許使用 log4.properties,并成為有效的 Log4j Appender。
WebServiceAppender
使用基于 XML 的遠(yuǎn)程過(guò)程調(diào)用 (JAX-RPC) 的 Java API,來(lái)將消息發(fā)送到服務(wù)器。JAX-RPC 是一種規(guī)范,它描述使用
RPC 和 XML 構(gòu)建 Web 服務(wù)和 Web 服務(wù)客戶(hù)端的應(yīng)用編程接口 (API) 和約定。JAX-RPC 又被稱(chēng)為 JSR 101。
LoggingEvent
通過(guò) SOAPElement
被分割并表示為 XML。javax.xml.soap.SOAPElement 接口意味著服務(wù)端點(diǎn)接口將包含一個(gè)參數(shù),或返回 javax.xml.soap.SOAPElement 類(lèi)型的值,以對(duì)應(yīng)于 schema 中每個(gè)使用<xsd:any/>
的地方。從本質(zhì)上看,它是 XML 參數(shù)的封裝,且沒(méi)有相應(yīng)的序列化/反序列化 JAVA 類(lèi)。例如,一旦客戶(hù)請(qǐng)求記錄一個(gè)消息,就會(huì)創(chuàng)建一個(gè) LoggEvent
對(duì)象,然后傳送給 Appender。在這種情況下,Appender 就是 WebServiceAppender
。Appender 檢索事件,并在解析事件中的信息。一些額外的信息會(huì)被加入,如主機(jī)名稱(chēng),這樣您就知道這些消息來(lái)自哪個(gè)系統(tǒng)。同時(shí),append 方法也將消息轉(zhuǎn)換為 SOAPElement
,這樣就可以通過(guò) executeWebService
方法將消息傳遞給 Web 服務(wù)。使用 SOAPElement
充分考慮了 WebServiceAppender
未來(lái)版本的可擴(kuò)展性問(wèn)題。
清單4:執(zhí)行 WebServiceAppender 服務(wù)的 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
沒(méi)有包含 Hostname,而 Hostname 是 Web Service Appender 眾多需求之一。在創(chuàng)建 SOAPElement
以前,您可以用下面的語(yǔ)句將 Hostname 添加到 XML 文件里:
String hostName = "<hostname>" + InetAddress.getLocalHost() + "</hostname>";
SoapElementFactory
SoapElementFactory
是主要用于創(chuàng)建 SOAPElement
的類(lèi)。它同時(shí)支持創(chuàng)建 IBM 和 Java 的 SOAPElement
實(shí)現(xiàn),如清單 5 所示:
清單 5:使用 SoapElementFactory 類(lèi)的創(chuàng)建方法 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
類(lèi)封裝了 Call
接口的 JAX-RPC 實(shí)現(xiàn),javax.xml.rpc.Call 接口提供了對(duì)服務(wù)端點(diǎn)動(dòng)態(tài)調(diào)用的支持。javax.xml.rpc.Service 接口就好象是創(chuàng)建 Call
實(shí)例的工廠。
清單 6 說(shuō)明了客戶(hù)端如何動(dòng)態(tài)調(diào)用服務(wù)。這允許對(duì)服務(wù)進(jìn)行變更,而無(wú)需生成客戶(hù)端代理來(lái)訪問(wèn)遠(yuǎn)程服務(wù)。
清單 6:使用 SoapClient 類(lèi)的調(diào)用方法 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); }
|

 |

|
服務(wù)組件
Log4j.server.properties
Log4j.server.properties 文件包含了一個(gè)基本的 Log4j 配置文件,該文件可以讓您指定把哪些日志發(fā)送給 Web 服務(wù)系統(tǒng)。
清單 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 服務(wù)所要使用的 EJB。該服務(wù)啟動(dòng) LogAppenderBean
以處理來(lái)自每個(gè) Web Service Appender 客戶(hù)端的每一個(gè)請(qǐng)求。
清單 8 顯示了來(lái)自 WebServiceAppender
EJB 的 log 方法,該方法解析來(lái)自客戶(hù)端的消息,并將客戶(hù)端信息紀(jì)錄到服務(wù)的服務(wù)器端。
清單 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("http://hostname").getText(); String threadName = document.selectSingleNode("http://thread_name").getText(); String syslog = document.selectSingleNode("http://syslog").getText(); String startTime = new Long( document.selectSingleNode("http://start_time"). getText()).toString(); log = '[' + startTime + ':' + hostname + ':' + threadName + "] " + document.selectSingleNode( "http://message").getText(); // retrieve any throwable messages List throwableList = document.selectNodes( "http://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(); } }
|
通過(guò) IBM SOAPElement
的 InputSource,每一個(gè) SOAPElement
的內(nèi)容都會(huì)被檢索。目前,只有 IBM WebSphere? Application Server (Application Server) 支持這些代碼(請(qǐng)參閱參考資料)。 然而,如果您移除 IBM SOAPElement
,那么您就可以在任何應(yīng)用服務(wù)器上使用這些代碼。IBM
SOAPElement
內(nèi)置的性能優(yōu)化也適用于 Application Server。
每一個(gè) SOAPElement
都使用 Dom4j 來(lái)讀取、解析和轉(zhuǎn)換。Dom4j 是一種在內(nèi)存中表示 XML 樹(shù)的對(duì)象模型。Dom4j 提供了一組易于使用的
API,從而為我們提供了一整套強(qiáng)大的功能來(lái)處理、操作或定位 XML,使用 XPath 和 XSLT 進(jìn)行工作,以及與 SAX、
JAXP、DOM 集成。
除了可以使用任意的 XML 解析器外,DOM4J 還允許使用任意的 SAX 解析器,為實(shí)現(xiàn)更好的性能,還允許使用所有標(biāo)準(zhǔn)的 XSLT 轉(zhuǎn)換器。
轉(zhuǎn)換被用來(lái)析取發(fā)送給 Web Service Appender 的客戶(hù)端 LoggingEvent
的元素。
如果您允許使用 SOAPElement
,那么就需要在代碼中維持最大限度的靈活性。Web Service Appender 服務(wù)可以被修改,以支持所有發(fā)送給服務(wù)的 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 是機(jī)器名和 IP 地址,而 main 是日志所在處的方法名。
結(jié)束語(yǔ)
Web
Service Appender 是將日志集中到某一位置的基本工具。由于 Web Service Appender 是 Log4j 的
Appender 類(lèi)的子集,因而配置和使用 Appender 都非常簡(jiǎn)單易懂。您可以修改 Log4j 的屬性文件,這樣,使用 Log4j
的現(xiàn)有應(yīng)用程序和服務(wù)就可以馬上使用 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 |
---|