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