• tomcat accesslog日志扩展


        由于工作需要,最近对tomcat的日志进行了一些研究,发现其日志大致可以分为两类,一类是运行日志,即平常我们所说的catalina.out日志,由tomcat内部代码调用logger打印出来的;另一类是accesslog访问日志,即记录外部请求访问的信息。处理这两类日志,tomcat默认采用了不同的方式,运行类日志默认采用的是java.util.logging框架,由conf下的logging.properties负责配置管理,也可以支持切换到log4j2(具体可参看我的前一篇博文:升级tomcat7的运行日志框架到log4j2 );对于访问日志,tomcat默认是按日期直接写进文件,由server.xml中配置Valve来管理。

        默认情况下,Valve是打开的,在server.xml中我们可以找到如下配置:
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" pattern="%h %l %u %t &quot;%r&quot; %s %b" prefix="localhost_access_log." suffix=".txt"/>

    此配置会在logs下生成一个localhost_access_log.日期.txt,里面记录每次外部访问的一些信息,信息的内容是根据pattern来配置的,%后加不同的字母表示不同的信息,如上述默认的pattern配置会记录“访问端ip 用户名 时间 第一行请求内容 http状态码 发送字节大小”等内容,详细配置细节可以参考tomcat的accelog(url:https://tomcat.apache.org/tomcat-7.0-doc/config/valve.html#Access_Logging  )

    通过分析AccessLogValve代码,其内部用的java方法操作文件处理代码:
    @Override
        public void log(Request request, Response response, long time) {
            if (!getState().isAvailable() || !getEnabled() || logElements == null
                    || condition != null
                    && null != request.getRequest().getAttribute(condition)
                    || conditionIf != null
                    && null == request.getRequest().getAttribute(conditionIf)) {
                return;
            }
            /**
             * XXX This is a bit silly, but we want to have start and stop time and
             * duration consistent. It would be better to keep start and stop
             * simply in the request and/or response object and remove time
             * (duration) from the interface.
             */
            long start = request.getCoyoteRequest().getStartTime();
            Date date = getDate(start + time);
            // 字符缓冲区
            CharArrayWriter result = charArrayWriters.pop();
            if (result == null) {
                result = new CharArrayWriter(128);
            }
            // pattern里不同的%表示不同的logElement,此处用result收集所有logElement里追加的内容
            for (int i = 0; i < logElements.length; i++) {
                logElements[i].addElement(result, date, request, response, time);
            }
            // 写文件将result写入
            log(result);
            if (result.size() <= maxLogMessageBufferSize) {
                result.reset();
                charArrayWriters.push(result);
            }
        }

    其中log(result)实现如下:

    @Override
        public void log(CharArrayWriter message) {
            // 每个一秒检查一下是否需要切换文件
            rotate();
            // 如果存在文件,先关闭再重新打开一个新日期的文件
            if (checkExists) {
                synchronized (this) {
                    if (currentLogFile != null && !currentLogFile.exists()) {
                        try {
                            close(false);
                        } catch (Throwable e) {
                            ExceptionUtils.handleThrowable(e);
                            log.info(sm.getString("accessLogValve.closeFail"), e);
                        }
                        /* Make sure date is correct */
                        dateStamp = fileDateFormatter.format(
                                new Date(System.currentTimeMillis()));
                       
                        open();
                    }
                }
            }
            // Log this message 同步加锁写入日志文件,此处使用了buffer
            try {
                synchronized(this) {
                    if (writer != null) {
                        message.writeTo(writer);
                        writer.println("");
                        if (!buffered) {
                            writer.flush();
                        }
                    }
                }
            } catch (IOException ioe) {
                log.warn(sm.getString(
                        "accessLogValve.writeFail", message.toString()), ioe);
            }
        }

      通过上述核心代码可以看到,默认的tomcat是利用缓冲写文件的方式进行访问日志记录的,如果需要分析访问日志,比如找出一天内有多少过ip访问过,或者某一个ip在一分钟内访问了多少次,一般的处理方式是读取accesslog文件内容并进行分析,这么做一方面是无法满足实时分析的目的,更重要的数据量大的时候会严重影响分析效率,因此我们需要对其进行扩展,比如我们可以把访问日志打到kafka或mango中。

      tomcat 8之前版本的扩展相比于8及以后的版本有点麻烦,因为从tomcat 8以后,把accesslog专门提取了一个抽象类,负责根据pattern来组装内容,并留出了log(CharArrayWriter message)抽象方法用于扩展,开发只要扩展重写此方法即可,但8以前的版本需要自己继承ValveBase并实现AccessLog接口,重写log(Request request, Response response, long time)方法,由于作者所在的公司目前线上使用的是tomcat7 ,因此下面主要讲述如何在tomcat 7下进accesslog日志扩展进kafka。
    扩展的步骤:
    1.     创建LeKafkaAccesslogValve继承ValveBase并实现AccessLog接口:
      @Override
          public void log(Request request, Response response, long time) {
              if (producerList != null && getEnabled() && getState().isAvailable() && null != this.accessLogElement) {
                  try {
                      getNextProducer().send(new ProducerRecord<byte[], byte[]>(topic, this.accessLogElement.buildLog(request,response,time,this).getBytes(StandardCharsets.UTF_8))).get(timeoutMillis, TimeUnit.MILLISECONDS);
                  } catch (InterruptedException | ExecutionException | TimeoutException e) {
                      log.error("accesslog in kafka exception", e);
                  }
              }
          }

       

    2. 处理可配的参数
      private String topic;
          private String bootstrapServers;
          
          //  If set to zero then the producer will not wait for any acknowledgment from the server at all. 
          private String acks; 
          
          private String producerSize ;
          
          private String properties;
          
          private List<Producer<byte[], byte[]>> producerList;
          private AtomicInteger producerIndex = new AtomicInteger(0);
          private int timeoutMillis;
          private boolean enabled = true; // 默认配置问true,即打入kafka,除非有异常情况或主动设置了。
          
          private String pattern;
          private AccessLogElement accessLogElement;
          private String localeName;
          private Locale locale = Locale.getDefault();

       

    3. 根据不同的pattern配置解析出需要打印的内容(此部分tomcat8 已经在AbstractAccessLogValve中抽取出来)
      public static AccessLogElement parsePattern(String pattern) {
              final List<AccessLogElement> list = new ArrayList<>();
              boolean replace = false;
              StringBuilder buf = new StringBuilder();
              for (int i = 0; i < pattern.length(); ++i) {
                  char ch = pattern.charAt(i);
                  if (replace) {
                      if ('{' == ch) {
                          StringBuilder name = new StringBuilder();
                          int j = i + 1;
                          for (; (j < pattern.length()) && ('}' != pattern.charAt(j)); ++j) {
                              name.append(pattern.charAt(j));
                          }
                          if (j + 1 < pattern.length()) {
                              ++j;
                              list.add(createAccessLogElement(name.toString(), pattern.charAt(j)));
                              i = j;
                          } else {
                              list.add(createAccessLogElement(ch));
                          }
                      } else {
                          list.add(createAccessLogElement(ch));
                      }
                      replace = false;
                  } else if (ch == '%') {
                      replace = true;
                      list.add(new StringElement(buf.toString()));
                      buf = new StringBuilder();
                  } else {
                      buf.append(ch);
                  }
              }
              if (buf.length() > 0) {
                  list.add(new StringElement(buf.toString()));
              }
              return new AccessLogElement() {
                  @Override
                  protected String buildLog(Request request, Response response, long time, AccessLog accesslog) {
                      StringBuilder sBuilder = new StringBuilder(30);
                      for (AccessLogElement accessLogElement : list) {
                          sBuilder.append(accessLogElement.buildLog(request, response, time, accesslog));
                      }
                      return sBuilder.toString();
                  }
              };
          }

       

    4. 在server.xml中增加配置
      <Valve className="com.letv.shop.lekafkavalve.LeKafkaAccesslogValve" enabled="true"  topic="info" pattern="%{yyyy-MM-dd HH:mm:ss}t||info||AccessValve||Tomcat||%A||%a||%r||%s||%D" bootstrapServers="kafka地址" producerSize="5" properties="acks=0||producer.size=3"/>

      tomcat8及以后版本的扩展要方便的多,直接继承AbstractAccessLogValve并重写log方法。

     
    附件是kafka实现的源码下载
     
     
     
     
     
  • 相关阅读:
    多线程编程1-定义理解与三种实现方式
    Java类集框架详细汇总-底层分析
    Trie、并查集、堆、Hash表学习过程以及遇到的问题
    spring前导知识-Tomcat、Maven等配置
    双指针、位运算、离散化、区间合并的手动模拟
    单点登录原理与简单实现
    Spring引入外部配置文件
    Java异常机制
    Java多线程-线程的概念和创建
    java webservice服务器端获取request对象的三种方式
  • 原文地址:https://www.cnblogs.com/dimmacro/p/5566480.html
Copyright © 2020-2023  润新知