tomcat accesslog日誌擴展

    因爲工做須要,最近對tomcat的日誌進行了一些研究,發現其日誌大體能夠分爲兩類,一類是運行日誌,即日常咱們所說的catalina.out日誌,由tomcat內部代碼調用logger打印出來的;另外一類是accesslog訪問日誌,即記錄外部請求訪問的信息。處理這兩類日誌,tomcat默認採用了不一樣的方式,運行類日誌默認採用的是java.util.logging框架,由conf下的logging.properties負責配置管理,也能夠支持切換到log4j2(具體可參看個人前一篇博文:升級tomcat7的運行日誌框架到log4j2 );對於訪問日誌,tomcat默認是按日期直接寫進文件,由server.xml中配置Valve來管理。html

    默認狀況下,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  )java

經過分析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)實現以下:apache

@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中。bootstrap

  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方法。tomcat

 
附件是kafka實現的 源碼下載
相關文章
相關標籤/搜索