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