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