Appender of Log4j [5]

和网络有关的Appender有:

  • JDBCAppender
  • TelnetAppender
  • JMSAppender
  • SMTPAppender
  • SocketAppender
  • SocketHubAppender
  • SyslogAppender

org.apache.log4j.net包下主要是一些网络相关的类:



netetc

12.SocketAppender

本部分介绍SocketAppender,SocketAppender提供了将LoggingEvent发送到一个远程日志服务器上的功能,这里SocketAppender作为socket编程的客户端存在,日志服务器作为server socker监听请求。

设想一种情况:远程服务使用Linux环境部署(无GUI)、配置SocketAppender将日志消息发送到远程日志服务器(有图形界面),在日志服务器处配置其他Appender如LF5Appender,通过LF5Appender进行日志可视化监控、检索等。SocketAppender提供了一种日志发送机制,使用TCP协议而不是应用层协议,这是和SyslogAppender(使用syslog协议)的一个区别。

log4j默认提供了一个日志服务器实现,即和SocketAppender相同包下的org.apache.log4j.net.SocketNode。SocketNode作为日志服务是非入侵的,即SocketNode仅仅对LoggingEvent到来进行监听,对于收到的LoggingEvent并不会修改其时间(日志输出的是应用发生日志的时间,而非SocketNode收到LoggingEvent的时间)、NDC、位置信息等,具体和应用在本地日志输出内容一致。

SocketAppender并不依赖Layout序列化LoggingEvent,即requiresLayout返回false,SocketAppender是通过ObjectOutputStream的writeObject方法将LoggingEvent序列化发送到日志服务器的。LoggingEvent和日志服务器之间的网络交互使用TCP协议,在运行中,如果发生网络问题导致连接中断,SocketAppender中的LoggingEvent会简单丢弃处理。在网络恢复后,SocketAppender还能继续向日志服务器发送LoggingEvent,会有后台线程Connector轮询日志服务器的可达性并更新相关状态位的。

注意:SocketAppender由于需要使用TCP将数据发送到远程日志服务器,会受到网络带宽的影响。如果日志产生速度小于网络带宽能力,则服务不会受到影响;如果日志产生速度大于网络带宽登录,则日志消息仅能以网络带宽速度发送;如果极端情况日志服务器网络不可达,则会导致客户端程序阻塞block住;如果日志服务器网络可达,但是日志服务器进程或线程不活动,则客户端SocketAppender不会被阻塞掉,但是发送到日志服务器的LoggingEvent由于没有接收者持久化,会丢失掉。

对于SocketAppender建议显式的调用其close或shutdown方法关闭相关资源,仅仅关闭应用进程可能导致部分日志数据还未发送到日志服务导致丢失。日志服务器的默认端口是4560,日志服务器断开重连间隔默认30秒(30000毫秒)。

12.1额外属性

SocketAppender在AppenderSkeleton基础上增加的配置属性有:

  • AdvertiseViaMulticastDNS mDNS即组播DNS(multicast DNS)在一个没有常规DNS服务器的小型网络内,可以使用mDNS来实现类似DNS的编程接口、包格式和操作语义。组播DNS是IETF零配置网络(zeroconf)的参与者和DNS扩展(dnsext)工作组共同努力的结果。Zeroconf工作组提出了需求,DNSEXT组受到特许进行细节实现。如果为true,使用下面代码激活配置:
  public void activateOptions() {
    if (advertiseViaMulticastDNS) {
      zeroConf = new ZeroConfSupport(ZONE, port, getName());
      zeroConf.advertise();
    }
    connect(address, port);
    // 本质上是这个Socket编程
    // oos = new ObjectOutputStream(new Socket(address, port).getOutputStream());
  }
  • RemoteHost 远程日志服务器ip地址或者主机名
  • Port 远程日志服务器进程监听端口
  • LocationInfo 默认false,表示将日志发往日志服务器时,是否携带代码位置信息
  • Application 应用名称,用于区分不同应用的日志,设想下有10个应用忘一个日志服务器发送LoggingEvent的情况
  • ReconnectionDelay 后台线程Connector轮询日志服务可达性间隔,SocketAppender在连接远程日志服务器失败时,会尝试轮询重连

12.2append实现

