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