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