| // Copyright 2013 Google Inc. All Rights Reserved. |
| // |
| // Licensed 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 com.google.enterprise.adaptor; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableSet; |
| |
| import com.sun.net.httpserver.HttpExchange; |
| import com.sun.net.httpserver.HttpHandler; |
| |
| import java.io.*; |
| import java.net.HttpURLConnection; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TimeZone; |
| import java.util.TreeMap; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipOutputStream; |
| |
| /** |
| * Generates and serves a .zip file containing diagnostic information. |
| * |
| * <p>For example, it can include logs and a thread dump. |
| */ |
| class DownloadDumpHandler implements HttpHandler { |
| private static final Logger log |
| = Logger.getLogger(DownloadDumpHandler.class.getName()); |
| |
| /** Used to generate the configuration output file */ |
| private Config config; |
| |
| /** To be used as part of the zip file name */ |
| private String feedName; |
| |
| /** Used to obtain statistics for one file in the .zip */ |
| private final StatRpcMethod statRpcMethod; |
| |
| /** Used to specify the top-level directory where logs are kept */ |
| private final File logsDir; |
| |
| /** Only used by the test class, to pass in a canned date */ |
| private final TimeProvider timeProvider; |
| |
| /** Used in handle() to generate the date portion of the zip file name; |
| we are implicitly using the local time zone. */ |
| private final DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd"); |
| |
| /** Default to "logs/" and System time */ |
| public DownloadDumpHandler(Config config, String feedName, |
| StatRpcMethod statRpcMethod) { |
| this(config, feedName, statRpcMethod, new File("logs/"), |
| new SystemTimeProvider()); |
| } |
| |
| @VisibleForTesting |
| DownloadDumpHandler(Config config, String feedName, |
| StatRpcMethod statRpcMethod, File logsDir, TimeProvider timeProvider) { |
| if (null == config) { |
| throw new NullPointerException(); |
| } |
| if (null == feedName) { |
| throw new NullPointerException(); |
| } |
| if (feedName.contains("\"")) { |
| throw new IllegalArgumentException( |
| "feedName must not contain the \" character"); |
| } |
| this.config = config; |
| this.feedName = feedName; |
| this.statRpcMethod = statRpcMethod; // OK to leave as null |
| this.logsDir = logsDir; |
| this.timeProvider = timeProvider; |
| } |
| |
| @Override |
| public void handle(HttpExchange ex) throws IOException { |
| String requestMethod = ex.getRequestMethod(); |
| if (!"GET".equals(requestMethod)) { |
| HttpExchanges.cannedRespond(ex, HttpURLConnection.HTTP_BAD_METHOD, |
| Translation.HTTP_BAD_METHOD); |
| return; |
| } |
| if (!ex.getRequestURI().getPath().equals(ex.getHttpContext().getPath())) { |
| HttpExchanges.cannedRespond(ex, HttpURLConnection.HTTP_NOT_FOUND, |
| Translation.HTTP_NOT_FOUND); |
| return; |
| } |
| String dateAsString; |
| synchronized (this) { // DateFormat.format() is not thread-safe! |
| dateAsString = dateFormat.format( |
| new Date(timeProvider.currentTimeMillis())); |
| } |
| String filename = feedName + "-" + dateAsString + ".zip"; |
| String contentType = "application/zip"; |
| ex.getResponseHeaders().set("Content-Disposition", |
| "attachment; filename=\"" + filename + "\""); |
| // stream the contents of the zip |
| HttpExchanges.startResponse( |
| ex, HttpURLConnection.HTTP_OK, contentType, true); |
| OutputStream os = ex.getResponseBody(); |
| BufferedOutputStream bos = new BufferedOutputStream(os); |
| ZipOutputStream zos = new ZipOutputStream(bos); |
| generateZipContents(logsDir, zos); |
| zos.close(); // NOT in a "finally" clause - connection killed on an error. |
| } |
| |
| private void generateZipContents(File logsDir, ZipOutputStream zos) |
| throws IOException { |
| dumpLogFiles(logsDir, zos); |
| dumpStackTraces(zos); |
| dumpConfig(zos); |
| dumpStats(zos); |
| zos.flush(); |
| } |
| |
| private void dumpLogFiles(File logsDir, ZipOutputStream zos) |
| throws IOException { |
| File[] files = logsDir.listFiles(); |
| if (files == null) { |
| log.log(Level.FINER, "Unable to find logs directory {0}", logsDir); |
| return; |
| } |
| for (File f: files) { |
| // avoid zipping the (empty) lock file |
| if (f.getName().endsWith(".lck")) { |
| log.log(Level.FINEST, "Skipping lock file: {0}", f.getName()); |
| continue; |
| } |
| if (!f.isFile()) { |
| // TODO(myk): consider zipping up files present under subdirectories. |
| log.log(Level.FINEST, "Ignoring directory entry: {0}", f.getName()); |
| continue; |
| } |
| log.log(Level.FINEST, "Adding file: {0}/{1}", |
| new Object[] {logsDir, f.getName()}); |
| InputStream is = createInputStream(f); |
| try { |
| zos.putNextEntry(new ZipEntry(logsDir.toString() + "/" + f.getName())); |
| IOHelper.copyStream(is, zos); |
| } finally { |
| is.close(); |
| } |
| zos.closeEntry(); |
| } |
| } |
| |
| /** |
| * Output the stack trace for every running thread (sorted by thread name). |
| * |
| * <p>For example: |
| * <p><code> |
| * Thread[Reference Handler,10,system] |
| * java.lang.Object.wait(Native Method) |
| * java.lang.Object.wait(Object.java:502) |
| * java.lang.ref.Reference$ReferenceHandler.run(Reference.java:129) |
| * </code><p><code> |
| * Thread ... |
| * </code> |
| */ |
| private void dumpStackTraces(ZipOutputStream zos) throws IOException { |
| OutputStreamWriter writer = new OutputStreamWriter(zos, "UTF-8"); |
| String newline = "\n"; // so our support folks always see the same results |
| Map<Thread, StackTraceElement[]> allThreads = Thread.getAllStackTraces(); |
| Map<String, StackTraceElement[]> sortedThreads = |
| new TreeMap<String, StackTraceElement[]>(); |
| for (Map.Entry<Thread, StackTraceElement[]> me : allThreads.entrySet()) { |
| sortedThreads.put(me.getKey().toString(), me.getValue()); |
| } |
| zos.putNextEntry(new ZipEntry("threaddump.txt")); |
| for (Map.Entry<String, StackTraceElement[]> me : sortedThreads.entrySet()) { |
| writer.write(me.getKey()); |
| writer.write(newline); |
| for (StackTraceElement element : me.getValue()) { |
| writer.write(" "); |
| writer.write("" + element); |
| writer.write(newline); |
| } |
| writer.write(newline); |
| } |
| writer.flush(); |
| zos.closeEntry(); |
| } |
| |
| /** |
| * Output the configuration into the diagnostics zip |
| */ |
| private void dumpConfig(ZipOutputStream zos) throws IOException { |
| PrintWriter writer = new PrintWriter(new OutputStreamWriter(zos, "UTF-8")); |
| String newline = "\n"; // so our support folks always see the same results |
| TreeMap<String, String> sortedConfig = new TreeMap<String, String>(); |
| for (String key : config.getAllKeys()) { |
| sortedConfig.put(key, config.getValue(key)); |
| } |
| zos.putNextEntry(new ZipEntry("config.txt")); |
| prettyPrintMap(writer, sortedConfig); |
| writer.flush(); |
| zos.closeEntry(); |
| } |
| |
| /** |
| * Output the version info and statistics into the diagnostics zip |
| */ |
| private void dumpStats(ZipOutputStream zos) throws IOException { |
| PrintWriter writer = new PrintWriter(new OutputStreamWriter(zos, "UTF-8")); |
| if (statRpcMethod == null) { |
| return; // don't generate empty stats file |
| } |
| @SuppressWarnings("unchecked") |
| Map<String, Object> map = (Map<String, Object>) statRpcMethod.run(null); |
| |
| zos.putNextEntry(new ZipEntry("stats.txt")); |
| |
| if (null != map.get("versionStats")) { |
| @SuppressWarnings("unchecked") |
| Map<String, Object> vMap = (Map<String, Object>) map.get("versionStats"); |
| prettyPrintMap(writer, vMap); |
| } |
| |
| if (null != map.get("simpleStats")) { |
| @SuppressWarnings("unchecked") |
| Map<String, Object> sMap = (Map<String, Object>) map.get("simpleStats"); |
| Set<String> expectedDateAttrs = ImmutableSet.of("whenStarted", |
| "lastSuccessfulFullPushStart", "lastSuccessfulFullPushEnd", |
| "currentFullPushStart", "lastSuccessfulIncremementalPushStart", |
| "lastSuccessfulIncremementalPushEnd", "currentIncrementalPushEnd"); |
| DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss Z"); |
| dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); |
| for (String key : expectedDateAttrs) { |
| if (!sMap.containsKey(key)) { |
| log.log(Level.INFO, |
| "Did not find expected key \"{0}\" in simpleStats", key); |
| continue; |
| } |
| Object value = sMap.get(key); |
| if (value == null) { |
| continue; |
| } |
| if (!(value instanceof Number)) { |
| log.log(Level.INFO, |
| "Key \"{0}\" contained non-date value \"{1}\" in simpleStats", |
| new Object[] {key, value}); |
| continue; |
| } |
| long date = ((Number) value).longValue(); |
| if (date <= 0) { |
| continue; |
| } |
| // It's a number > 0 -- assume it's a date. |
| // Replace value in map with a formatted date. |
| sMap.put(key, dateFormat.format(new Date(date))); |
| } |
| prettyPrintMap(writer, sMap); |
| } |
| |
| writer.flush(); |
| zos.closeEntry(); |
| } |
| |
| /** |
| * Pretty-prints a map |
| */ |
| void prettyPrintMap(PrintWriter writer, Map<String, ?> map) { |
| int maxKeyLength = 0; |
| |
| for (String key : map.keySet()) { |
| if (key.length() > maxKeyLength) { |
| maxKeyLength = key.length(); |
| } |
| } |
| |
| String outputFormat = "%-" + (maxKeyLength + 1) + "s= %s%n"; |
| for (Map.Entry<String, ?> me : map.entrySet()) { |
| writer.format(outputFormat, me.getKey(), |
| (me.getValue() == null ? "[null]" : me.getValue().toString())); |
| } |
| writer.format("%n"); |
| } |
| |
| /** |
| * Method gets overriden in test class to avoid using "real" IO. |
| */ |
| @VisibleForTesting |
| InputStream createInputStream(File file) throws IOException { |
| return new FileInputStream(file); |
| } |
| } |