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