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.HttpMethod.*;
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(n="highlight", d="Add severity color highlighting.", ex="true") boolean highlight,
136         @Query(n="start", d="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", ex="2014-01-23T11:25:47") String start,
137         @Query(n="end", d="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", ex="2014-01-24") String end,
138         @Query(n="thread", d="Thread name filter.\nOnly show log entries with the specified thread name.", ex="thread-pool-33-thread-1") String thread,
139         @Query(n="loggers", d="Logger filter (simple class name).\nOnly show log entries if they were produced by one of the specified loggers.", ex="['LinkIndexService','LinkIndexRestService']") String[] loggers,
140         @Query(n="severity", d="Severity filter.\nOnly show log entries with the specified severity.", ex="['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(n="start", d="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", ex="2014-01-23T11:25:47") String start,
205         @Query(n="end", d="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", ex="2014-01-24") String end,
206         @Query(n="thread", d="Thread name filter.\nOnly show log entries with the specified thread name.", ex="thread-pool-33-thread-1") String thread,
207         @Query(n="loggers", d="Logger filter (simple class name).\nOnly show log entries if they were produced by one of the specified loggers.", ex="['LinkIndexService','LinkIndexRestService']") String[] loggers,
208         @Query(n="severity", d="Severity filter.\nOnly show log entries with the specified severity.", ex="['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}