You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/10/18 23:24:46 UTC

[sling-org-apache-sling-tail] 01/08: SLING-4897 - Web console log tail plugin, contributed by Varun Nagpal, thanks!

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-tail.git

commit 7984a843cefd1855b8c36f2692eeea37fbef5c70
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Fri Aug 7 13:46:15 2015 +0000

    SLING-4897 - Web console log tail plugin, contributed by Varun Nagpal, thanks!
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1694685 13f79535-47bb-0310-9956-ffa450edef68
---
 README.md                                          |   5 +
 pom.xml                                            | 125 ++++++
 src/main/java/org/apache/sling/tail/LogFilter.java |  27 ++
 .../sling/tail/impl/LogTailerWebConsolePlugin.java | 458 +++++++++++++++++++++
 src/main/resources/libs/tail/css/tail.css          | 121 ++++++
 src/main/resources/libs/tail/js/tail.js            | 425 +++++++++++++++++++
 6 files changed, 1161 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..849d6f9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+sling-logtail
+=============
+
+Sling bundle encapsulating a Web Console for tailing logs over a browser
+Install the bundle via System Console or mvn sling:install on any running sling instance and navigate to /system/console/tail
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..26653e5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 
+	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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>24</version>
+    </parent>
+
+    <artifactId>org.apache.sling.tail</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Log Tail Implementation</name>
+    <description>
+        This bundle enables a web tail view of the system log files.
+    </description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Sling-Bundle-Resources>
+                            /libs/tail/css, /libs/tail/js
+                        </Sling-Bundle-Resources>
+                    </instructions>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+                <extensions>true</extensions>
+                <executions>
+                    <execution>
+                        <id>generate-scr-descriptor</id>
+                        <goals>
+                            <goal>scr</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>biz.aQute.bnd</groupId>
+            <artifactId>bnd</artifactId>
+            <version>2.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- OSGi Libraries not included here -->
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>4.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>4.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- servlet API for the web console plugin -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.3</version>
+        </dependency>
+
+        <!-- Required for log tail plugin -->
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.webconsole</artifactId>
+            <version>3.1.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.json</artifactId>
+            <version>2.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.log</artifactId>
+            <version>4.0.0</version>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/src/main/java/org/apache/sling/tail/LogFilter.java b/src/main/java/org/apache/sling/tail/LogFilter.java
new file mode 100644
index 0000000..030cd8d
--- /dev/null
+++ b/src/main/java/org/apache/sling/tail/LogFilter.java
@@ -0,0 +1,27 @@
+package org.apache.sling.tail;
+
+/*
+ * 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.
+ */
+
+/**
+ *
+ */
+public interface LogFilter {
+    boolean eval(String input);
+}
diff --git a/src/main/java/org/apache/sling/tail/impl/LogTailerWebConsolePlugin.java b/src/main/java/org/apache/sling/tail/impl/LogTailerWebConsolePlugin.java
new file mode 100644
index 0000000..78d8668
--- /dev/null
+++ b/src/main/java/org/apache/sling/tail/impl/LogTailerWebConsolePlugin.java
@@ -0,0 +1,458 @@
+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/tail/js/tail.js\"></script>");
+            printWriter.println("<link href=\"/libs/tail/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\">&nbsp;</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 &nbsp; <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\">&nbsp;==&nbsp;</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;
+    }
+}
diff --git a/src/main/resources/libs/tail/css/tail.css b/src/main/resources/libs/tail/css/tail.css
new file mode 100644
index 0000000..062210e
--- /dev/null
+++ b/src/main/resources/libs/tail/css/tail.css
@@ -0,0 +1,121 @@
+/*
+ * 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.
+ */
+
+.header-cont {
+    position:fixed;
+    top:0;
+    left:0;
+}
+
+.pulldown {
+    height:20px;
+    cursor:pointer;
+    background:#CCCCCC;
+    margin: 0 auto;
+    border-radius: 0 0 25px 25px;
+    text-align: center;
+}
+
+.header {
+    background:#F0F0F0;
+    border:1px solid #CCC;
+    margin:0px auto;
+    height:50px;
+    vertical-align:middle;
+}
+
+.header table td {
+	width:5%;
+    text-align:center;
+    vertical-align:middle;
+}
+
+.header table td.longer {
+    width:20%;
+}
+
+.header table td button {
+	cursor:pointer;
+}
+
+.content {
+	font:Courier-New;
+    font-size:9px;
+}
+
+.criteria-item {
+	cursor:pointer;
+}
+
+.criteria-item.selected {
+	background-color: #2B60DE;
+    color:white;
+}
+
+.highlight-content-inner-div {
+	float:left;
+    clear:both;
+    width:95%;
+    margin-bottom: 10px;
+}
+
+span.box {
+    min-width:50px;
+    text-overflow:clip;
+    border:1px solid black;
+    margin-right: 5px;
+    margin-left: 5px;
+    margin-top:5px;
+    margin-bottom:5px;
+}
+
+.criteria-list {
+	padding: 0;
+    list-style:none;
+    margin:0;
+}
+
+#criteria {
+	min-height: 200px;
+    border:1px solid;
+}
+
+.lineNumberCol {
+	background-color: grey;
+	color: white;
+}
+
+#speed {
+    width:50px;
+}
+
+.loadingstatus li {
+	font-size:30px;
+	color:grey;
+	list-style: inherit !important;
+}
+
+.hide {
+    display: none;
+}
+
+.filterClear {
+  display: inline-block;
+  vertical-align: middle;
+}
\ No newline at end of file
diff --git a/src/main/resources/libs/tail/js/tail.js b/src/main/resources/libs/tail/js/tail.js
new file mode 100644
index 0000000..fc41594
--- /dev/null
+++ b/src/main/resources/libs/tail/js/tail.js
@@ -0,0 +1,425 @@
+/*
+ * 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.
+ */
+
+    var logarea;
+    var follow = true;
+    var searchCriteria = [];
+    var logfontsize = 9;
+    var refreshInterval = 3000;
+    var _load = false;
+    var _lineNum = 0;
+    var _isLineNumShowing = false;
+
+    var modal;
+
+    var showLine = function(text) {
+        logarea.append("<br/>");
+
+        if(text.indexOf("\t") == 0) {
+            text = "<span style='padding-left:5%;'>" + text + "</span>";
+        }
+        else {
+            text = "<span>" + text + "</span>"
+        }
+
+        for(var i=0; i < searchCriteria.length; i++) {
+            if(text.indexOf(searchCriteria[i]["string"]) >=0) {
+                if(searchCriteria[i]["bold"]) {text = "<b>" + text + "</b>";}
+                if(searchCriteria[i]["italic"]) {text = "<i>" + text + "</i>";}
+                var color = "red"; if(searchCriteria[i]["forecolor"]) {color = searchCriteria[i]["forecolor"];}
+                var bg = ""; if(searchCriteria[i]["backcolor"]) {bg = searchCriteria[i]["backcolor"];}
+                text = "<span style='color:" + color + ";background-color:" + bg + "'>" + text + "</span>";
+            }
+        }
+		_lineNum++;
+        logarea.append("<a class='lineNumberCol " + (_isLineNumShowing?"":"hide") + "' name='line_" + _lineNum + "'>&nbsp;&nbsp;&nbsp;" + _lineNum + "&nbsp;&nbsp;&nbsp;</a>" + text);
+
+    };
+
+    var loadTail = function() {
+		_load = false;
+
+        $.ajax({
+            url: "/system/console/tail",
+            data: {},
+            dataType: "json",
+            method: "GET",
+            async: true,
+            success: function(s) {
+                if(s.content) {
+                    $(".loadingstatus").data("status", "active");
+                    for(var i = 0; i < s.content.length; i++) {
+						var line = s.content[i].line;
+                        showLine(line);
+                    }
+                    if(follow) {
+			            $("html,body").scrollTop(logarea[0].scrollHeight);
+        			}
+                }
+                else {
+					$(".loadingstatus").data("status", "inactive");
+                }
+            },
+            error: function(e) {
+				$(".loadingstatus").data("status", "error");
+        	},
+            complete: function(d) {
+				_load = true;
+            }
+        });
+    };
+
+    var sendCmd = function(cmd, callback) {
+        $.ajax({
+            url: "/system/console/tail",
+            data: {command: cmd},
+            dataType: "json",
+            method: "POST",
+            async: true,
+            success: function(s) {},
+            error: function(e) {},
+            complete: function(d) {
+                if(callback) {
+                    callback();
+                }
+            }
+        });
+    }
+
+    var clearAll = function() {
+        logarea.find("span").css({"color":"black", "background-color":"white"});
+        var b = logarea[0].getElementsByTagName('b');
+
+        while(b.length) {
+            var parent = b[ 0 ].parentNode;
+            while( b[ 0 ].firstChild ) {
+                parent.insertBefore(  b[ 0 ].firstChild, b[ 0 ] );
+            }
+            parent.removeChild( b[ 0 ] );
+        }
+
+        var i = logarea[0].getElementsByTagName('i');
+
+        while(i.length) {
+            var parent = i[ 0 ].parentNode;
+            while( i[ 0 ].firstChild ) {
+                parent.insertBefore(  i[ 0 ].firstChild, i[ 0 ] );
+            }
+            parent.removeChild( i[ 0 ] );
+        }
+
+    };
+
+    $(document).ready(function(e){
+		logarea = $("#logarea");
+
+		if ($("#highlighting").length === 0) {
+	        var insertModal = $("<div>", {"class": "", "id": "highlighting", "style": "width:30rem", "title": "Highlighting"}).hide();
+			$(document.body).append(insertModal);
+			var criteria = "";
+            for(var i=0; i < searchCriteria.length; i++) {
+                criteria = criteria + "<li class='criteria-item'><div class='box'>" + searchCriteria[i]["string"] + "</div></li>";
+        	}
+
+            var content = "<div id='criteria' class='highlight-content-inner-div'><ul class='criteria-list'>" + criteria + "</ul></div>" +
+				"<div class='highlight-content-inner-div'><button class='add'>Add</button><button class='delete'>Delete</button></div>" +
+                "<div class='highlight-content-inner-div'><input id='search'>String</input></div>" +
+                "<div class='highlight-content-inner-div'><input type='checkbox' id='bold' value='off'>Bold</input> <input type='checkbox' id='italic' value='off'>Italic</input></div> " +
+                "<div class='highlight-content-inner-div'><input type='color' id='forecolor' value='#FFFFFF'>Foreground Color</input> &nbsp;<input type='color' id='backcolor' value='#ff0000'>Background Color</input></div> ";
+            $("#highlighting").append(content);
+			var buttonsArr = [];
+			buttonsArr.push({
+				 text : "OK",
+				 click : function() {
+				    clearAll();
+                    if(searchCriteria.length > 0) {
+                    	var logEntries = logarea[0].getElementsByTagName("span");
+                        for(var j=0; j<logEntries.length; j++) {
+                            var text = logEntries[j].innerHTML;
+                            for(var i=0; i < searchCriteria.length; i++) {
+                                if(text.indexOf(searchCriteria[i]["string"]) >=0) {
+                                    if(searchCriteria[i]["bold"]) {text = "<b>" + text + "</b>";}
+                                    if(searchCriteria[i]["italic"]) {text = "<i>" + text + "</i>";}
+                                    var color = "red"; if(searchCriteria[i]["forecolor"]) {color = searchCriteria[i]["forecolor"];}
+                                    var bg = ""; if(searchCriteria[i]["backcolor"]) {bg = searchCriteria[i]["backcolor"];}
+                                    logEntries[j].innerHTML = text;
+                                    logEntries[j].style.color = color;
+                                    logEntries[j].style.backgroundColor = bg;
+                                }
+                            }
+                        }
+                    }
+                    $(this).dialog("close");
+				 }
+			});
+			buttonsArr.push({
+				 text : "Cancel",
+				 click : function() {
+					$(this).dialog("close");
+				}
+			});
+
+            var modal = $("#highlighting").dialog({
+                autoOpen: false,
+                width: "30rem",
+                modal: true,
+                buttons: buttonsArr,
+                open: function(event, ui) {
+                    $(this).data("bkp-load-val", _load);
+                    if(_load) {
+                        $(".pause").click();
+                    }
+                },
+                close: function(event, ui) {
+                    if($(this).data("bkp-load-val") && !_load) {
+                        $(".pause").click();
+                    }
+                    $(this).removeData("bkp-load-val");
+                }
+            });
+
+            $("#highlighting").find(".add").click(function(e) {
+                var val = $("#highlighting").find("#search").val();
+                var color = $("#forecolor").val();
+                var bg = $("#backcolor").val();
+                var b = $("#bold").is(":checked");
+                var i = $("#italic").is(":checked");
+                var index = searchCriteria.length;
+                searchCriteria.push({"string":val, "bold":b, "italic":i, "forecolor":color, "backcolor":bg});
+                $("#highlighting").find(".criteria-list").append("<li class='criteria-item' data-index='" + index + "'><span class='box' style='color:"+color+";background-color:"+bg+";font:" + (b?" bold ":"") + (i?" italic ":"") + ";'>" + val + "</span>" + val + "</li>");
+                $("#highlighting").find("#search").val("");
+                $("#highlighting").find("#bold")[0].checked = false;
+                $("#highlighting").find("#italic")[0].checked = false;
+            });
+
+            $("#highlighting").find(".delete").click(function(e) {
+                var $selected = $("#highlighting").find(".criteria-item.selected");
+                if($selected.length == 0) return;
+
+				var index = parseInt($(e.target).data("index"));
+				searchCriteria.splice(index, 1);
+
+				$selected.remove();
+            });
+
+            $("#highlighting").on("click", ".criteria-item", function(e) {
+            	$(e.target).toggleClass("selected").siblings().removeClass("selected");
+            });
+
+		}
+
+        $(".highlighting").click(function(e) {
+			$("#highlighting").dialog("open");
+        });
+
+        $(".clear").click(function(e) {
+            logarea.empty();
+        });
+
+        $(".refresh").click(function(e) {
+			sendCmd("reset");
+            _load = false;
+            logarea.empty();
+			$("#filter").val("");
+            document.cookie = "log.tail.position=0";
+            _load = true;
+        });
+
+        $(".filter").click(function(e) {
+            var filterVal = $("#filter").val();
+            sendCmd("filter:"+filterVal);
+        });
+
+        $(".filterClear").click(function(e) {
+            $("#filter").val("");
+            $(".filter").click();
+        });
+
+		$(".tail").click(function(e) {
+            var $elem = $(e.target);
+            var currStatus = $elem.data("following");
+            $elem.data("following", !currStatus);
+            follow = $elem.data("following");
+            if(follow) {
+				$elem.attr("title", "Unfollow Tail");
+				$elem.html("Unfollow");
+            }
+            else {
+                $elem.attr("title", "Follow Tail");
+                $elem.html("Follow");
+            }
+        });
+
+        $(".sizeplus").click(function(e) {
+            logfontsize++;
+			$(".content").css("font-size", logfontsize+"px");
+        });
+
+        $(".sizeminus").click(function(e) {
+            logfontsize--;
+			$(".content").css("font-size", logfontsize+"px");
+        });
+
+        $(".pause").click(function(e) {
+            var $elem = $(e.target);
+            if(_load) {
+                $elem.attr("title", "Click to Resume");
+                $elem.html("Resume");
+
+                $(".loadingstatus").data("status", "inactive");
+                _load = false;
+	        }
+            else {
+				$elem.attr("title", "Click to Pause");
+                $elem.html("Pause");
+                _load = true;
+            }
+        });
+
+        $(".top").click(function(e) {
+			$("html,body").scrollTop(0);
+            if(follow) {
+                $(".tail").click();
+            }
+        });
+
+        $(".bottom").click(function(e) {
+			$("html,body").scrollTop(logarea[0].scrollHeight);
+            follow = true;
+        });
+
+        $(".numbering").click(function(e) {
+			var $elem = $(e.target);
+			var currStatus = $elem.data("numbers");
+			$elem.data("numbers", !currStatus);
+			_isLineNumShowing = $elem.data("numbers");
+            if(_isLineNumShowing) {
+                $(".lineNumberCol").removeClass("hide");
+                $elem.attr("title", "Hide Line Numbers");
+                $elem.html("Hide Line No.");
+            }
+            else {
+                $(".lineNumberCol").addClass("hide");
+                $elem.attr("title", "Show Line Numbers");
+                $elem.html("Show Line No.");
+            }
+        });
+
+        $(".slower").click(function(e) {
+            refreshInterval += 500;
+			$("#speed").val(refreshInterval);
+        });
+
+        $(".faster").click(function(e) {
+            if(refreshInterval >= 1500) {
+            	refreshInterval -= 500;
+            }
+			$("#speed").val(refreshInterval);
+        });
+
+        var timerFunc = function(){
+            if(_load) {
+                loadTail();
+            }
+        };
+
+        $("#speed").change(function(e) {
+			refreshInterval = parseInt($(e.target).val());
+            clearInterval(intervalObj);
+            intervalObj = setInterval(timerFunc, refreshInterval);
+        });
+
+        var intervalObj = setInterval(timerFunc, refreshInterval);
+
+        var getScrollbarWidth = function() {
+            var outer = document.createElement("div");
+            outer.style.visibility = "hidden";
+            outer.style.width = "100px";
+            outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
+
+            document.body.appendChild(outer);
+
+            var widthNoScroll = outer.offsetWidth;
+            // force scrollbars
+            outer.style.overflow = "scroll";
+
+            // add innerdiv
+            var inner = document.createElement("div");
+            inner.style.width = "100%";
+            outer.appendChild(inner);
+
+            var widthWithScroll = inner.offsetWidth;
+
+            // remove divs
+            outer.parentNode.removeChild(outer);
+
+            return widthNoScroll - widthWithScroll;
+        };
+
+        var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0) - getScrollbarWidth();
+
+        $(".pulldown").width(w);
+        $(".header").width(w);
+        $(".pulldown").click(function() {
+             if($(".header").is(":visible")) {
+                $(".header").slideUp();
+             }
+             else {
+                $(".header").slideDown();
+                $(".pulldown").attr("title", "Click to hide options");
+             }
+        });
+
+        var statusOpacity = 0.2;
+
+        setInterval(function() {
+            var status = $(".loadingstatus").data("status");
+            var color = "grey";
+            switch(status) {
+                case "error":color="red";break;
+                case "inactive":color="grey";break;
+                case "active":color="green";break;
+                default:color="grey";
+            }
+            $(".loadingstatus").find("li").css("color", color);
+            $(".loadingstatus").find("li").html("<span style='border-radius:10px;'>"+status+"</span>");
+
+            if(status == "active") {
+                $(".loadingstatus").fadeTo(1000, statusOpacity);
+                if(statusOpacity == 0.2) {
+                    statusOpacity = 1.0;
+                }
+                else {
+                    statusOpacity = 0.2;
+                }
+            }
+            else {
+                statusOpacity = 1.0;
+                $(".loadingstatus").css("opacity", statusOpacity);
+            }
+        }, 1000);
+
+        $("#logfiles").change(function() {
+            var selected = $("#logfiles").val();
+            if(selected != "") {
+                _load = false;
+                sendCmd("file:"+selected, function(){_load=true;});
+            }
+        });
+	});
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.