blob: 86bef9c832f7ea73fd65739dfc2d94485139d5e7 [file] [log] [blame]
// 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.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
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", "lastSuccessfulIncrementalPushStart",
"lastSuccessfulIncrementalPushEnd");
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);
}
}