001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.microservice.resources; 018 019import static org.apache.juneau.common.utils.StringUtils.*; 020 021import java.io.*; 022import java.nio.charset.*; 023import java.util.*; 024 025import org.apache.juneau.annotation.*; 026import org.apache.juneau.bean.*; 027import org.apache.juneau.config.*; 028import org.apache.juneau.html.annotation.*; 029import org.apache.juneau.http.annotation.*; 030import org.apache.juneau.http.response.*; 031import org.apache.juneau.rest.*; 032import org.apache.juneau.rest.annotation.*; 033import org.apache.juneau.rest.beans.*; 034import org.apache.juneau.rest.converter.*; 035import org.apache.juneau.rest.servlet.*; 036 037/** 038 * REST resource for viewing and accessing log files. 039 */ 040@Rest( 041 path="/logs", 042 title="Log files", 043 description="Log files from this service", 044 allowedMethodParams="*" 045) 046@HtmlConfig(uriAnchorText="PROPERTY_NAME") 047@SuppressWarnings("javadoc") 048public class LogsResource extends BasicRestServlet { 049 private static final long serialVersionUID = 1L; 050 051 //------------------------------------------------------------------------------------------------------------------- 052 // Instance 053 //------------------------------------------------------------------------------------------------------------------- 054 055 private File logDir; 056 private LogEntryFormatter leFormatter; 057 boolean allowDeletes; 058 059 @RestInit 060 public void init(Config config) throws Exception { 061 logDir = new File(config.get("Logging/logDir").asString().orElse("logs")); 062 allowDeletes = config.get("Logging/allowDeletes").asBoolean().orElse(true); 063 leFormatter = new LogEntryFormatter( 064 config.get("Logging/format").asString().orElse("[{date} {level}] {msg}%n"), 065 config.get("Logging/dateFormat").asString().orElse("yyyy.MM.dd hh:mm:ss"), 066 config.get("Logging/useStackTraceHashes").asBoolean().orElse(true) 067 ); 068 } 069 070 @RestGet( 071 path="/*", 072 summary="View information on file or directory", 073 description="Returns information about the specified file or directory." 074 ) 075 @HtmlDocConfig( 076 nav={"<h5>Folder: $RA{fullPath}</h5>"} 077 ) 078 public FileResource getFile(RestRequest req, @Path("/*") String path) throws NotFound, Exception { 079 080 File dir = getFile(path); 081 req.setAttribute("fullPath", dir.getAbsolutePath()); 082 083 return new FileResource(dir, path, allowDeletes, true); 084 } 085 086 @RestOp( 087 method="VIEW", 088 path="/*", 089 summary="View contents of log file", 090 description="View the contents of a log file." 091 ) 092 public void viewFile( 093 RestResponse res, 094 @Path("/*") String path, 095 @Query(name="highlight", schema=@Schema(d="Add severity color highlighting.")) boolean highlight, 096 @Query(name="start", schema=@Schema(d="Start timestamp (ISO8601, full or partial).\nDon't print lines logged before the specified timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS")) String start, 097 @Query(name="end", schema=@Schema(d="End timestamp (ISO8601, full or partial).\nDon't print lines logged after the specified timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS")) String end, 098 @Query(name="thread", schema=@Schema(d="Thread name filter.\nOnly show log entries with the specified thread name.")) String thread, 099 @Query(name="loggers", schema=@Schema(d="Logger filter (simple class name).\nOnly show log entries if they were produced by one of the specified loggers.")) String[] loggers, 100 @Query(name="severity",schema=@Schema( d="Severity filter.\nOnly show log entries with the specified severity.")) String[] severity 101 ) throws NotFound, MethodNotAllowed, IOException { 102 103 File f = getFile(path); 104 105 Date startDate = parseIsoDate(start), endDate = parseIsoDate(end); 106 107 if (! highlight) { 108 Object o = getReader(f, startDate, endDate, thread, loggers, severity); 109 res.setContentType("text/plain"); 110 if (o instanceof Reader) 111 res.setContent(o); 112 else { 113 try (LogParser p = (LogParser)o; Writer w = res.getNegotiatedWriter()) { 114 p.writeTo(w); 115 } 116 } 117 return; 118 } 119 120 res.setContentType("text/html"); 121 try (PrintWriter w = res.getNegotiatedWriter()) { 122 w.println("<html><body style='font-family:monospace;font-size:8pt;white-space:pre;'>"); 123 try (LogParser lp = getLogParser(f, startDate, endDate, thread, loggers, severity)) { 124 if (! lp.hasNext()) 125 w.append("<span style='color:gray'>[EMPTY]</span>"); 126 else for (LogParser.Entry le : lp) { 127 char s = le.severity.charAt(0); 128 String color = "black"; 129 //SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST 130 if (s == 'I') 131 color = "#006400"; 132 else if (s == 'W') 133 color = "#CC8400"; 134 else if (s == 'E' || s == 'S') 135 color = "#DD0000"; 136 else if (s == 'D' || s == 'F' || s == 'T') 137 color = "#000064"; 138 w.append("<span style='color:").append(color).append("'>"); 139 le.appendHtml(w).append("</span>"); 140 } 141 w.append("</body></html>"); 142 } 143 } 144 } 145 146 @RestOp( 147 method="PARSE", 148 path="/*", 149 converters=Queryable.class, 150 summary="View parsed contents of file", 151 description="View the parsed contents of a file.", 152 swagger=@OpSwagger( 153 parameters={ 154 Queryable.SWAGGER_PARAMS 155 } 156 ) 157 ) 158 @HtmlDocConfig( 159 nav={"<h5>Folder: $RA{fullPath}</h5>"} 160 ) 161 public LogParser viewParsedEntries( 162 RestRequest req, 163 @Path("/*") String path, 164 @Query(name="start", schema=@Schema(d="Start timestamp (ISO8601, full or partial).\nDon't print lines logged before the specified timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS")) String start, 165 @Query(name="end", schema=@Schema(d="End timestamp (ISO8601, full or partial).\nDon't print lines logged after the specified timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss, yyyy-MM-ddThh:mm:ss.SSS")) String end, 166 @Query(name="thread", schema=@Schema(d="Thread name filter.\nOnly show log entries with the specified thread name.")) String thread, 167 @Query(name="loggers", schema=@Schema(d="Logger filter (simple class name).\nOnly show log entries if they were produced by one of the specified loggers.")) String[] loggers, 168 @Query(name="severity", schema=@Schema(d="Severity filter.\nOnly show log entries with the specified severity.")) String[] severity 169 ) throws NotFound, IOException { 170 171 File f = getFile(path); 172 req.setAttribute("fullPath", f.getAbsolutePath()); 173 174 Date startDate = parseIsoDate(start), endDate = parseIsoDate(end); 175 176 return getLogParser(f, startDate, endDate, thread, loggers, severity); 177 } 178 179 @RestOp( 180 method="DOWNLOAD", 181 path="/*", 182 summary="Download file", 183 description="Download the contents of a file.\nContent-Type is set to 'application/octet-stream'." 184 ) 185 public FileContents downloadFile(RestResponse res, @Path("/*") String path) throws NotFound, MethodNotAllowed { 186 res.setContentType("application/octet-stream"); 187 try { 188 return new FileContents(getFile(path)); 189 } catch (FileNotFoundException e) { 190 throw new NotFound("File not found"); 191 } 192 } 193 194 @RestDelete( 195 path="/*", 196 summary="Delete log file", 197 description="Delete a log file on the file system." 198 ) 199 public RedirectToRoot deleteFile(@Path("/*") String path) throws MethodNotAllowed { 200 deleteFile(getFile(path)); 201 return new RedirectToRoot(); 202 } 203 204 205 //----------------------------------------------------------------------------------------------------------------- 206 // Helper beans 207 //----------------------------------------------------------------------------------------------------------------- 208 209 @Response(schema=@Schema(type="string",format="binary",description="Contents of file")) 210 static class FileContents extends FileInputStream { 211 public FileContents(File file) throws FileNotFoundException { 212 super(file); 213 } 214 } 215 216 @Response(schema=@Schema(description="Redirect to root page on success")) 217 static class RedirectToRoot extends SeeOtherRoot {} 218 219 @Response(schema=@Schema(description="File action")) 220 public static class Action extends LinkString { 221 public Action(String name, String uri, Object...uriArgs) { 222 super(name, uri, uriArgs); 223 } 224 } 225 226 @Response(schema=@Schema(description="File or directory details")) 227 @Bean(properties="type,name,size,lastModified,actions,files") 228 public static class FileResource { 229 private final File f; 230 private final String path; 231 private final String uri; 232 private final boolean includeChildren, allowDeletes; 233 234 public FileResource(File f, String path, boolean allowDeletes, boolean includeChildren) { 235 this.f = f; 236 this.path = path; 237 this.uri = "servlet:/"+(path == null ? "" : path); 238 this.includeChildren = includeChildren; 239 this.allowDeletes = allowDeletes; 240 } 241 242 public String getType() { 243 return (f.isDirectory() ? "dir" : "file"); 244 } 245 246 public LinkString getName() { 247 return new LinkString(f.getName(), uri); 248 } 249 250 public long getSize() { 251 return f.isDirectory() ? f.listFiles().length : f.length(); 252 } 253 254 public Date getLastModified() { 255 return new Date(f.lastModified()); 256 } 257 258 @Html(format=HtmlFormat.HTML_CDC) 259 public List<Action> getActions() throws Exception { 260 List<Action> l = new ArrayList<>(); 261 if (f.canRead() && ! f.isDirectory()) { 262 l.add(new Action("view", uri + "?method=VIEW")); 263 l.add(new Action("highlighted", uri + "?method=VIEW&highlight=true")); 264 l.add(new Action("parsed", uri + "?method=PARSE")); 265 l.add(new Action("download", uri + "?method=DOWNLOAD")); 266 if (allowDeletes) 267 l.add(new Action("delete", uri + "?method=DELETE")); 268 } 269 return l; 270 } 271 272 public Set<FileResource> getFiles() { 273 if (f.isFile() || ! includeChildren) 274 return null; 275 Set<FileResource> s = new TreeSet<>(FILE_COMPARATOR); 276 for (File fc : f.listFiles(FILE_FILTER)) 277 s.add(new FileResource(fc, (path != null ? (path + '/') : "") + urlEncode(fc.getName()), allowDeletes, false)); 278 return s; 279 } 280 281 static final FileFilter FILE_FILTER = f -> f.isDirectory() || f.getName().endsWith(".log"); 282 283 static final Comparator<FileResource> FILE_COMPARATOR = (o1, o2) -> { 284 int c = o1.getType().compareTo(o2.getType()); 285 return c != 0 ? c : o1.getName().compareTo(o2.getName()); 286 }; 287 } 288 289 290 //----------------------------------------------------------------------------------------------------------------- 291 // Helper methods 292 //----------------------------------------------------------------------------------------------------------------- 293 294 private File getFile(String path) throws NotFound { 295 if (path == null) 296 return logDir; 297 File f = new File(logDir.getAbsolutePath() + '/' + path); 298 if (f.exists()) 299 return f; 300 throw new NotFound("File not found."); 301 } 302 303 private void deleteFile(File f) { 304 if (! allowDeletes) 305 throw new MethodNotAllowed("DELETE not enabled"); 306 if (f.isDirectory()) { 307 File[] files = f.listFiles(); 308 if (files != null) { 309 for (File fc : files) 310 deleteFile(fc); 311 } 312 } 313 if (! f.delete()) 314 throw new Forbidden("Could not delete file {0}", f.getAbsolutePath()) ; 315 } 316 317 private static BufferedReader getReader(File f) throws IOException { 318 return new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset())); 319 } 320 321 private Object getReader(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException { 322 if (start == null && end == null && thread == null && loggers == null) 323 return getReader(f); 324 return getLogParser(f, start, end, thread, loggers, severity); 325 } 326 327 private LogParser getLogParser(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException { 328 return new LogParser(leFormatter, f, start, end, thread, loggers, severity); 329 } 330}