001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.microservice.resources; 014 015import static javax.servlet.http.HttpServletResponse.*; 016import static org.apache.juneau.html.HtmlDocSerializer.*; 017import static org.apache.juneau.rest.annotation.HookEvent.*; 018import static org.apache.juneau.internal.StringUtils.*; 019import static org.apache.juneau.http.HttpMethodName.*; 020 021import java.io.*; 022import java.net.URI; 023import java.nio.charset.*; 024import java.util.*; 025 026import org.apache.juneau.annotation.*; 027import org.apache.juneau.config.*; 028import org.apache.juneau.dto.LinkString; 029import org.apache.juneau.rest.*; 030import org.apache.juneau.rest.annotation.*; 031import org.apache.juneau.rest.converters.*; 032import org.apache.juneau.transforms.*; 033 034/** 035 * REST resource for viewing and accessing log files. 036 */ 037@RestResource( 038 path="/logs", 039 title="Log files", 040 description="Log files from this service", 041 properties={ 042 @Property(name=HTML_uriAnchorText, value="PROPERTY_NAME"), 043 }, 044 allowedMethodParams="*", 045 pojoSwaps={ 046 IteratorSwap.class, // Allows Iterators and Iterables to be serialized. 047 DateSwap.ISO8601DT.class // Serialize Date objects as ISO8601 strings. 048 } 049) 050public class LogsResource extends BasicRestServlet { 051 private static final long serialVersionUID = 1L; 052 053 private File logDir; 054 private LogEntryFormatter leFormatter; 055 056 private final FileFilter filter = new FileFilter() { 057 @Override /* FileFilter */ 058 public boolean accept(File f) { 059 return f.isDirectory() || f.getName().endsWith(".log"); 060 } 061 }; 062 063 /** 064 * Initializes the log directory and formatter. 065 * 066 * @param builder The resource config. 067 * @throws Exception 068 */ 069 @RestHook(INIT) 070 public void init(RestContextBuilder builder) throws Exception { 071 Config c = builder.getConfig(); 072 073 logDir = new File(c.getString("Logging/logDir", ".")); 074 leFormatter = new LogEntryFormatter( 075 c.getString("Logging/format", "[{date} {level}] {msg}%n"), 076 c.getString("Logging/dateFormat", "yyyy.MM.dd hh:mm:ss"), 077 c.getBoolean("Logging/useStackTraceHashes") 078 ); 079 } 080 081 /** 082 * [GET /*] - Get file details or directory listing. 083 * 084 * @param req The HTTP request 085 * @param res The HTTP response 086 * @param properties The writable properties for setting the descriptions. 087 * @param path The log file path. 088 * @return The log file. 089 * @throws Exception 090 */ 091 @RestMethod( 092 name=GET, 093 path="/*", 094 swagger= { 095 "responses:{", 096 "200: {description:'OK'},", 097 "404: {description:'Not Found'}", 098 "}" 099 } 100 ) 101 public Object getFileOrDirectory(RestRequest req, RestResponse res, RequestProperties properties, @PathRemainder String path) throws Exception { 102 103 File f = getFile(path); 104 105 if (f.isDirectory()) { 106 Set<FileResource> l = new TreeSet<>(new FileResourceComparator()); 107 File[] files = f.listFiles(filter); 108 if (files != null) { 109 for (File fc : files) { 110 URI fUrl = new URI("servlet:/" + fc.getName()); 111 l.add(new FileResource(fc, fUrl)); 112 } 113 } 114 return l; 115 } 116 117 return new FileResource(f, new URI("servlet:/")); 118 } 119 120 /** 121 * [VIEW /*] - Retrieve the contents of a log file. 122 * 123 * @param req The HTTP request. 124 * @param res The HTTP response. 125 * @param path The log file path. 126 * @param properties The writable properties for setting the descriptions. 127 * @param highlight If <code>true</code>, add color highlighting based on severity. 128 * @param start Optional start timestamp. Don't print lines logged before the specified timestamp. Example: "&start=2014-01-23 11:25:47". 129 * @param end Optional end timestamp. Don't print lines logged after the specified timestamp. Example: "&end=2014-01-23 11:25:47". 130 * @param thread Optional thread name filter. Only show log entries with the specified thread name. Example: "&thread=pool-33-thread-1". 131 * @param loggers Optional logger filter. Only show log entries if they were produced by one of the specified loggers (simple class name). Example: "&loggers=(LinkIndexService,LinkIndexRestService)". 132 * @param severity Optional severity filter. Only show log entries with the specified severity. Example: "&severity=(ERROR,WARN)". 133 * @throws Exception 134 */ 135 @RestMethod( 136 name="VIEW", 137 path="/*", 138 swagger= { 139 "responses:{", 140 "200: {description:'OK'},", 141 "404: {description:'Not Found'}", 142 "}" 143 } 144 ) 145 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 { 146 147 File f = getFile(path); 148 if (f.isDirectory()) 149 throw new RestException(SC_METHOD_NOT_ALLOWED, "View not available on directories"); 150 151 Date startDate = parseISO8601Date(start), endDate = parseISO8601Date(end); 152 153 if (! highlight) { 154 Object o = getReader(f, startDate, endDate, thread, loggers, severity); 155 res.setContentType("text/plain"); 156 if (o instanceof Reader) 157 res.setOutput(o); 158 else { 159 try (LogParser p = (LogParser)o; Writer w = res.getNegotiatedWriter()) { 160 p.writeTo(w); 161 } 162 } 163 return; 164 } 165 166 res.setContentType("text/html"); 167 try (PrintWriter w = res.getNegotiatedWriter()) { 168 w.println("<html><body style='font-family:monospace;font-size:8pt;white-space:pre;'>"); 169 try (LogParser lp = getLogParser(f, startDate, endDate, thread, loggers, severity)) { 170 if (! lp.hasNext()) 171 w.append("<span style='color:gray'>[EMPTY]</span>"); 172 else for (LogParser.Entry le : lp) { 173 char s = le.severity.charAt(0); 174 String color = "black"; 175 //SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST 176 if (s == 'I') 177 color = "#006400"; 178 else if (s == 'W') 179 color = "#CC8400"; 180 else if (s == 'E' || s == 'S') 181 color = "#DD0000"; 182 else if (s == 'D' || s == 'F' || s == 'T') 183 color = "#000064"; 184 w.append("<span style='color:").append(color).append("'>"); 185 le.appendHtml(w).append("</span>"); 186 } 187 w.append("</body></html>"); 188 } 189 } 190 } 191 192 /** 193 * [VIEW /*] - Retrieve the contents of a log file as parsed entries. 194 * 195 * @param req The HTTP request. 196 * @param path The log file path. 197 * @param start Optional start timestamp. Don't print lines logged before the specified timestamp. Example: "&start=2014-01-23 11:25:47". 198 * @param end Optional end timestamp. Don't print lines logged after the specified timestamp. Example: "&end=2014-01-23 11:25:47". 199 * @param thread Optional thread name filter. Only show log entries with the specified thread name. Example: "&thread=pool-33-thread-1". 200 * @param loggers Optional logger filter. Only show log entries if they were produced by one of the specified loggers (simple class name). Example: "&loggers=(LinkIndexService,LinkIndexRestService)". 201 * @param severity Optional severity filter. Only show log entries with the specified severity. Example: "&severity=(ERROR,WARN)". 202 * @return The parsed contents of the log file. 203 * @throws Exception 204 */ 205 @RestMethod( 206 name="PARSE", 207 path="/*", 208 converters=Queryable.class, 209 swagger= { 210 "responses:{", 211 "200: {description:'OK'},", 212 "404: {description:'Not Found'}", 213 "}" 214 } 215 ) 216 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 { 217 218 File f = getFile(path); 219 Date startDate = parseISO8601Date(start), endDate = parseISO8601Date(end); 220 221 if (f.isDirectory()) 222 throw new RestException(SC_METHOD_NOT_ALLOWED, "View not available on directories"); 223 224 return getLogParser(f, startDate, endDate, thread, loggers, severity); 225 } 226 227 /** 228 * [DOWNLOAD /*] - Download file. 229 * 230 * @param res The HTTP response. 231 * @param path The log file path. 232 * @return The contents of the log file. 233 * @throws Exception 234 */ 235 @RestMethod( 236 name="DOWNLOAD", 237 path="/*", 238 swagger= { 239 "responses:{", 240 "200: {description:'OK'},", 241 "404: {description:'Not Found'}", 242 "}" 243 } 244 ) 245 public Object downloadFile(RestResponse res, @PathRemainder String path) throws Exception { 246 247 File f = getFile(path); 248 249 if (f.isDirectory()) 250 throw new RestException(SC_METHOD_NOT_ALLOWED, "Download not available on directories"); 251 252 res.setContentType("application/octet-stream"); 253 res.setContentLength((int)f.length()); 254 return new FileInputStream(f); 255 } 256 257 /** 258 * [DELETE /*] - Delete a file. 259 * 260 * @param path The log file path. 261 * @return A redirect object to the root. 262 * @throws Exception 263 */ 264 @RestMethod( 265 name=DELETE, 266 path="/*", 267 swagger= { 268 "responses:{", 269 "200: {description:'OK'},", 270 "404: {description:'Not Found'}", 271 "}" 272 } 273 ) 274 public Object deleteFile(@PathRemainder String path) throws Exception { 275 276 File f = getFile(path); 277 278 if (f.isDirectory()) 279 throw new RestException(SC_BAD_REQUEST, "Delete not available on directories."); 280 281 if (f.canWrite()) 282 if (! f.delete()) 283 throw new RestException(SC_FORBIDDEN, "Could not delete file."); 284 285 return new Redirect(path + "/.."); 286 } 287 288 private static BufferedReader getReader(File f) throws IOException { 289 return new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset())); 290 } 291 292 private File getFile(String path) { 293 if (path != null && path.indexOf("..") != -1) 294 throw new RestException(SC_NOT_FOUND, "File not found."); 295 File f = (path == null ? logDir : new File(logDir.getAbsolutePath() + '/' + path)); 296 if (filter.accept(f)) 297 return f; 298 throw new RestException(SC_NOT_FOUND, "File not found."); 299 } 300 301 /** 302 * File bean. 303 */ 304 @SuppressWarnings("javadoc") 305 public static class FileResource { 306 final File f; 307 public final String type; 308 public final Object name; 309 public final Long size; 310 @Swap(DateSwap.DateTimeMedium.class) public Date lastModified; 311 public URI view, highlighted, parsed, download, delete; 312 313 public FileResource(File f, URI uri) throws Exception { 314 this.f = f; 315 this.type = (f.isDirectory() ? "dir" : "file"); 316 this.name = f.isDirectory() ? new LinkString(f.getName(), uri.toString()) : f.getName(); 317 this.size = f.isDirectory() ? null : f.length(); 318 this.lastModified = new Date(f.lastModified()); 319 if (f.canRead() && ! f.isDirectory()) { 320 this.view = new URI(uri + "?method=VIEW"); 321 this.highlighted = new URI(uri + "?method=VIEW&highlight=true"); 322 this.parsed = new URI(uri + "?method=PARSE"); 323 this.download = new URI(uri + "?method=DOWNLOAD"); 324 this.delete = new URI(uri + "?method=DELETE"); 325 } 326 } 327 } 328 329 static final class FileResourceComparator implements Comparator<FileResource>, Serializable { 330 private static final long serialVersionUID = 1L; 331 @Override /* Comparator */ 332 public int compare(FileResource o1, FileResource o2) { 333 int c = o1.type.compareTo(o2.type); 334 return c != 0 ? c : o1.f.getName().compareTo(o2.f.getName()); 335 } 336 } 337 338 private Object getReader(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException { 339 if (start == null && end == null && thread == null && loggers == null) 340 return getReader(f); 341 return getLogParser(f, start, end, thread, loggers, severity); 342 } 343 344 private LogParser getLogParser(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException { 345 return new LogParser(leFormatter, f, start, end, thread, loggers, severity); 346 } 347}