| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.sling.tracer.internal; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.UUID; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.annotation.Nonnull; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import com.google.common.cache.Cache; |
| import com.google.common.cache.CacheBuilder; |
| import com.google.common.cache.Weigher; |
| import org.apache.commons.io.FileUtils; |
| import org.apache.felix.webconsole.SimpleWebConsolePlugin; |
| import org.apache.sling.commons.json.JSONException; |
| import org.apache.sling.commons.json.io.JSONWriter; |
| import org.osgi.framework.BundleContext; |
| |
| class TracerLogServlet extends SimpleWebConsolePlugin implements TraceLogRecorder { |
| static final String ATTR_RECORDING = TracerLogServlet.class.getName(); |
| |
| public static final String CLEAR = "clear"; |
| |
| private static final String LABEL = "tracer"; |
| |
| public static final String HEADER_TRACER_RECORDING = "Sling-Tracer-Record"; |
| |
| public static final String HEADER_TRACER_REQUEST_ID = "Sling-Tracer-Request-Id"; |
| |
| public static final String HEADER_TRACER_PROTOCOL_VERSION = "Sling-Tracer-Protocol-Version"; |
| |
| public static final int TRACER_PROTOCOL_VERSION = 1; |
| |
| private final Cache<String, JSONRecording> cache; |
| |
| private final boolean compressRecording; |
| |
| private final int cacheSizeInMB; |
| |
| private final long cacheDurationInSecs; |
| |
| private final boolean gzipResponse; |
| |
| public TracerLogServlet(BundleContext context){ |
| this(context, |
| LogTracer.PROP_TRACER_SERVLET_CACHE_SIZE_DEFAULT, |
| LogTracer.PROP_TRACER_SERVLET_CACHE_DURATION_DEFAULT, |
| LogTracer.PROP_TRACER_SERVLET_COMPRESS_DEFAULT, |
| LogTracer.PROP_TRACER_SERVLET_GZIP_RESPONSE_DEFAULT |
| ); |
| } |
| |
| public TracerLogServlet(BundleContext context, int cacheSizeInMB, long cacheDurationInSecs, |
| boolean compressionEnabled, boolean gzipResponse) { |
| super(LABEL, "Sling Tracer", "Sling", null); |
| this.compressRecording = compressionEnabled; |
| this.cacheDurationInSecs = cacheDurationInSecs; |
| this.cacheSizeInMB = cacheSizeInMB; |
| this.gzipResponse = compressionEnabled && gzipResponse; |
| this.cache = CacheBuilder.newBuilder() |
| .maximumWeight(cacheSizeInMB * FileUtils.ONE_MB) |
| .weigher(new Weigher<String, JSONRecording>() { |
| @Override |
| public int weigh(@Nonnull String key, @Nonnull JSONRecording value) { |
| return value.size(); |
| } |
| }) |
| .expireAfterAccess(cacheDurationInSecs, TimeUnit.SECONDS) |
| .recordStats() |
| .build(); |
| register(context); |
| } |
| |
| boolean isCompressRecording() { |
| return compressRecording; |
| } |
| |
| public boolean isGzipResponse() { |
| return gzipResponse; |
| } |
| |
| int getCacheSizeInMB() { |
| return cacheSizeInMB; |
| } |
| |
| long getCacheDurationInSecs() { |
| return cacheDurationInSecs; |
| } |
| |
| //~-----------------------------------------------< WebConsole Plugin > |
| |
| @Override |
| protected void renderContent(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
| if (isHtmlRequest(request)){ |
| PrintWriter pw = response.getWriter(); |
| renderStatus(pw); |
| renderRequests(pw); |
| } else { |
| String requestId = getRequestId(request); |
| prepareJSONResponse(response); |
| try { |
| boolean responseDone = false; |
| if (requestId != null) { |
| JSONRecording recording = cache.getIfPresent(requestId); |
| if (recording != null){ |
| boolean shouldGZip = prepareForGZipResponse(request, response); |
| responseDone = recording.render(response.getOutputStream(), shouldGZip); |
| } |
| } |
| |
| if (!responseDone) { |
| PrintWriter pw = response.getWriter(); |
| JSONWriter jw = new JSONWriter(pw); |
| jw.object(); |
| jw.key("error").value("Not found"); |
| jw.endObject(); |
| } |
| } catch (JSONException e) { |
| throw new ServletException(e); |
| } |
| } |
| } |
| |
| @Override |
| protected void doPost(HttpServletRequest req, HttpServletResponse resp) |
| throws IOException { |
| if (req.getParameter(CLEAR) != null) { |
| resetCache(); |
| resp.sendRedirect(req.getRequestURI()); |
| } |
| } |
| |
| @Override |
| protected boolean isHtmlRequest(HttpServletRequest request) { |
| return request.getRequestURI().endsWith(LABEL); |
| } |
| |
| private boolean prepareForGZipResponse(HttpServletRequest request, HttpServletResponse response) { |
| if (!gzipResponse) { |
| return false; |
| } |
| |
| String acceptEncoding = request.getHeader("Accept-Encoding"); |
| boolean acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip"); |
| |
| if (acceptsGzip) { |
| response.setHeader("Content-Encoding", "gzip"); |
| } |
| return acceptsGzip; |
| } |
| |
| /** |
| * Returns true if the given accept header accepts the given value. |
| * @param acceptHeader The accept header. |
| * @param toAccept The value to be accepted. |
| * @return True if the given accept header accepts the given value. |
| */ |
| private static boolean accepts(String acceptHeader, String toAccept) { |
| return acceptHeader.contains(toAccept) || acceptHeader.contains("*/*"); |
| } |
| |
| private static void prepareJSONResponse(HttpServletResponse response) { |
| response.setContentType("application/json"); |
| response.setCharacterEncoding("UTF-8"); |
| } |
| |
| private void renderStatus(PrintWriter pw) { |
| pw.printf("<p class='statline'>Log Tracer Recordings: %d recordings, %s memory " + |
| "(Max %dMB, Expired in %d secs)</p>%n", cache.size(), |
| memorySize(), cacheSizeInMB, cacheDurationInSecs); |
| |
| pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>"); |
| pw.println("<span style='float: left; margin-left: 1em'>Tracer Recordings</span>"); |
| pw.println("<form method='POST'><input type='hidden' name='clear' value='clear'><input type='submit' value='Clear' class='ui-state-default ui-corner-all'></form>"); |
| pw.println("</div>"); |
| } |
| |
| private String memorySize() { |
| long size = 0; |
| for (JSONRecording r : cache.asMap().values()){ |
| size += r.size(); |
| } |
| return humanReadableByteCount(size); |
| } |
| |
| private void renderRequests(PrintWriter pw) { |
| if (cache.size() > 0){ |
| pw.println("<ol>"); |
| List<JSONRecording> recordings = new ArrayList<JSONRecording>(cache.asMap().values()); |
| SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); |
| Collections.sort(recordings); |
| for (JSONRecording r : recordings){ |
| String id = r.getRequestId(); |
| String date = sdf.format(new Date(r.getStart())); |
| pw.printf("<li>%s - <a href='%s/%s.json'>%s</a> - %s (%s) (%dms)</li>", |
| date, LABEL, id, id, |
| r.getUri(), |
| humanReadableByteCount(r.size()), |
| r.getTimeTaken()); |
| } |
| pw.println("</ol>"); |
| } |
| } |
| |
| private static String getRequestId(HttpServletRequest request) { |
| String requestUri = request.getRequestURI(); |
| int lastSlash = requestUri.lastIndexOf('/'); |
| int lastDot = requestUri.indexOf('.', lastSlash + 1); |
| if (lastDot > 0){ |
| return requestUri.substring(lastSlash + 1, lastDot); |
| } |
| return null; |
| } |
| |
| //~-----------------------------------------------< TraceLogRecorder > |
| |
| @Override |
| public Recording startRecording(HttpServletRequest request, HttpServletResponse response) { |
| if (request.getHeader(HEADER_TRACER_RECORDING) == null){ |
| return Recording.NOOP; |
| } |
| |
| if (request.getAttribute(ATTR_RECORDING) != null){ |
| //Already processed |
| return getRecordingForRequest(request); |
| } |
| |
| String requestId = generateRequestId(); |
| JSONRecording recording = record(requestId, request); |
| |
| response.setHeader(HEADER_TRACER_REQUEST_ID, requestId); |
| response.setHeader(HEADER_TRACER_PROTOCOL_VERSION, String.valueOf(TRACER_PROTOCOL_VERSION)); |
| |
| return recording; |
| } |
| |
| @Override |
| public Recording getRecordingForRequest(HttpServletRequest request) { |
| Recording recording = (Recording) request.getAttribute(ATTR_RECORDING); |
| if (recording == null){ |
| recording = Recording.NOOP; |
| } |
| return recording; |
| } |
| |
| @Override |
| public void endRecording(HttpServletRequest httpRequest, Recording recording) { |
| if (recording instanceof JSONRecording) { |
| JSONRecording r = (JSONRecording) recording; |
| r.done(); |
| cache.put(r.getRequestId(), r); |
| } |
| httpRequest.removeAttribute(ATTR_RECORDING); |
| } |
| |
| Recording getRecording(String requestId) { |
| Recording recording = cache.getIfPresent(requestId); |
| return recording == null ? Recording.NOOP : recording; |
| } |
| |
| private JSONRecording record(String requestId, HttpServletRequest request) { |
| JSONRecording data = new JSONRecording(requestId, request, compressRecording); |
| request.setAttribute(ATTR_RECORDING, data); |
| return data; |
| } |
| |
| private static String generateRequestId() { |
| return UUID.randomUUID().toString(); |
| } |
| |
| /** |
| * Returns a human-readable version of the file size, where the input represents |
| * a specific number of bytes. Based on http://stackoverflow.com/a/3758880/1035417 |
| */ |
| @SuppressWarnings("Duplicates") |
| private static String humanReadableByteCount(long bytes) { |
| if (bytes < 0) { |
| return "0"; |
| } |
| int unit = 1000; |
| if (bytes < unit) { |
| return bytes + " B"; |
| } |
| int exp = (int) (Math.log(bytes) / Math.log(unit)); |
| char pre = "kMGTPE".charAt(exp - 1); |
| return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); |
| } |
| |
| void resetCache(){ |
| cache.invalidateAll(); |
| } |
| } |