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



    默认情况下,Valve是打开的,在server.xml中我们可以找到如下配置:



<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" pattern="%h %l %u %t "%r" %s %b" prefix="localhost_access_log." suffix=".txt"/>



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



通过分析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。



扩展的步骤:




@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);
            }
        }
    }
  1. 处理可配的参数

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();
  1. 根据不同的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();
            }
        };
    }
  1. 在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方法。