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