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
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\"> </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;
+ }
+}
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 + "'> " + _lineNum + " </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> <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