append函数主要逻辑如下,主要区别是使用oos.writeObject(event);序列化将日志对象通过Socket流发送到日志服务器。

  public void append(LoggingEvent event) {
    if(event == null)
      return;
    if(address==null) {
      errorHandler.error("No remote host is set for SocketAppender named \"" + this.name + "\".");
      return;
    }
    if(oos != null) {
      try {
        if(locationInfo) {
          event.getLocationInformation();
        }
        if (application != null) {
          event.setProperty("application", application);
        }
        event.getNDC();
        event.getThreadName();
        event.getMDCCopy();
        event.getRenderedMessage();
        event.getThrowableStrRep();
    
        oos.writeObject(event);
        //LogLog.debug("=========Flushing.");
        oos.flush();
        if(++counter >= RESET_FREQUENCY) {
          counter = 0;
          // Failing to reset the object output stream every now and
          // then creates a serious memory leak.
          //System.err.println("Doing oos.reset()");
          oos.reset();
        }
      } catch(IOException e) {
        if (e instanceof InterruptedIOException) {
          Thread.currentThread().interrupt();
        }
        oos = null;
        LogLog.warn("Detected problem with connection: "+e);
        if(reconnectionDelay > 0) {
          fireConnector();
        } else {
          errorHandler.error("Detected problem with connection, not reconnecting.", e, ErrorCode.GENERIC_FAILURE);
        }
      }
    }
  }

12.3日志服务器的实现

SocketNode是日志服务器LoggingEvent接收的主要逻辑,SocketNode对于收到的LoggingEvent会根据日志服务器端配置的log4j配置文件进行日志处理(正如普通本地产生的日志一样),也可以发送到另一个远程日志服务器。在SocketNode基础上,log4j进行封装提供了2个直接可以使用的日志服务器类SimpleSocketServer、SocketServer。

SocketNode实现了Runnable接口,核心代码见下面while循环体:

  public void run() {
    LoggingEvent event;
    Logger remoteLogger;
    try {
      if (ois != null) {
        while(true) {
          // read an event from the wire
          event = (LoggingEvent) ois.readObject();
          // get a logger from the hierarchy. The name of the logger is taken to be the name contained in the event.
          remoteLogger = hierarchy.getLogger(event.getLoggerName());
          //event.logger = remoteLogger;
          // apply the logger-level filter
          if(event.getLevel().isGreaterOrEqual(
            remoteLogger.getEffectiveLevel())) {
            // finally log the event as if was generated locally
            remoteLogger.callAppenders(event);
          }
        }
      }
    } catch(java.io.EOFException e) {
      logger.info("Caught java.io.EOFException closing conneciton.");
    } catch(java.net.SocketException e) {
      logger.info("Caught java.net.SocketException closing conneciton.");
    } catch(InterruptedIOException e) {
      Thread.currentThread().interrupt();
      logger.info("Caught java.io.InterruptedIOException: "+e);
      logger.info("Closing connection.");
    } catch(IOException e) {
      logger.info("Caught java.io.IOException: "+e);
      logger.info("Closing connection.");
    } catch(Exception e) {
      logger.error("Unexpected exception. Closing conneciton.", e);
    } finally {
      if (ois != null) {
        try {
          ois.close();
        } catch(Exception e) {
          logger.info("Could not close connection.", e);
        }
      }
      if (socket != null) {
        try {
          socket.close();
        } catch(InterruptedIOException e) {
          Thread.currentThread().interrupt();
        } catch(IOException ex) {
        }
      }
    }
  }

12.3.1SimpleSocketServer

SimpleSocketServer是log4j提供的简单日志服务器封装,启动ServerSocket监听端口,对于客户端的连接创建一个SocketNode线程处理。SimpleSocketServer的启动方式是:

java org.apache.log4j.net.SimpleSocketServer port configfile

后跟监听端口和log4j配置文件(支持xml和.properties2种配置文件格式)路径。
核心逻辑代码片段是:

          
    // 根据配置后缀使用不同工具类解析配置
    if(configFile.endsWith(".xml")) {
      DOMConfigurator.configure(configFile);
    } else {
      PropertyConfigurator.configure(configFile);
    }

    // 监听服务处理
    try {
      cat.info("Listening on port " + port);
      ServerSocket serverSocket = new ServerSocket(port);
      while(true) {
        cat.info("Waiting to accept a new client.");
        Socket socket = serverSocket.accept();
        cat.info("Connected to client at " + socket.getInetAddress());
        cat.info("Starting new socket node.");
        new Thread(new SocketNode(socket,
        LogManager.getLoggerRepository()),"SimpleSocketServer-" + port).start();
      }
    } catch(Exception e) {
      e.printStackTrace();
    }

