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}