/* * 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(); } }