12.3.2SocketServer

SocketServer提供了更加可控的日志服务器功能,SimpleSocketServer对于多个客户端,服务端使用相同的日志接受处理策略,这里SocketServer可以根据客户端区别使用不同的服务端log4j配置文件,从而达到更强的定制性。SocketServer启动命令为:

java org.apache.log4j.net.SocketServer port configFile configDir

其中port表示日志服务器监听端口,configFile表示SocketServer的默认配置文件,configDir表示针对客户端的每一个具体配置文件。SocketServer的特点是每当一个客户端Socket建立后,根据客户端的ip地址如/127.0.0.1在configDir寻找针对此客户端的log4j配置文件,如127.0.0.1.lcf,如果找不到则使用generic.lcf。如果generic.lcf仍然不存在则使用默认的SocketServer配置文件configFile。如果一台机器上部署了2个应用,此2个应用都向同一个SocketServer汇报日志,则SocketServer会出现2个应用混合日志打印。

SocketServer是log4j提供的一个默认实现,做示例展示用途,里面发现了一些bug,如下面根据ip地址获取配置文件即有问题,这个只能截取空字符串而非ip地址。

SocketServer

12.4demo

  • demo log4j config:
log4j.rootLogger=INFO,socket
log4j.appender.socket=org.apache.log4j.net.SocketAppender
log4j.appender.socket.AdvertiseViaMulticastDNS=false
log4j.appender.socket.RemoteHost=127.0.0.1
log4j.appender.socket.Port=4560
log4j.appender.socket.LocationInfo=true
log4j.appender.socket.Application=luohw
log4j.appender.socket.ReconnectionDelay=30000
  • 日志服务器使用SimpleSocketServer,启动方式是:
java org.apache.log4j.net.SimpleSocketServer 4560 log4jRemote.properties
  • 远程日志服务器配置文件为(这里使用ConsoleAppender):
log4j.rootLogger=INFO,writer
log4j.appender.writer=org.apache.log4j.ConsoleAppender
log4j.appender.writer.Target=System.out
log4j.appender.writer.ImmediateFlush=true
log4j.appender.writer.Encoding=utf8
log4j.appender.writer.Threshold=debug
log4j.appender.writer.layout=org.apache.log4j.PatternLayout
log4j.appender.writer.layout.ConversionPattern=%t %m%n
log4j.appender.writer.name=writerdemo
  • 日志效果,上面是日志服务打印的消息,后面红框中是日志服务接收到的LoggingEvent:

SocketAppender

13.SocketHubAppender

本部分介绍SocketHubAppender,SocketHubAppender提供了将LoggingEvent发送到一个远程日志监听者的功能,SocketHubAppender作为socket编程的服务端存在,日志监听者作为client socket。注意SocketHubAppender和SocketAppender在Socket编程中客户端、服务端的角色区别。另外,SocketHubAppender有些类似TelnetAppender(使用telnet协议),不过SocketHubAppender依赖的是TCP协议而非具体telnet应用层协议。

在AppenderSkeleton基础上,SocketHubAppender增加的一些配置属性有:

  • Port SocketHubAppender作为服务端ServerSocket监听的端口
  • Application 应用服务名,可用于区分不同应用日志
  • BufferSize 日志缓冲区大小
  • LocationInfo 发送给日志监听客户端是否包括日志代码位置信息
  • AdvertiseViaMulticastDNS mDNS即组播DNS(multicast DNS)在一个没有常规DNS服务器的小型网络内,可以使用mDNS来实现类似DNS的编程接口、包格式和操作语义。见SocketAppender部分介绍

13.1实现细节

SocketHubAppender的配置激活activateOptions中,会初始化监听服务,如下:

  
  public void activateOptions() {
    if (advertiseViaMulticastDNS) {
      zeroConf = new ZeroConfSupport(ZONE, port, getName());
      zeroConf.advertise();
    }
    startServer();
  }

  private void startServer() {
    serverMonitor = new ServerMonitor(port, oosList);
  }

