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 javax.servlet.http.HttpServletResponse.*;
016import static org.apache.juneau.html.HtmlDocSerializer.*;
017import static org.apache.juneau.rest.annotation.HookEvent.*;
018import static org.apache.juneau.internal.StringUtils.*;
019import static org.apache.juneau.http.HttpMethodName.*;
020
021import java.io.*;
022import java.net.URI;
023import java.nio.charset.*;
024import java.util.*;
025
026import org.apache.juneau.annotation.*;
027import org.apache.juneau.config.*;
028import org.apache.juneau.dto.LinkString;
029import org.apache.juneau.rest.*;
030import org.apache.juneau.rest.annotation.*;
031import org.apache.juneau.rest.converters.*;
032import org.apache.juneau.transforms.*;
033
034/**
035 * REST resource for viewing and accessing log files.
036 */
037@RestResource(
038   path="/logs",
039   title="Log files",
040   description="Log files from this service",
041   properties={
042      @Property(name=HTML_uriAnchorText, value="PROPERTY_NAME"),
043   },
044   allowedMethodParams="*",
045   pojoSwaps={
046      IteratorSwap.class,       // Allows Iterators and Iterables to be serialized.
047      DateSwap.ISO8601DT.class  // Serialize Date objects as ISO8601 strings.
048   }
049)
050public class LogsResource extends BasicRestServlet {
051   private static final long serialVersionUID = 1L;
052
053   private File logDir;
054   private LogEntryFormatter leFormatter;
055
056   private final FileFilter filter = new FileFilter() {
057      @Override /* FileFilter */
058      public boolean accept(File f) {
059         return f.isDirectory() || f.getName().endsWith(".log");
060      }
061   };
062
063   /**
064    * Initializes the log directory and formatter.
065    * 
066    * @param builder The resource config.
067    * @throws Exception
068    */
069   @RestHook(INIT) 
070   public void init(RestContextBuilder builder) throws Exception {
071      Config c = builder.getConfig();
072
073      logDir = new File(c.getString("Logging/logDir", "."));
074      leFormatter = new LogEntryFormatter(
075         c.getString("Logging/format", "[{date} {level}] {msg}%n"),
076         c.getString("Logging/dateFormat", "yyyy.MM.dd hh:mm:ss"),
077         c.getBoolean("Logging/useStackTraceHashes")
078      );
079   }
080
081   /**
082    * [GET /*] - Get file details or directory listing.
083    * 
084    * @param req The HTTP request
085    * @param res The HTTP response
086    * @param properties The writable properties for setting the descriptions.
087    * @param path The log file path.
088    * @return The log file.
089    * @throws Exception
090    */
091   @RestMethod(
092      name=GET,
093      path="/*",
094      swagger= {
095         "responses:{",
096            "200: {description:'OK'},",
097            "404: {description:'Not Found'}",
098         "}"
099      }
100   )
101   public Object getFileOrDirectory(RestRequest req, RestResponse res, RequestProperties properties, @PathRemainder String path) throws Exception {
102
103      File f = getFile(path);
104
105      if (f.isDirectory()) {
106         Set<FileResource> l = new TreeSet<>(new FileResourceComparator());
107         File[] files = f.listFiles(filter);
108         if (files != null) {
109            for (File fc : files) {
110               URI fUrl = new URI("servlet:/" + fc.getName());
111               l.add(new FileResource(fc, fUrl));
112            }
113         }
114         return l;
115      }
116
117      return new FileResource(f, new URI("servlet:/"));
118   }
119
120   /**
121    * [VIEW /*] - Retrieve the contents of a log file.
122    * 
123    * @param req The HTTP request.
124    * @param res The HTTP response.
125    * @param path The log file path.
126    * @param properties The writable properties for setting the descriptions.
127    * @param highlight If <code>true</code>, add color highlighting based on severity.
128    * @param start Optional start timestamp.  Don't print lines logged before the specified timestamp.  Example:  "&amp;start=2014-01-23 11:25:47".
129    * @param end Optional end timestamp.  Don't print lines logged after the specified timestamp.  Example:  "&amp;end=2014-01-23 11:25:47".
130    * @param thread Optional thread name filter.  Only show log entries with the specified thread name.  Example: "&amp;thread=pool-33-thread-1".
131    * @param loggers Optional logger filter.  Only show log entries if they were produced by one of the specified loggers (simple class name).  Example: "&amp;loggers=(LinkIndexService,LinkIndexRestService)".
132    * @param severity Optional severity filter.  Only show log entries with the specified severity.  Example: "&amp;severity=(ERROR,WARN)".
133    * @throws Exception
134    */
135   @RestMethod(
136      name="VIEW",
137      path="/*",
138      swagger= {
139         "responses:{",
140            "200: {description:'OK'},",
141            "404: {description:'Not Found'}",
142         "}"
143      }
144   )
145   public void viewFile(RestRequest req, RestResponse res, @PathRemainder String path, RequestProperties properties, @Query("highlight") boolean highlight, @Query("start") String start, @Query("end") String end, @Query("thread") String thread, @Query("loggers") String[] loggers, @Query("severity") String[] severity) throws Exception {
146
147      File f = getFile(path);
148      if (f.isDirectory())
149         throw new RestException(SC_METHOD_NOT_ALLOWED, "View not available on directories");
150
151      Date startDate = parseISO8601Date(start), endDate = parseISO8601Date(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   /**
193    * [VIEW /*] - Retrieve the contents of a log file as parsed entries.
194    * 
195    * @param req The HTTP request.
196    * @param path The log file path.
197    * @param start Optional start timestamp.  Don't print lines logged before the specified timestamp.  Example:  "&amp;start=2014-01-23 11:25:47".
198    * @param end Optional end timestamp.  Don't print lines logged after the specified timestamp.  Example:  "&amp;end=2014-01-23 11:25:47".
199    * @param thread Optional thread name filter.  Only show log entries with the specified thread name.  Example: "&amp;thread=pool-33-thread-1".
200    * @param loggers Optional logger filter.  Only show log entries if they were produced by one of the specified loggers (simple class name).  Example: "&amp;loggers=(LinkIndexService,LinkIndexRestService)".
201    * @param severity Optional severity filter.  Only show log entries with the specified severity.  Example: "&amp;severity=(ERROR,WARN)".
202    * @return The parsed contents of the log file.
203    * @throws Exception
204    */
205   @RestMethod(
206      name="PARSE",
207      path="/*",
208      converters=Queryable.class,
209      swagger= {
210         "responses:{",
211            "200: {description:'OK'},",
212            "404: {description:'Not Found'}",
213         "}"
214      }
215   )
216   public LogParser viewParsedEntries(RestRequest req, @PathRemainder String path, @Query("start") String start, @Query("end") String end, @Query("thread") String thread, @Query("loggers") String[] loggers, @Query("severity") String[] severity) throws Exception {
217
218      File f = getFile(path);
219      Date startDate = parseISO8601Date(start), endDate = parseISO8601Date(end);
220
221      if (f.isDirectory())
222         throw new RestException(SC_METHOD_NOT_ALLOWED, "View not available on directories");
223
224      return getLogParser(f, startDate, endDate, thread, loggers, severity);
225   }
226
227   /**
228    * [DOWNLOAD /*] - Download file.
229    * 
230    * @param res The HTTP response.
231    * @param path The log file path.
232    * @return The contents of the log file.
233    * @throws Exception
234    */
235   @RestMethod(
236      name="DOWNLOAD",
237      path="/*",
238      swagger= {
239         "responses:{",
240            "200: {description:'OK'},",
241            "404: {description:'Not Found'}",
242         "}"
243      }
244   )
245   public Object downloadFile(RestResponse res, @PathRemainder String path) throws Exception {
246
247      File f = getFile(path);
248
249      if (f.isDirectory())
250         throw new RestException(SC_METHOD_NOT_ALLOWED, "Download not available on directories");
251
252      res.setContentType("application/octet-stream");
253      res.setContentLength((int)f.length());
254      return new FileInputStream(f);
255   }
256
257   /**
258    * [DELETE /*] - Delete a file.
259    * 
260    * @param path The log file path.
261    * @return A redirect object to the root.
262    * @throws Exception
263    */
264   @RestMethod(
265      name=DELETE,
266      path="/*",
267      swagger= {
268         "responses:{",
269            "200: {description:'OK'},",
270            "404: {description:'Not Found'}",
271         "}"
272      }
273   )
274   public Object deleteFile(@PathRemainder String path) throws Exception {
275
276      File f = getFile(path);
277
278      if (f.isDirectory())
279         throw new RestException(SC_BAD_REQUEST, "Delete not available on directories.");
280
281      if (f.canWrite())
282         if (! f.delete())
283            throw new RestException(SC_FORBIDDEN, "Could not delete file.");
284
285      return new Redirect(path + "/..");
286   }
287
288   private static BufferedReader getReader(File f) throws IOException {
289      return new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.defaultCharset()));
290   }
291
292   private File getFile(String path) {
293      if (path != null && path.indexOf("..") != -1)
294         throw new RestException(SC_NOT_FOUND, "File not found.");
295      File f = (path == null ? logDir : new File(logDir.getAbsolutePath() + '/' + path));
296      if (filter.accept(f))
297         return f;
298      throw new RestException(SC_NOT_FOUND, "File not found.");
299   }
300
301   /**
302    * File bean.
303    */
304   @SuppressWarnings("javadoc")
305   public static class FileResource {
306      final File f;
307      public final String type;
308      public final Object name;
309      public final Long size;
310      @Swap(DateSwap.DateTimeMedium.class) public Date lastModified;
311      public URI view, highlighted, parsed, download, delete;
312
313      public FileResource(File f, URI uri) throws Exception {
314         this.f = f;
315         this.type = (f.isDirectory() ? "dir" : "file");
316         this.name = f.isDirectory() ? new LinkString(f.getName(), uri.toString()) : f.getName();
317         this.size = f.isDirectory() ? null : f.length();
318         this.lastModified = new Date(f.lastModified());
319         if (f.canRead() && ! f.isDirectory()) {
320            this.view = new URI(uri + "?method=VIEW");
321            this.highlighted = new URI(uri + "?method=VIEW&highlight=true");
322            this.parsed = new URI(uri + "?method=PARSE");
323            this.download = new URI(uri + "?method=DOWNLOAD");
324            this.delete = new URI(uri + "?method=DELETE");
325         }
326      }
327   }
328
329   static final class FileResourceComparator implements Comparator<FileResource>, Serializable {
330      private static final long serialVersionUID = 1L;
331      @Override /* Comparator */
332      public int compare(FileResource o1, FileResource o2) {
333         int c = o1.type.compareTo(o2.type);
334         return c != 0 ? c : o1.f.getName().compareTo(o2.f.getName());
335      }
336   }
337
338   private Object getReader(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException {
339      if (start == null && end == null && thread == null && loggers == null)
340         return getReader(f);
341      return getLogParser(f, start, end, thread, loggers, severity);
342   }
343
344   private LogParser getLogParser(File f, final Date start, final Date end, final String thread, final String[] loggers, final String[] severity) throws IOException {
345      return new LogParser(leFormatter, f, start, end, thread, loggers, severity);
346   }
347}