package org.apache.sling.tail.impl; | |
/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
import ch.qos.logback.classic.LoggerContext; | |
import ch.qos.logback.classic.spi.ILoggingEvent; | |
import ch.qos.logback.core.Appender; | |
import ch.qos.logback.core.FileAppender; | |
import org.apache.felix.scr.annotations.*; | |
import org.apache.felix.scr.annotations.Properties; | |
import org.apache.felix.webconsole.AbstractWebConsolePlugin; | |
import org.apache.felix.webconsole.WebConsoleConstants; | |
import org.apache.sling.commons.json.io.JSONWriter; | |
import org.apache.sling.tail.LogFilter; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import javax.servlet.Servlet; | |
import javax.servlet.ServletException; | |
import javax.servlet.http.Cookie; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.PrintWriter; | |
import java.io.RandomAccessFile; | |
import java.util.*; | |
/** | |
* | |
*/ | |
@Component | |
@Service(value = { Servlet.class }) | |
@Properties({ | |
@Property(name=org.osgi.framework.Constants.SERVICE_DESCRIPTION, | |
value="Apache Sling Web Console Plugin to tail log(s) of this Sling instance"), | |
@Property(name= WebConsoleConstants.PLUGIN_LABEL, value=LogTailerWebConsolePlugin.LABEL), | |
@Property(name=WebConsoleConstants.PLUGIN_TITLE, value=LogTailerWebConsolePlugin.TITLE), | |
@Property(name="felix.webconsole.configprinter.modes", value={"always"}) | |
}) | |
public class LogTailerWebConsolePlugin extends AbstractWebConsolePlugin { | |
public static final String LABEL = "tail"; | |
public static final String TITLE = "Tail Logs"; | |
private final Logger log = LoggerFactory.getLogger(this.getClass()); | |
private static final int LINES_TO_TAIL = 100; | |
private static final String POSITION_COOKIE = "log.tail.position"; | |
private static final String FILTER_COOKIE = "log.tail.filter"; | |
private static final String MODIFIED_COOKIE = "log.modified"; | |
private static final String CREATED_COOKIE = "log.created"; | |
private String fileName = ""; | |
private File errLog; | |
@Override | |
protected void renderContent(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { | |
if(isAjaxRequest(request)) { | |
parseCommand(request, response); | |
RandomAccessFile randomAccessFile = null; | |
try { | |
try { | |
randomAccessFile = new RandomAccessFile(errLog, "r"); | |
log.debug("Tailing file " + fileName + " of length " + randomAccessFile.length()); | |
} catch (Exception e) { | |
log.error("Error reading " + fileName, e); | |
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); | |
return; | |
} | |
JSONWriter json = new JSONWriter(response.getWriter()); | |
json.setTidy(true); | |
json.object(); | |
boolean reverse = false; | |
//long created = getCreatedTimestampFromCookie(request); | |
/*long modified = getModifiedTimestampFromCookie(request); | |
if(errLog.lastModified() == modified) { | |
json.endObject(); | |
return; | |
} | |
else { | |
persistCookie(response, MODIFIED_COOKIE, String.valueOf(errLog.lastModified())); | |
}*/ | |
long pos = getPositionFromCookie(request); | |
if(pos < 0) { | |
pos = randomAccessFile.length()-1; | |
reverse = true; | |
} | |
else if(pos > randomAccessFile.length()) {//file rotated | |
pos = 0; | |
} | |
LogFilter[] query = getQueryFromCookie(request); | |
if(reverse) { | |
randomAccessFile.seek(pos); | |
if(randomAccessFile.read() == '\n') { | |
pos--; | |
randomAccessFile.seek(pos); | |
if(randomAccessFile.read() == '\r') { | |
pos--; | |
} | |
} | |
json.key("content").array(); | |
int found = 0; | |
StringBuilder sb = new StringBuilder(); | |
String line = null; | |
List<String> lines = new ArrayList<String>(); | |
while(found != LINES_TO_TAIL && pos > 0) { | |
boolean eol = false; | |
randomAccessFile.seek(pos); | |
int c = randomAccessFile.read(); | |
if(c == '\n') { | |
found++; | |
sb = sb.reverse(); | |
line = sb.toString(); | |
sb = new StringBuilder(); | |
eol = true; | |
pos--; | |
if(pos > 0) { | |
randomAccessFile.seek(pos); | |
if(randomAccessFile.read() == '\r') { | |
pos--; | |
} | |
} | |
} | |
else { | |
sb.append((char)c); | |
pos--; | |
} | |
if(eol) { | |
if(filter(line, query)){ | |
lines.add(line); | |
} | |
} | |
} | |
if(pos < 0) { | |
if(filter(line, query)){ | |
lines.add(line); | |
} | |
} | |
for(int i=lines.size()-1; i > -1; i--) { | |
json.object().key("line").value(lines.get(i)).endObject(); | |
} | |
json.endArray(); | |
json.endObject(); | |
} | |
else { | |
randomAccessFile.seek(pos); | |
String line = null; | |
int lineCount = 0; | |
json.key("content").array(); | |
boolean read = true; | |
while(read) { | |
StringBuilder input = new StringBuilder(); | |
int c = -1; | |
boolean eol = false; | |
while (!eol) { | |
switch (c = randomAccessFile.read()) { | |
case -1: | |
case '\n': | |
eol = true; | |
break; | |
case '\r': | |
eol = true; | |
long cur = randomAccessFile.getFilePointer(); | |
if ((randomAccessFile.read()) != '\n') { | |
randomAccessFile.seek(cur); | |
} | |
break; | |
default: | |
input.append((char)c); | |
break; | |
} | |
} | |
if ((c == -1) && (input.length() == 0)) { | |
read = false; | |
continue; | |
} | |
line = input.toString(); | |
lineCount++; | |
if(lineCount == LINES_TO_TAIL) { | |
read = false; | |
} | |
pos = randomAccessFile.getFilePointer(); | |
if(filter(line, query)){ | |
json.object().key("line").value(line).endObject(); | |
} | |
} | |
json.endArray(); | |
json.endObject(); | |
} | |
persistCookie(response, POSITION_COOKIE, String.valueOf(randomAccessFile.getFilePointer())); | |
} catch (Exception e) { | |
log.error("Error tailing " + fileName, e); | |
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); | |
} | |
finally { | |
try { | |
if(randomAccessFile != null) { | |
randomAccessFile.close(); | |
} | |
} | |
catch (Exception e) { | |
log.error("Error closing " + fileName, e); | |
} | |
} | |
} | |
else { | |
PrintWriter printWriter = response.getWriter(); | |
printWriter.println("<script type=\"text/javascript\" src=\"/libs/sling/logtail-plugin/js/tail.js\"></script>"); | |
printWriter.println("<link href=\"/libs/sling/logtail-plugin/css/tail.css\" rel=\"stylesheet\" type=\"text/css\"></link>"); | |
printWriter.println("<div class=\"header-cont\">"); | |
printWriter.println(" <div class=\"header\" style=\"display:none;\">"); | |
printWriter.println(" <table>"); | |
printWriter.println(" <tr>"); | |
printWriter.println(" <td><button class=\"numbering\" title=\"Show Line Numbers\" data-numbers=\"false\">Show Line No.</button></td>"); | |
printWriter.println(" <td><button class=\"pause\" title=\"Pause\">Pause</button></td>"); | |
printWriter.println(" <td class=\"longer\"><label>Sync frequency(msec)</label>"); | |
printWriter.println(" <button class=\"faster\" title=\"Sync Faster\">-</button>"); | |
printWriter.println(" <input id=\"speed\" type=\"text\" value=\"3000\"/>"); | |
printWriter.println(" <button class=\"slower\" title=\"Sync Slower\">+</button></td>"); | |
printWriter.println(" <td><button class=\"tail\" title=\"Unfollow Tail\" data-following=\"true\">Unfollow</button></td>"); | |
printWriter.println(" <td><button class=\"highlighting\" title=\"Highlight\">Highlight</button></td>"); | |
printWriter.println(" <td><button class=\"clear\" title=\"Clear Display\">Clear</button></td>"); | |
printWriter.println(" <td class=\"longer\"><input id=\"filter\" type=\"text\"/><span class=\"filterClear ui-icon ui-icon-close\" title=\"Clear Filter\"> </span><button class=\"filter\" title=\"Filter Logs\">Filter</button></td>"); | |
printWriter.println(" <td><button class=\"refresh\" title=\"Reload Logs\">Reload</button></td>"); | |
printWriter.println(" <td><button class=\"sizeplus\" title=\"Bigger\">a->A</button></td>"); | |
printWriter.println(" <td><button class=\"sizeminus\" title=\"Smaller\">A->a</button></td>"); | |
printWriter.println(" <td><button class=\"top\" title=\"Scroll to Top\">Top</button></td>"); | |
printWriter.println(" <td><button class=\"bottom\" title=\"Scroll to Bottom\">Bottom</button></td>"); | |
printWriter.println(" </tr>"); | |
printWriter.println(" <tr>"); | |
printWriter.println(" <td class=\"loadingstatus\" colspan=\"2\" data-status=\"inactive\"><ul><li></li></ul></td>"); | |
printWriter.println(" <td>Tailing <select id=\"logfiles\">" + getOptions() + "</select></td>"); | |
printWriter.println(" </tr>"); | |
printWriter.println(" </table>"); | |
printWriter.println(" </div>"); | |
printWriter.println(" <div class=\"pulldown\" title=\"Click to show options\"> == </div>"); | |
printWriter.println("</div>"); | |
printWriter.println(""); | |
printWriter.println(" <div class=\"content\">"); | |
printWriter.println(""); | |
printWriter.println(" <div id=\"logarea\"></div>"); | |
printWriter.println(""); | |
printWriter.println(" </div>"); | |
} | |
} | |
protected void parseCommand(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { | |
String cmd = request.getParameter("command"); | |
if(cmd == null) { | |
return; | |
} | |
if(cmd.equals("reset")) { | |
deleteCookie(response, FILTER_COOKIE); | |
} | |
else if(cmd.startsWith("filter:")) { | |
String queryStr = cmd.substring(7); | |
if(queryStr.length()==0) { | |
deleteCookie(response, FILTER_COOKIE); | |
} | |
else { | |
persistCookie(response, FILTER_COOKIE, queryStr); | |
log.info("Filtering on : " + queryStr); | |
} | |
} | |
else if(cmd.startsWith("file:")) { | |
if(!fileName.equals(cmd.substring(5))) { | |
deleteCookie(response, FILTER_COOKIE); | |
deleteCookie(response, POSITION_COOKIE); | |
fileName = cmd.substring(5); | |
errLog = new File(filePathMap.get(fileName)); | |
if(!errLog.exists()) { | |
throw new ServletException("File " + fileName + " doesn't exist"); | |
} | |
if(!errLog.canRead()) { | |
throw new ServletException("Cannot read file " + fileName); | |
} | |
} | |
} | |
} | |
@Override | |
public String getLabel() { | |
return LABEL; | |
} | |
@Override | |
public String getTitle() { | |
return TITLE; | |
} | |
private HashMap<String, String> filePathMap = new HashMap<String, String>(); | |
private String getKey(File file) { | |
if(!filePathMap.containsKey(file.getName())) { | |
filePathMap.put(file.getName(), file.getAbsolutePath()); | |
} | |
return file.getName(); | |
} | |
private String getOptions() { | |
Set<String> logFiles = new HashSet<String>(); | |
LoggerContext context = (LoggerContext)LoggerFactory.getILoggerFactory(); | |
for (ch.qos.logback.classic.Logger logger : context.getLoggerList()) { | |
for (Iterator<Appender<ILoggingEvent>> index = logger.iteratorForAppenders(); index.hasNext();) { | |
Appender<ILoggingEvent> appender = index.next(); | |
if(appender instanceof FileAppender) { | |
FileAppender fileAppender = (FileAppender) appender; | |
String logfilePath = fileAppender.getFile(); | |
logFiles.add(logfilePath); | |
} | |
} | |
} | |
String logFilesHtml = "<option value=\"\"> - Select file - </option>"; | |
for(String logFile : logFiles) { | |
File file = new File(logFile); | |
logFilesHtml += "<option value=\"" + getKey(file) + "\">" + file.getName() + "</option>"; | |
} | |
return logFilesHtml; | |
} | |
@Override | |
protected boolean isHtmlRequest( final HttpServletRequest request ) { | |
return !isAjaxRequest(request); | |
} | |
private boolean isAjaxRequest( final HttpServletRequest request) { | |
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); | |
} | |
private boolean filter(String str, LogFilter[] query) { | |
if(query != null) { | |
for(LogFilter q : query) { | |
if(!q.eval(str)) { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
private void deleteCookie(HttpServletResponse response, String name) { | |
Cookie cookie = new Cookie(name, ""); | |
cookie.setMaxAge(0); | |
response.addCookie(cookie); | |
log.debug("Deleting cookie :: " + cookie.getName()); | |
} | |
private void persistCookie(HttpServletResponse response, String name, String value) { | |
Cookie cookie = new Cookie(name , value); | |
//cookie.setPath("/system/console/" + LABEL); | |
response.addCookie(cookie); | |
log.debug("Adding cookie :: " + cookie.getName() + " " + cookie.getValue()); | |
} | |
private LogFilter[] getQueryFromCookie(HttpServletRequest request) { | |
try { | |
for(Cookie cookie : request.getCookies()) { | |
if(cookie.getName().equals(FILTER_COOKIE)) { | |
String[] parts = cookie.getValue().split("&&"); | |
LogFilter[] conditions = new LogFilter[parts.length]; | |
for(int i=0; i<parts.length; i++) { | |
final String part = parts[i]; | |
conditions[i] = new LogFilter() { | |
public boolean eval(String input) { | |
return input.contains(part); | |
} | |
public String toString() { | |
return part; | |
} | |
}; | |
} | |
return conditions; | |
} | |
} | |
} | |
catch (Exception e) { | |
} | |
return null; | |
} | |
private long getCreatedTimestampFromCookie(HttpServletRequest request) { | |
try { | |
for(Cookie cookie : request.getCookies()) { | |
if(cookie.getName().equals(CREATED_COOKIE)) { | |
return Long.parseLong(cookie.getValue()); | |
} | |
} | |
} | |
catch (Exception e) { | |
} | |
return -1; | |
} | |
private long getModifiedTimestampFromCookie(HttpServletRequest request) { | |
try { | |
for(Cookie cookie : request.getCookies()) { | |
if(cookie.getName().equals(MODIFIED_COOKIE)) { | |
return Long.parseLong(cookie.getValue()); | |
} | |
} | |
} | |
catch (Exception e) { | |
} | |
return -1; | |
} | |
private long getPositionFromCookie(HttpServletRequest request) { | |
try { | |
for(Cookie cookie : request.getCookies()) { | |
if(cookie.getName().equals(POSITION_COOKIE)) { | |
return Long.parseLong(cookie.getValue()); | |
} | |
} | |
} | |
catch (Exception e) { | |
log.debug("Position specified is invalid, Tailing from beginning of the file.", e); | |
} | |
return -1; | |
} | |
} |