ServerMonitor是log4j提供的日志服务器实现,其实现了Runnable接口并作为一个后台线程运行。ServerMonitor会维护好所有的客户端连接请求的数据读写流,对于触发的日志LoggingEvent会通过ObjectOutputStream的writeObject方法序列化发送到远程。

append的实现如下,注意for循环遍历所有的客户端流输出:

  
  /**
    Append an event to all of current connections. */
  public
  void append(LoggingEvent event) {
    if (event != null) {
      // set up location info if requested
      if (locationInfo) {
        event.getLocationInformation();
      }
      if (application != null) {
        event.setProperty("application", application);
      } 
      event.getNDC();
      event.getThreadName();
      event.getMDCCopy();
      event.getRenderedMessage();
      event.getThrowableStrRep();
        
      if (buffer != null) {
        buffer.add(event);
      }
    }
    // if no event or no open connections, exit now
    if ((event == null) || (oosList.size() == 0)) {
      return;
    }

    // loop through the current set of open connections, appending the event to each
    for (int streamCount = 0; streamCount<oosList.size();streamCount++){     
      ObjectOutputStream oos = null;
      try {
        oos = (ObjectOutputStream)oosList.elementAt(streamCount);
      }
      catch (ArrayIndexOutOfBoundsException e) {
        // catch this, but just don't assign a value
        // this should not really occur as this method is
        // the only one that can remove oos's (besides cleanUp).
      }
      
      // list size changed unexpectedly? Just exit the append.
      if (oos == null)
        break;
        
      try {
        oos.writeObject(event);
        oos.flush();
        // Failing to reset the object output stream every now and
        // then creates a serious memory leak.
        // right now we always reset. TODO - set up frequency counter per oos?
        oos.reset();
      } catch(IOException e) {
        if (e instanceof InterruptedIOException) {
          Thread.currentThread().interrupt();
        }
        // there was an io exception so just drop the connection
        oosList.removeElementAt(streamCount);
        LogLog.debug("dropped connection");
        // decrement to keep the counter in place (for loop always increments)
        streamCount--;
      }
    }
  }

13.2demo

demo log4j config:

log4j.rootLogger=INFO,socket
log4j.appender.socket=org.apache.log4j.net.SocketHubAppender
log4j.appender.socket.AdvertiseViaMulticastDNS=false
log4j.appender.socket.Port=4560
log4j.appender.socket.LocationInfo=true
log4j.appender.socket.Application=luohw
log4j.appender.socket.BufferSize=1
日志效果,这里使用telnet作为客户端查看SocketHubAppender返回的数据

SocketHubAppender

14.SyslogAppender

Syslog常被称为系统日志或系统记录,是一种用来在互联网协定(TCP/IP)的网络中传递记录档讯息的标准。syslog协定属于一种主从式协定:syslog发送端会传送出一个小的文字讯息(小于1024字节)到syslog接收端。接收端通常名为syslogd、syslog daemon或syslog服务器,系统日志讯息可以被以UDP协定及╱或TCP协定来传送。完整syslog协议可见:http://www.ietf.org/rfc/rfc3164.txt

14.1 syslog消息格式

syslog消息由一个百分号开始,其结构为:%FACILITY-SUBFACILITY-SEVERITY-MNEMONIC:Message-text

  • Facility(归属模块):由2个或2个以上大写字母组成的代码,用来表示硬件设备、协议或系统软件的型号。
  • Severity(严重性):范围为0~7的数字编码,表示了事件的严重程度。
  • Mnemonic(助记码):唯一标识出错误消息的代码。
  • Message-text(消息文本):用于描述事件的文本串。消息中的这一部分有时会包含事件的细节信息,其中包括目的端口号、网络地址或系统内存地址空间中所对应的的地址。

