blob: a90ab989c83544ae4f32c201173ec257b174bb93 [file] [log] [blame]
// ***************************************************************************************************************************
// * 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. *
// ***************************************************************************************************************************
package org.apache.juneau.microservice.resources;
import static javax.servlet.http.HttpServletResponse.*;
import static org.apache.juneau.html.HtmlDocSerializer.*;
import static org.apache.juneau.rest.annotation.HookEvent.*;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.http.HttpMethodName.*;
import java.io.*;
import java.net.URI;
import java.nio.charset.*;
import java.util.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.config.*;
import org.apache.juneau.dto.LinkString;
import org.apache.juneau.rest.*;
import org.apache.juneau.rest.annotation.*;
import org.apache.juneau.rest.converters.*;
import org.apache.juneau.transforms.*;
/**
* REST resource for viewing and accessing log files.
*/
@RestResource(
path="/logs",
title="Log files",
description="Log files from this service",
properties={
@Property(name=HTML_uriAnchorText, value="PROPERTY_NAME"),
},
allowedMethodParams="*",
pojoSwaps={
IteratorSwap.class, // Allows Iterators and Iterables to be serialized.
DateSwap.ISO8601DT.class // Serialize Date objects as ISO8601 strings.
}
)
public class LogsResource extends BasicRestServlet {
private static final long serialVersionUID = 1L;
private File logDir;
private LogEntryFormatter leFormatter;
private final FileFilter filter = new FileFilter() {
@Override /* FileFilter */
public boolean accept(File f) {
return f.isDirectory() || f.getName().endsWith(".log");
}
};
/**
* Initializes the log directory and formatter.
*
* @param builder The resource config.
* @throws Exception
*/
@RestHook(INIT)
public void init(RestContextBuilder builder) throws Exception {
Config c = builder.getConfig();
logDir = new File(c.getString("Logging/logDir", "."));
leFormatter = new LogEntryFormatter(
c.getString("Logging/format", "[{date} {level}] {msg}%n"),
c.getString("Logging/dateFormat", "yyyy.MM.dd hh:mm:ss"),
c.getBoolean("Logging/useStackTraceHashes")
);
}
/**
* [GET /*] - Get file details or directory listing.
*
* @param req The HTTP request
* @param res The HTTP response
* @param properties The writable properties for setting the descriptions.
* @param path The log file path.
* @return The log file.
* @throws Exception
*/
@RestMethod(
name=GET,
path="/*",
swagger=@MethodSwagger(
responses={@Response(200),@Response(404)}
)
)
public Object getFileOrDirectory(RestRequest req, RestResponse res, RequestProperties properties, @PathRemainder String path) throws Exception {
File f = getFile(path);
if (f.isDirectory()) {
Set<FileResource> l = new TreeSet<>(new FileResourceComparator());
File[] files = f.listFiles(filter);
if (files != null) {
for (File fc : files) {
URI fUrl = new URI("servlet:/" + fc.getName());
l.add(new FileResource(fc, fUrl));
}
}
return l;
}
return new FileResource(f, new URI("servlet:/"));
}
/**
* [VIEW /*] - Retrieve the contents of a log file.
*
* @param req The HTTP request.
* @param res The HTTP response.
* @param path The log file path.
* @param properties The writable properties for setting the descriptions.
* @param highlight If <code>true</code>, add color highlighting based on severity.
* @param start Optional start timestamp. Don't print lines logged before the specified timestamp. Example: "&amp;start=2014-01-23 11:25:47".
* @param end Optional end timestamp. Don't print lines logged after the specified timestamp. Example: "&amp;end=2014-01-23 11:25:47".
* @param thread Optional thread name filter. Only show log entries with the specified thread name. Example: "&amp;thread=pool-33-thread-1".
* @param loggers Optional logger filter. Only show log entries if they were produced by one of the specified loggers (simple class name). Example: "&amp;loggers=(LinkIndexService,LinkIndexRestService)".
* @param severity Optional severity filter. Only show log entries with the specified severity. Example: "&amp;severity=(ERROR,WARN)".
* @throws Exception
*/
@RestMethod(
name="VIEW",
path="/*",
swagger=@MethodSwagger(
responses={@Response(200),@Response(404)}
)
)
public void viewFile(RestRequest req, RestResponse res, @PathRemainder String path, RequestProperties properties, @Query("highlight") boolean highlight, @Query("start") String start, @Query("end") String end, @Query("thread") String thread, @Query("loggers") String[] loggers, @Query("severity") String[] severity) throws Exception {
File f = getFile(path);
if (f.isDirectory())
throw new RestException(SC_METHOD_NOT_ALLOWED, "View not available on directories");
Date startDate = parseISO8601Date(start), endDate = parseISO8601Date(end);
if (! highlight) {
Object o = getReader(f, startDate, endDate, thread, loggers, severity);
res.setContentType("text/plain");
if (o instanceof Reader)
res.setOutput(o);
else {
try (LogParser p = (LogParser)o; Writer w = res.getNegotiatedWriter()) {
p.writeTo(w);
}
}
return;
}
res.setContentType("text/html");
try (PrintWriter w = res.getNegotiatedWriter()) {
w.println("<html><body style='font-family:monospace;font-size:8pt;white-space:pre;'>");
try (LogParser lp = getLogParser(f, startDate, endDate, thread, loggers, severity)) {
if (! lp.hasNext())
w.append("<span style='color:gray'>[EMPTY]</span>");
else for (LogParser.Entry le : lp) {
char s = le.severity.charAt(0);
String color = "black";
//SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST
if (s == 'I')
color = "#006400";
else if (s == 'W')
color = "#CC8400";
else if (s == 'E' || s == 'S')
color = "#DD0000";
else if (s == 'D' || s == 'F' || s == 'T')
color = "#000064";
w.append("<span style='color:").append(color).append("'>");
le.appendHtml(w).append("</span>");
}
w.append("</body></html>");
}
}
}
/**
* [VIEW /*] - Retrieve the contents of a log file as parsed entries.
*
* @param req The HTTP request.
* @param path The log file path.
* @param start Optional start timestamp. Don't print lines logged before the specified timestamp. Example: "&amp;start=2014-01-23 11:25:47".
* @param end Optional end timestamp. Don't print lines logged after the specified timestamp. Example: "&amp;end=2014-01-23 11:25:47".
* @param thread Optional thread name filter. Only show log entries with the specified thread name. Example: "&amp;thread=pool-33-thread-1".
* @param loggers Optional logger filter. Only show log entries if they were produced by one of the specified loggers (simple class name). Example: "&amp;loggers=(LinkIndexService,LinkIndexRestService)".
* @param severity Optional severity filter. Only show log entries with the specified severity. Example: "&amp;severity=(ERROR,WARN)".
* @return The parsed contents of the log file.
* @throws Exception
*/
@RestMethod(
name="PARSE",
path="/*",
converters=Queryable.class,
swagger=@MethodSwagger(
responses={@Response(200),@Response(404)}
)
)
public LogParser viewParsedEntries(RestRequest req, @PathRemainder String path, @Query("start") String start, @Query("end") String end, @Query("thread") String thread, @Query("loggers") String[] loggers, @Query("severity") String[] severity) throws Exception {
File f = getFile(path);
Date startDate = parseISO8601Date(start), endDate = parseISO8601Date(end);
if (f.isDirectory())
throw new RestException(SC_METHOD_NOT_ALLOWED, "View not available on directories");
return getLogParser(f, startDate, endDate, thread, loggers, severity);
}
/**
* [DOWNLOAD /*] - Download file.
*
* @param res The HTTP response.
* @param path The log file path.
* @return The contents of the log file.
* @throws Exception
*/
@RestMethod(
name="DOWNLOAD",
path="/*",
swagger=@MethodSwagger(
responses={@Response(200),@Response(404)}
)
)
public Object downloadFile(RestResponse res, @PathRemainder String path) throws Exception {
File f = getFile(path);
if (f.isDirectory())
throw new RestException(SC_METHOD_NOT_ALLOWED, "Download not available on directories");
res.setContentType("application/octet-stream");
res.setContentLength((int)f.length());
return new FileInputStream(f);
}
/**
* [DELETE /*] - Delete a file.
*
* @param path The log file path.
* @return A redirect object to the root.
* @throws Exception
*/
@RestMethod(
name=DELETE,
path="/*",
swagger=@MethodSwagger(
responses={@Response(200),@Response(404)}
)
)
public Object deleteFile(@PathRemainder String path) throws Exception {
File f = getFile(path);
if (f.isDirectory())
throw new RestException(SC_BAD_REQUEST, "Delete not available on directories.");
if (f.canWrite())
if (! f.delete())
throw new RestException(SC_FORBIDDEN, "Could not delete file.");
return new Redirect(path + "/..");
}
private static BufferedReader getReader(File f) throws IOException {
return new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset()));
}
private File getFile(String path) {
if (path != null && path.indexOf("..") != -1)
throw new RestException(SC_NOT_FOUND, "File not found.");
File f = (path == null ? logDir : new File(logDir.getAbsolutePath() + '/' + path));
if (filter.accept(f))
return f;
throw new RestException(SC_NOT_FOUND, "File not found.");
}
/**
* File bean.
*/
@SuppressWarnings("javadoc")
public static class FileResource {
final File f;
public final String type;
public final Object name;
public final Long size;
@Swap(DateSwap.DateTimeMedium.class) public Date lastModified;
public URI view, highlighted, parsed, download, delete;
public FileResource(File f, URI uri) throws Exception {
this.f = f;
this.type = (f.isDirectory() ? "dir" : "file");
this.name = f.isDirectory() ? new LinkString(f.getName(), uri.toString()) : f.getName();
this.size = f.isDirectory() ? null : f.length();
this.lastModified = new Date(f.lastModified());
if (f.canRead() && ! f.isDirectory()) {
this.view = new URI(uri + "?method=VIEW");
this.highlighted = new URI(uri + "?method=VIEW&highlight=true");
this.parsed = new URI(uri + "?method=PARSE");
this.download = new URI(uri + "?method=DOWNLOAD");
this.delete = new URI(uri + "?method=DELETE");
}
}
}
static final class FileResourceComparator implements Comparator<FileResource>, Serializable {
private static final long serialVersionUID = 1L;
@Override /* Comparator */
public int compare(FileResource o1, FileResource o2) {
int c = o1.type.compareTo(o2.type);
return c != 0 ? c : o1.f.getName().compareTo(o2.f.getName());
}
}
private Object getReader(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException {
if (start == null && end == null && thread == null && loggers == null)
return getReader(f);
return getLogParser(f, start, end, thread, loggers, severity);
}
private LogParser getLogParser(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException {
return new LogParser(leFormatter, f, start, end, thread, loggers, severity);
}
}