SyslogAppender用于将日志LoggingEvent输出到系统syslog守护进程,此进程可以是在一台远程主机上面。底层SyslogAppender使用java.net.DatagramSocket.DatagramSocket写出消息。SyslogAppender可配置属性主要有:

  • SyslogHost syslog守护进程所在的主机,不支持配置端口,使用默认的514
  • Facility 日志归属程序模块,不区分大小写,需要为下面之一:KERN, USER, MAIL, DAEMON, AUTH, SYSLOG, LPR, NEWS, UUCP,CRON, AUTHPRIV, FTP, LOCAL0, LOCAL1, LOCAL2, LOCAL3, LOCAL4,LOCAL5, LOCAL6, LOCAL7
  • FacilityPrinting LoggingEvent的渲染后消息是否包含Facility信息作为前缀,默认为false
  • Header LoggingEvent的渲染后消息是否包含日志发生处主机名和日志产生时间,建议配置true

14.2 实现细节

SyslogAppender的append实现如下,具体使用java.net.DatagramSocket.DatagramSocket写出消息:

  
public void append(LoggingEvent event) {
    if(!isAsSevereAsThreshold(event.getLevel()))
        return;
    // We must not attempt to append if sqw is null.
    if(sqw == null) {
        errorHandler.error("No syslog host is set for SyslogAppedender named \""+this.name+"\".");
        return;
    }
    if (!layoutHeaderChecked) {
        if (layout != null && layout.getHeader() != null) {
            sendLayoutMessage(layout.getHeader());
        }
        layoutHeaderChecked = true;
    }

    String hdr = getPacketHeader(event.timeStamp);
    String packet;
    if (layout == null) {
        packet = String.valueOf(event.getMessage());
    } else {
        packet = layout.format(event);
    }
    if(facilityPrinting || hdr.length() > 0) {
        StringBuffer buf = new StringBuffer(hdr);
        if(facilityPrinting) {
            buf.append(facilityStr);
        }
        buf.append(packet);
        packet = buf.toString();
    }

    sqw.setLevel(event.getLevel().getSyslogEquivalent());
    //
    //   if message has a remote likelihood of exceeding 1024 bytes
    //      when encoded, consider splitting message into multiple packets
    if (packet.length() > 256) {
        splitPacket(hdr, packet);
    } else {
        sqw.write(packet);
    }

    if (layout == null || layout.ignoresThrowable()) {
        String[] s = event.getThrowableStrRep();
        if (s != null) {
            for(int i = 0; i < s.length; i++) {
                if (s[i].startsWith("\t")) {
                    sqw.write(hdr+TAB+s[i].substring(1));
                } else {
                    sqw.write(hdr+s[i]);
                }
            }
        }
    }
}

org.apache.log4j.helpers.SyslogWriter 是对java.net.DatagramSocket的封装,提供java.io.Writer的接口。

 
public void write(final String string) throws IOException {
    if(this.ds != null && this.address != null) {
        byte[] bytes = string.getBytes();
        //  syslog packets must be less than 1024 bytes
        int bytesLength = bytes.length;
        if (bytesLength >= 1024) {
            bytesLength = 1024;
        }
        DatagramPacket packet = new DatagramPacket(bytes, bytesLength, address, port);
        ds.send(packet);
    }
}

org.apache.log4j.helpers.SyslogQuietWriter利用了装饰器模式,在日志消息前增加Facility和日志级别前缀。

  
public void write(String string) {
    super.write("<"+(syslogFacility | level)+">" + string);
}

14.3 demo

demo log4j config:

log4j.rootLogger=INFO,syslog
log4j.appender.syslog=org.apache.log4j.net.SyslogAppender
log4j.appender.syslog.SyslogHost=127.0.0.1
log4j.appender.syslog.Facility=LOCAL1
log4j.appender.syslog.header=true
log4j.appender.syslog.Threshold=INFO
log4j.appender.syslog.layout=org.apache.log4j.PatternLayout
log4j.appender.syslog.layout.ConversionPattern=%d %-5p [%t] [%F:%L] - %m%n

后面继续~

标签: none
评论列表
  1. 写得真是太好了,我要保存上去当成条记,随时检查.
    365bet官网 http://xxgc.hbust.com.cn

  2. 文章写的真的挺好,学习了

  3. 赞一个 非常不错 很有个性
    ca88亚洲城官网 http://purchase.hbust.com.cn

  4. 来晚了啊 看不到了 看不到了
    ca88亚洲城 http://yjsc.hbust.com.cn/page3.asp

  5. 博主的文章写得非常棒 很喜欢

添加新评论