You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by zm...@apache.org on 2015/08/26 23:00:14 UTC

[24/51] [partial] aurora git commit: Move packages from com.twitter.common to org.apache.aurora.common

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/GuiceServletConfig.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/GuiceServletConfig.java b/commons/src/main/java/org/apache/aurora/common/net/http/GuiceServletConfig.java
new file mode 100644
index 0000000..e4fa8aa
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/GuiceServletConfig.java
@@ -0,0 +1,41 @@
+/**
+ * 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 org.apache.aurora.common.net.http;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.servlet.GuiceServletContextListener;
+
+import javax.servlet.ServletContextEvent;
+import java.util.logging.Logger;
+
+/**
+ * A wrapper around the GuiceServletContextListener that has access to the injector.
+ *
+ * @author Florian Leibert
+ */
+public class GuiceServletConfig extends GuiceServletContextListener {
+  private final Injector injector;
+
+  @Inject
+  public GuiceServletConfig(Injector injector) {
+    this.injector = Preconditions.checkNotNull(injector);
+  }
+
+  @Override
+  protected Injector getInjector() {
+    return injector;
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/HttpServerDispatch.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/HttpServerDispatch.java b/commons/src/main/java/org/apache/aurora/common/net/http/HttpServerDispatch.java
new file mode 100644
index 0000000..3208267
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/HttpServerDispatch.java
@@ -0,0 +1,123 @@
+/**
+ * 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 org.apache.aurora.common.net.http;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.servlet.Filter;
+import javax.servlet.ServletContextAttributeListener;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletRequestAttributeListener;
+import javax.servlet.ServletRequestListener;
+import javax.servlet.http.HttpServlet;
+
+/**
+ * A HTTP server dispatcher. Supports registering handlers for different
+ * URI paths, which will be called when a request is received.
+ *
+ * @author Florian Leibert
+ */
+public interface HttpServerDispatch {
+
+  /**
+   * Opens the HTTP server on the given port.
+   *
+   * @param port The port to listen on.
+   * @return {@code true} if the server started successfully on the port, {@code false} otherwise.
+   */
+  boolean listen(int port);
+
+  /**
+   * Opens the HTTP server on random port within the given range.
+   *
+   * @param minPort The minimum port number to listen on.
+   * @param maxPort The maximum port number to listen on.
+   * @return {@code true} if the server started successfully on the port, {@code false} otherwise.
+   */
+  boolean listen(int minPort, int maxPort);
+
+  /**
+   * @return true if the underlying HttpServer is started, false otherwise.
+   */
+  boolean isStarted();
+
+  /**
+   * @return the port the underlying HttpServer is listening on, which requires
+   *         the underlying HttpServer to be started and listening.
+   */
+  int getPort();
+
+  /**
+   * Stops the HTTP server.
+   */
+  void stop();
+
+  /**
+   * Adds an arbitrary endpoint to the root servlet.
+   * This can be used to include convenience links, or references to endpoints served by
+   * a different servlet container under this HTTP server.
+   *
+   * @param path The URI path of the endpoint.
+   */
+  void registerIndexLink(String path);
+
+  /**
+   * Registers a URI handler, replacing the existing handler if it exists.
+   *
+   * @param path       The URI path that the handler should be called for.
+   * @param handler    The handler to call.
+   * @param initParams An optional map of servlet init parameter names and their values.
+   * @param silent     Whether to display the registered handler in the root "/" response.
+   *                   Useful for handlers that you want to avoid accidental clicks on.
+   */
+  void registerHandler(String path, HttpServlet handler,
+                       @Nullable Map<String, String> initParams, boolean silent);
+
+  /**
+   * Registers a servlet filter.
+   *
+   * @param filterClass Filter class to register.
+   * @param pathSpec Path spec that the filter should be activated on.
+   */
+  void registerFilter(Class<? extends Filter> filterClass, String pathSpec);
+
+  /**
+   * Registers a context listener.
+   *
+   * @param servletContextListener Listener to register.
+   */
+  void registerListener(ServletContextListener servletContextListener);
+
+  /**
+   * Registers a context attribute listener.
+   *
+   * @param servletContextAttributeListener Listener to register.
+   */
+  void registerListener(ServletContextAttributeListener servletContextAttributeListener);
+
+  /**
+   * Registers a request listener.
+   *
+   * @param servletRequestListener Listener to register.
+   */
+  void registerListener(ServletRequestListener servletRequestListener);
+
+  /**
+   * Registers a request attribute listener.
+   *
+   * @param servletRequestAttributeListener Listener to register.
+   */
+  void registerListener(ServletRequestAttributeListener servletRequestAttributeListener);
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/JettyHttpServerDispatch.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/JettyHttpServerDispatch.java b/commons/src/main/java/org/apache/aurora/common/net/http/JettyHttpServerDispatch.java
new file mode 100644
index 0000000..a3fa57e
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/JettyHttpServerDispatch.java
@@ -0,0 +1,283 @@
+/**
+ * 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 org.apache.aurora.common.net.http;
+
+import java.io.IOException;
+import java.util.EventListener;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.servlet.Filter;
+import javax.servlet.ServletContextAttributeListener;
+import javax.servlet.ServletContextListener;
+import javax.servlet.ServletRequestAttributeListener;
+import javax.servlet.ServletRequestListener;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+
+import org.apache.aurora.common.net.http.handlers.TextResponseHandler;
+import org.mortbay.jetty.AbstractConnector;
+import org.mortbay.jetty.Connector;
+import org.mortbay.jetty.Handler;
+import org.mortbay.jetty.RequestLog;
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.handler.RequestLogHandler;
+import org.mortbay.jetty.nio.SelectChannelConnector;
+import org.mortbay.jetty.servlet.Context;
+import org.mortbay.jetty.servlet.ServletHolder;
+
+import org.apache.aurora.common.base.MorePreconditions;
+
+/**
+ * A simple multi-threaded HTTP server dispatcher.  Supports registering handlers for different
+ * URI paths, which will be called when a request is received.
+ *
+ * @author William Farner
+ */
+public class JettyHttpServerDispatch implements HttpServerDispatch {
+  private static final Logger LOG = Logger.getLogger(JettyHttpServerDispatch.class.getName());
+
+  // Registered endpoints. Used only for display.
+  private final Set<String> registeredEndpoints = Sets.newHashSet();
+
+  private final Optional<RequestLog> requestLog;
+  private Server server;
+  private Context context;
+  private int port;
+
+  /**
+   * Creates an HTTP server.
+   */
+  public JettyHttpServerDispatch() {
+    this.requestLog = Optional.absent();
+  }
+
+  /**
+   * Creates an HTTP server which will be configured to log requests to the provided request log.
+   *
+   * @param requestLog HTTP request log.
+   */
+  @Inject
+  public JettyHttpServerDispatch(RequestLog requestLog) {
+    this.requestLog = Optional.of(requestLog);
+  }
+
+  /**
+   * Opens the HTTP server on the given port.
+   *
+   * @param port The port to listen on.
+   * @return {@code true} if the server started successfully on the port, {@code false} otherwise.
+   */
+  public boolean listen(int port) {
+    return listen(port, port);
+  }
+
+  @Override
+  public synchronized boolean listen(int minPort, int maxPort) {
+    boolean state = !isStarted();
+    Preconditions.checkState(state,
+        "HttpServerDispatch has already been started on port: %d", port);
+
+    Connector connector = openConnector(minPort, maxPort);
+    if (connector == null) return false; // Couldn't open a server port.
+    port = connector.getLocalPort();
+
+    server = new Server();
+    server.addConnector(connector);
+    context = new Context(server, "/", Context.NO_SESSIONS);
+    if (requestLog.isPresent()) {
+      RequestLogHandler logHandler = new RequestLogHandler();
+      logHandler.setRequestLog(requestLog.get());
+      context.addHandler(logHandler);
+    }
+
+    context.addServlet(new ServletHolder(new RootHandler()), "/");
+
+    try {
+      server.start();
+      LOG.info("HTTP server is listening on port " + port);
+      return true;
+    } catch (Exception e) {
+      LOG.log(Level.SEVERE, "HTTP server failed to start on port " + connector.getLocalPort(), e);
+      return false;
+    }
+  }
+
+  @Override
+  public synchronized boolean isStarted() {
+    return (server != null) && server.isStarted();
+  }
+
+  @Override
+  public synchronized int getPort() {
+    Preconditions.checkState(isStarted(), "HttpServer must be started before port can be determined");
+    return port;
+  }
+
+  /**
+   * Opens a new Connector which is a Jetty specific way of handling the
+   * lifecycle and configuration of the Jetty server. The connector will
+   * open a Socket on an available port between minPort and maxPort.
+   * A subclass can override this method to modify connector configurations
+   * such as queue-size or header-buffer-size.
+   * @param minPort the minimum port number to bind to.
+   * @param maxPort the maximum port number to bind to.
+   * @return
+   */
+  protected Connector openConnector(int minPort, int maxPort) {
+    if (minPort != 0 || maxPort != 0) {
+      Preconditions.checkState(minPort > 0, "Invalid port range.");
+      Preconditions.checkState(maxPort > 0, "Invalid port range.");
+      Preconditions.checkState(minPort <= maxPort, "Invalid port range.");
+    }
+    int attempts = 0;
+    int port;
+
+    int maxAttempts = minPort == maxPort ? 1 : 5;
+    while (++attempts <= maxAttempts) {
+      if (minPort == maxPort) {
+        port = minPort;
+      } else {
+        port = minPort + new Random().nextInt(maxPort - minPort);
+      }
+      LOG.info("Attempting to listen on port " + port);
+
+      try {
+        // TODO(John Sirois): consider making Connector impl parametrizable
+        AbstractConnector connector = new SelectChannelConnector();
+        connector.setPort(port);
+        // Create the server with a maximum TCP backlog of 50, meaning that when the request queue
+        // exceeds 50, subsequent connections may be rejected.
+        connector.setAcceptQueueSize(50);
+        connector.open();
+        return connector;
+      } catch (IOException e) {
+        LOG.log(Level.WARNING, "Failed to create HTTP server on port " + port, e);
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public synchronized void stop() {
+    if (isStarted()) {
+      try {
+        server.stop();
+      } catch (Exception e) {
+        LOG.log(Level.SEVERE, "Error stopping HTTPServer on " + port, e);
+      }
+    }
+  }
+
+  @Override
+  public synchronized void registerHandler(
+      String path,
+      HttpServlet handler,
+      @Nullable Map<String, String> initParams,
+      boolean silent) {
+
+    Preconditions.checkNotNull(path);
+    Preconditions.checkNotNull(handler);
+    Preconditions.checkState(path.length() > 0);
+    Preconditions.checkState(path.charAt(0) == '/');
+
+    if (silent) {
+      registeredEndpoints.remove(path);
+    } else {
+      registeredEndpoints.add(path);
+    }
+
+    ServletHolder servletHolder = new ServletHolder(handler);
+    if (initParams != null) {
+      servletHolder.setInitParameters(initParams);
+    }
+    getRootContext().addServlet(servletHolder, path.replaceFirst("/?$", "/*"));
+  }
+
+  @Override
+  public synchronized void registerFilter(Class<? extends Filter> filterClass, String pathSpec) {
+    MorePreconditions.checkNotBlank(pathSpec);
+    Preconditions.checkNotNull(filterClass);
+    getRootContext().addFilter(filterClass, pathSpec, Handler.REQUEST);
+  }
+
+  @Override
+  public synchronized void registerIndexLink(String path) {
+    MorePreconditions.checkNotBlank(path);
+    registeredEndpoints.add(path);
+  }
+
+  @Override
+  public void registerListener(ServletContextListener servletContextListener) {
+    registerEventListener(servletContextListener);
+  }
+
+  @Override
+  public void registerListener(ServletContextAttributeListener servletContextAttributeListener) {
+    registerEventListener(servletContextAttributeListener);
+  }
+
+  @Override
+  public void registerListener(ServletRequestListener servletRequestListener) {
+    registerEventListener(servletRequestListener);
+  }
+
+  @Override
+  public void registerListener(ServletRequestAttributeListener servletRequestAttributeListener) {
+    registerEventListener(servletRequestAttributeListener);
+  }
+
+  private synchronized void registerEventListener(EventListener eventListener) {
+    Preconditions.checkNotNull(eventListener);
+    getRootContext().addEventListener(eventListener);
+  }
+
+  public synchronized Context getRootContext() {
+    Preconditions.checkState(context != null, "Context is not yet available. " +
+        "Ensure that listen(...) is called prior to calling this method.");
+    return context;
+  }
+
+  /**
+   * The root handler, which will display the paths at which all handlers are registered.
+   */
+  private class RootHandler extends TextResponseHandler {
+    public RootHandler() {
+      super("text/html");
+    }
+
+    @Override
+    public Iterable<String> getLines(HttpServletRequest request) {
+      List<String> lines = Lists.newArrayList();
+      lines.add("<html>");
+      for (String handler : Ordering.natural().sortedCopy(registeredEndpoints)) {
+        lines.add(String.format("<a href='%s'>%s</a><br />", handler, handler));
+      }
+      lines.add("</html>");
+      return lines;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/RequestLogger.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/RequestLogger.java b/commons/src/main/java/org/apache/aurora/common/net/http/RequestLogger.java
new file mode 100644
index 0000000..8677880
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/RequestLogger.java
@@ -0,0 +1,130 @@
+/**
+ * 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 org.apache.aurora.common.net.http;
+
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.mortbay.component.AbstractLifeCycle;
+import org.mortbay.jetty.HttpHeaders;
+import org.mortbay.jetty.Request;
+import org.mortbay.jetty.RequestLog;
+import org.mortbay.jetty.Response;
+import org.mortbay.util.DateCache;
+
+import org.apache.aurora.common.util.Clock;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A request logger that borrows formatting code from {@link org.mortbay.jetty.NCSARequestLog},
+ * but removes unneeded features (writing to file) and logging to java.util.logging.
+ */
+public class RequestLogger extends AbstractLifeCycle implements RequestLog {
+
+  private static final Logger LOG = Logger.getLogger(RequestLogger.class.getName());
+
+  private final Clock clock;
+  private final LogSink sink;
+  private final DateCache logDateCache;
+
+  interface LogSink {
+    boolean isLoggable(Level level);
+    void log(Level level, String messagge);
+  }
+
+  RequestLogger() {
+    this(Clock.SYSTEM_CLOCK, new LogSink() {
+      @Override
+      public boolean isLoggable(Level level) {
+        return LOG.isLoggable(level);
+      }
+
+      @Override public void log(Level level, String message) {
+        LOG.log(level, message);
+      }
+    });
+  }
+
+  @VisibleForTesting
+  RequestLogger(Clock clock, LogSink sink) {
+    this.clock = checkNotNull(clock);
+    this.sink = checkNotNull(sink);
+    logDateCache = new DateCache("dd/MMM/yyyy:HH:mm:ss Z", Locale.getDefault());
+    logDateCache.setTimeZoneID("GMT");
+  }
+
+  private String formatEntry(Request request, Response response) {
+    StringBuilder buf = new StringBuilder();
+
+    buf.append(request.getServerName());
+    buf.append(' ');
+
+    String addr = request.getHeader(HttpHeaders.X_FORWARDED_FOR);
+    if (addr == null) {
+      addr = request.getRemoteAddr();
+    }
+
+    buf.append(addr);
+    buf.append(" [");
+    buf.append(logDateCache.format(request.getTimeStamp()));
+    buf.append("] \"");
+    buf.append(request.getMethod());
+    buf.append(' ');
+    buf.append(request.getUri().toString());
+    buf.append(' ');
+    buf.append(request.getProtocol());
+    buf.append("\" ");
+    buf.append(response.getStatus());
+    buf.append(' ');
+    buf.append(response.getContentCount());
+    buf.append(' ');
+
+    String referer = request.getHeader(HttpHeaders.REFERER);
+    if (referer == null) {
+      buf.append("\"-\" ");
+    } else {
+      buf.append('"');
+      buf.append(referer);
+      buf.append("\" ");
+    }
+
+    String agent = request.getHeader(HttpHeaders.USER_AGENT);
+    if (agent == null) {
+      buf.append("\"-\" ");
+    } else {
+      buf.append('"');
+      buf.append(agent);
+      buf.append('"');
+    }
+
+    buf.append(' ');
+    buf.append(clock.nowMillis() - request.getTimeStamp());
+    return buf.toString();
+  }
+
+  @Override
+  public void log(Request request, Response response) {
+    int statusCategory = response.getStatus() / 100;
+    Level level = ((statusCategory == 2) || (statusCategory == 3)) ? Level.FINE : Level.INFO;
+    if (!sink.isLoggable(level)) {
+      return;
+    }
+
+    sink.log(level, formatEntry(request, response));
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/filters/AbstractHttpFilter.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/filters/AbstractHttpFilter.java b/commons/src/main/java/org/apache/aurora/common/net/http/filters/AbstractHttpFilter.java
new file mode 100644
index 0000000..681a95d
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/filters/AbstractHttpFilter.java
@@ -0,0 +1,63 @@
+/**
+ * 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 org.apache.aurora.common.net.http.filters;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A filter that allows subclass to omit implementations of {@link #init(FilterConfig)} and
+ * {@link #destroy()}.
+ */
+public abstract class AbstractHttpFilter implements Filter {
+
+  @Override
+  public void init(FilterConfig filterConfig) throws ServletException {
+    // No-op by default.
+  }
+
+  @Override
+  public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
+  }
+
+  /**
+   * Convenience method to allow subclasses to avoid type casting that may be necessary when
+   * implementing {@link #doFilter(ServletRequest, ServletResponse, FilterChain)}.
+   *
+   * @param request HTTP request.
+   * @param response HTTP response.
+   * @param chain Filter chain.
+   * @throws IOException If there is an error reading the request or writing the response.
+   * @throws ServletException If the filter or chain encounters an error handling the request.
+   */
+  public abstract void doFilter(
+      HttpServletRequest request,
+      HttpServletResponse response,
+      FilterChain chain) throws IOException, ServletException;
+
+  @Override
+  public void destroy() {
+    // No-op by default.
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/filters/HttpStatsFilter.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/filters/HttpStatsFilter.java b/commons/src/main/java/org/apache/aurora/common/net/http/filters/HttpStatsFilter.java
new file mode 100644
index 0000000..493018a
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/filters/HttpStatsFilter.java
@@ -0,0 +1,158 @@
+/**
+ * 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 org.apache.aurora.common.net.http.filters;
+
+import java.io.IOException;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.Context;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.inject.Inject;
+import com.sun.jersey.api.core.ExtendedUriInfo;
+import com.sun.jersey.api.model.AbstractResourceMethod;
+import com.sun.jersey.spi.container.ContainerRequest;
+import com.sun.jersey.spi.container.ContainerResponse;
+import com.sun.jersey.spi.container.ContainerResponseFilter;
+
+import org.apache.aurora.common.collections.Pair;
+import org.apache.aurora.common.stats.SlidingStats;
+import org.apache.aurora.common.stats.Stats;
+import org.apache.aurora.common.util.Clock;
+
+/**
+ * An HTTP filter that exports counts and timing for requests based on response code.
+ */
+public class HttpStatsFilter extends AbstractHttpFilter implements ContainerResponseFilter {
+  /**
+   * Methods tagged with this annotation will be intercepted and stats will be tracked accordingly.
+   */
+  @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
+  public @interface TrackRequestStats {
+    /**
+     * Indicates the identifier to use when tracking requests with this annotation.
+     */
+    String value();
+  }
+
+  private static final Logger LOG = Logger.getLogger(HttpStatsFilter.class.getName());
+
+  @VisibleForTesting
+  static final String REQUEST_START_TIME = "request_start_time";
+
+  private final Clock clock;
+  @Context private ExtendedUriInfo extendedUriInfo;
+
+  @VisibleForTesting
+  final LoadingCache<Pair<String, Integer>, SlidingStats> requestCounters =
+      CacheBuilder.newBuilder()
+          .build(new CacheLoader<Pair<String, Integer>, SlidingStats>() {
+            @Override
+            public SlidingStats load(Pair<String, Integer> identifierAndStatus) {
+              return new SlidingStats("http_" + identifierAndStatus.getFirst() + "_"
+                  + identifierAndStatus.getSecond() + "_responses", "nanos");
+            }
+          });
+
+  @Context private HttpServletRequest servletRequest;
+
+  @VisibleForTesting
+  final LoadingCache<Integer, SlidingStats> statusCounters = CacheBuilder.newBuilder()
+      .build(new CacheLoader<Integer, SlidingStats>() {
+        @Override
+        public SlidingStats load(Integer status) {
+          return new SlidingStats("http_" + status + "_responses", "nanos");
+        }
+      });
+
+  @VisibleForTesting
+  final AtomicLong exceptionCount = Stats.exportLong("http_request_exceptions");
+
+  @Inject
+  public HttpStatsFilter(Clock clock) {
+    this.clock = Preconditions.checkNotNull(clock);
+  }
+
+  private void trackStats(int status) {
+    long endTime = clock.nowNanos();
+
+    Object startTimeAttribute = servletRequest.getAttribute(REQUEST_START_TIME);
+    if (startTimeAttribute == null) {
+      LOG.fine("No start time attribute was found on the request, this filter should be wired"
+          + " as both a servlet filter and a container filter.");
+      return;
+    }
+
+    long elapsed = endTime - ((Long) startTimeAttribute).longValue();
+    statusCounters.getUnchecked(status).accumulate(elapsed);
+
+    AbstractResourceMethod matchedMethod =  extendedUriInfo.getMatchedMethod();
+    // It's possible for no method to have matched, e.g. in the case of a 404, don't let those
+    // cases lead to an exception and a 500 response.
+    if (matchedMethod == null) {
+      return;
+    }
+
+    TrackRequestStats trackRequestStats = matchedMethod.getAnnotation(TrackRequestStats.class);
+
+    if (trackRequestStats == null) {
+      Method method = matchedMethod.getMethod();
+      LOG.fine("The method that handled this request (" + method.getDeclaringClass() + "#"
+          + method.getName() + ") is not annotated with " + TrackRequestStats.class.getSimpleName()
+          + ". No request stats will recorded.");
+      return;
+    }
+
+    requestCounters.getUnchecked(Pair.of(trackRequestStats.value(), status)).accumulate(elapsed);
+  }
+
+  @Override
+  public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
+    trackStats(response.getStatus());
+
+    return response;
+  }
+
+  @Override
+  public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+
+    long startTime = clock.nowNanos();
+    request.setAttribute(REQUEST_START_TIME, startTime);
+
+    try {
+      chain.doFilter(request, response);
+    } catch (IOException e) {
+      exceptionCount.incrementAndGet();
+      throw e;
+    } catch (ServletException e) {
+      exceptionCount.incrementAndGet();
+      throw e;
+    }
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AbortHandler.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AbortHandler.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AbortHandler.java
new file mode 100644
index 0000000..e97bd82
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AbortHandler.java
@@ -0,0 +1,71 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * A servlet that provides a way to remotely terminate the running process immediately.
+ */
+public class AbortHandler extends HttpServlet {
+
+  /**
+   * A {@literal @Named} binding key for the QuitHandler listener.
+   */
+  public static final String ABORT_HANDLER_KEY =
+      "com.twitter.common.net.http.handlers.AbortHandler.listener";
+
+  private static final Logger LOG = Logger.getLogger(AbortHandler.class.getName());
+
+  private final Runnable abortListener;
+
+  /**
+   * Constructs a new AbortHandler that will notify the given {@code abortListener} when the servlet
+   * is accessed.  It is the responsibility of the listener to initiate an immediate shutdown of
+   * the system.
+   *
+   * @param abortListener Runnable to notify when the servlet is accessed.
+   */
+  @Inject
+  public AbortHandler(@Named(ABORT_HANDLER_KEY) Runnable abortListener) {
+    this.abortListener = Preconditions.checkNotNull(abortListener);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    LOG.info(String.format("Received abort HTTP signal from %s (%s)",
+        req.getRemoteAddr(), req.getRemoteHost()));
+
+    resp.setContentType("text/plain");
+    PrintWriter writer = resp.getWriter();
+    try {
+      writer.println("Aborting process NOW!");
+      writer.close();
+      abortListener.run();
+    } catch (Exception e) {
+      LOG.log(Level.WARNING, "Abort failed.", e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AssetHandler.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AssetHandler.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AssetHandler.java
new file mode 100644
index 0000000..a514f14
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/AssetHandler.java
@@ -0,0 +1,189 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Closeables;
+import com.google.common.io.InputSupplier;
+
+import org.apache.aurora.common.quantity.Amount;
+import org.apache.aurora.common.quantity.Time;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Servlet that is responsible for serving an asset.
+ *
+ * @author William Farner
+ */
+public class AssetHandler extends HttpServlet {
+
+  @VisibleForTesting
+  static final Amount<Integer, Time> CACHE_CONTROL_MAX_AGE_SECS = Amount.of(30, Time.DAYS);
+  private static final String GZIP_ENCODING = "gzip";
+
+  private final StaticAsset staticAsset;
+
+  public static class StaticAsset {
+    private final InputSupplier<? extends InputStream> inputSupplier;
+    private final String contentType;
+    private final boolean cacheLocally;
+
+    private byte[] gzipData = null;
+    private String hash = null;
+
+    /**
+     * Creates a new static asset.
+     *
+     * @param inputSupplier Supplier of the input stream from which to load the asset.
+     * @param contentType HTTP content type of the asset.
+     * @param cacheLocally If {@code true} the asset will be loaded once and stored in memory, if
+     *    {@code false} it will be loaded on each request.
+     */
+    public StaticAsset(InputSupplier<? extends InputStream> inputSupplier,
+        String contentType, boolean cacheLocally) {
+      this.inputSupplier = checkNotNull(inputSupplier);
+      this.contentType = checkNotNull(contentType);
+      this.cacheLocally = cacheLocally;
+    }
+
+    public String getContentType() {
+      return contentType;
+    }
+
+    public synchronized byte[] getRawData() throws IOException {
+      byte[] zipData = getGzipData();
+      GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(zipData));
+      return ByteStreams.toByteArray(in);
+    }
+
+    public synchronized byte[] getGzipData() throws IOException {
+      byte[] data = gzipData;
+      // Ensure we don't double-read after a call to getChecksum().
+      if (!cacheLocally || gzipData == null) {
+        load();
+        data = gzipData;
+      }
+      if (!cacheLocally) {
+        gzipData = null;
+      }
+
+      return data;
+    }
+
+    public synchronized String getChecksum() throws IOException {
+      if (hash == null) {
+        load();
+      }
+      return hash;
+    }
+
+    private void load() throws IOException {
+      ByteArrayOutputStream gzipBaos = new ByteArrayOutputStream();
+      GZIPOutputStream gzipStream = new GZIPOutputStream(gzipBaos);
+      ByteStreams.copy(inputSupplier, gzipStream);
+      gzipStream.flush();  // copy() does not flush or close output stream.
+      gzipStream.close();
+      gzipData = gzipBaos.toByteArray();
+
+      // Calculate a checksum of the gzipped data.
+      hash = Base64.encodeBase64String(DigestUtils.md5(gzipData)).trim();
+    }
+  }
+
+  /**
+   * Creates a new asset handler.
+   *
+   * @param staticAsset The asset to serve.
+   */
+  public AssetHandler(StaticAsset staticAsset) {
+    this.staticAsset = checkNotNull(staticAsset);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+
+    OutputStream responseBody = resp.getOutputStream();
+
+    if (checksumMatches(req)) {
+      resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+    } else {
+      setPayloadHeaders(resp);
+
+      boolean gzip = supportsGzip(req);
+      if (gzip) {
+        resp.setHeader("Content-Encoding", GZIP_ENCODING);
+      }
+
+      InputStream in = new ByteArrayInputStream(
+          gzip ? staticAsset.getGzipData() : staticAsset.getRawData());
+      ByteStreams.copy(in, responseBody);
+    }
+
+    Closeables.close(responseBody, /* swallowIOException */ true);
+  }
+
+  private void setPayloadHeaders(HttpServletResponse resp) throws IOException {
+    resp.setStatus(HttpServletResponse.SC_OK);
+    resp.setContentType(staticAsset.getContentType());
+    resp.setHeader("Cache-Control", "public,max-age=" + CACHE_CONTROL_MAX_AGE_SECS);
+
+    String checksum = staticAsset.getChecksum();
+    if (checksum != null) {
+      resp.setHeader("ETag", checksum);
+    }
+  }
+
+  private boolean checksumMatches(HttpServletRequest req) throws IOException {
+    // TODO(William Farner): Change this to more fully comply with
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
+    // Specifically - a response to 'If-None-Match: *' should include ETag as well as other
+    // cache-related headers.
+    String suppliedETag = req.getHeader("If-None-Match");
+    if ("*".equals(suppliedETag)) {
+      return true;
+    }
+
+    String checksum = staticAsset.getChecksum();
+    // Note - this isn't a completely accurate check since the tag we end up matching against could
+    // theoretically be the actual tag with some extra characters appended.
+    return (checksum != null) && (suppliedETag != null) && suppliedETag.contains(checksum);
+  }
+
+  private static boolean supportsGzip(HttpServletRequest req) {
+    String header = req.getHeader("Accept-Encoding");
+    return (header != null)
+        && Iterables.contains(Splitter.on(",").trimResults().split(header), GZIP_ENCODING);
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ContentionPrinter.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ContentionPrinter.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ContentionPrinter.java
new file mode 100644
index 0000000..1f8c453
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ContentionPrinter.java
@@ -0,0 +1,88 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Longs;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * HTTP request handler that prints information about blocked threads.
+ *
+ * @author William Farner
+ */
+public class ContentionPrinter extends TextResponseHandler {
+  public ContentionPrinter() {
+    ManagementFactory.getThreadMXBean().setThreadContentionMonitoringEnabled(true);
+  }
+
+  @Override
+  public Iterable<String> getLines(HttpServletRequest request) {
+    List<String> lines = Lists.newLinkedList();
+    ThreadMXBean bean = ManagementFactory.getThreadMXBean();
+
+    Map<Long, StackTraceElement[]> threadStacks = Maps.newHashMap();
+    for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
+      threadStacks.put(entry.getKey().getId(), entry.getValue());
+    }
+
+    Set<Long> lockOwners = Sets.newHashSet();
+
+    lines.add("Locked threads:");
+    for (ThreadInfo t : bean.getThreadInfo(bean.getAllThreadIds())) {
+      switch (t.getThreadState()) {
+        case BLOCKED:
+        case WAITING:
+        case TIMED_WAITING:
+          lines.addAll(getThreadInfo(t, threadStacks.get(t.getThreadId())));
+          if (t.getLockOwnerId() != -1) lockOwners.add(t.getLockOwnerId());
+          break;
+      }
+    }
+
+    if (lockOwners.size() > 0) {
+      lines.add("\nLock Owners");
+      for (ThreadInfo t : bean.getThreadInfo(Longs.toArray(lockOwners))) {
+        lines.addAll(getThreadInfo(t, threadStacks.get(t.getThreadId())));
+      }
+    }
+
+    return lines;
+  }
+
+  private static List<String> getThreadInfo(ThreadInfo t, StackTraceElement[] stack) {
+    List<String> lines = Lists.newLinkedList();
+
+    lines.add(String.format("'%s' Id=%d %s",
+        t.getThreadName(), t.getThreadId(), t.getThreadState()));
+    lines.add("Waiting for lock: " + t.getLockName());
+    lines.add("Lock is currently held by thread: " + t.getLockOwnerName());
+    lines.add("Wait time: " + t.getBlockedTime() + " ms.");
+    for (StackTraceElement s : stack) {
+      lines.add(String.format("    " + s.toString()));
+    }
+    lines.add("\n");
+
+    return lines;
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HealthHandler.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HealthHandler.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HealthHandler.java
new file mode 100644
index 0000000..9d9fb9b
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HealthHandler.java
@@ -0,0 +1,82 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.apache.aurora.common.base.ExceptionalSupplier;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A servlet that provides a crude mechanism for monitoring a service's health.  If the servlet
+ * returns {@link #IS_HEALTHY} then the containing service should be deemed healthy.
+ *
+ * @author John Sirois
+ */
+public class HealthHandler extends HttpServlet {
+
+  /**
+   * A {@literal @Named} binding key for the Healthz servlet health checker.
+   */
+  public static final String HEALTH_CHECKER_KEY =
+      "com.twitter.common.net.http.handlers.Healthz.checker";
+
+  /**
+   * The plain text response string this servlet returns in the body of its responses to health
+   * check requests when its containing service is healthy.
+   */
+  public static final String IS_HEALTHY = "OK";
+
+  private static final String IS_NOT_HEALTHY = "SICK";
+
+  private static final Logger LOG = Logger.getLogger(HealthHandler.class.getName());
+
+  private final ExceptionalSupplier<Boolean, ?> healthChecker;
+
+  /**
+   * Constructs a new Healthz that uses the given {@code healthChecker} to determine current health
+   * of the service for at the point in time of each GET request.  The given {@code healthChecker}
+   * should return {@code true} if the service is healthy and {@code false} otherwise.  If the
+   * {@code healthChecker} returns null or throws the service is considered unhealthy.
+   *
+   * @param healthChecker a supplier that is called to perform a health check
+   */
+  @Inject
+  public HealthHandler(@Named(HEALTH_CHECKER_KEY) ExceptionalSupplier<Boolean, ?> healthChecker) {
+    this.healthChecker = Preconditions.checkNotNull(healthChecker);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+
+    resp.setContentType("text/plain");
+    PrintWriter writer = resp.getWriter();
+    try {
+      writer.println(Boolean.TRUE.equals(healthChecker.get()) ? IS_HEALTHY : IS_NOT_HEALTHY);
+    } catch (Exception e) {
+      writer.println(IS_NOT_HEALTHY);
+      LOG.log(Level.WARNING, "Health check failed.", e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HttpServletRequestParams.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HttpServletRequestParams.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HttpServletRequestParams.java
new file mode 100644
index 0000000..c09be67
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/HttpServletRequestParams.java
@@ -0,0 +1,87 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Simple utility for parsing HttpServletRequest parameters by type.
+ */
+public class HttpServletRequestParams {
+  private static final Logger LOG = Logger.getLogger(HttpServletRequestParams.class.getName());
+
+  /**
+   * Parses an int param from an HttpServletRequest, returns a default value
+   * if the parameter is not set or is not a valid int.
+   */
+  public static int getInt(HttpServletRequest request, String param, int defaultValue) {
+    final String value = request.getParameter(param);
+    int result = defaultValue;
+    if (value != null) {
+      try {
+        result = Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        LOG.warning("Invalid int for " + param + ": " + value);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Parses a long param from an HttpServletRequest, returns a defualt value
+   * if the parameter is not set or is not a valid long.
+   */
+  public static long getLong(HttpServletRequest request, String param, long defaultValue) {
+    final String value = request.getParameter(param);
+    long result = defaultValue;
+    if (value != null) {
+      try {
+        result = Long.parseLong(value);
+      } catch (NumberFormatException e) {
+        LOG.warning("Invalid long for " + param + ": " + value);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Parses a bool param from an HttpServletRequest, returns a default value
+   * if the parameter is not set.  Note that any value that is set will be
+   * considered a legal bool by Boolean.valueOf, defualting to false if not
+   * understood.
+   */
+  public static boolean getBool(HttpServletRequest request, String param, boolean defaultValue) {
+    if (request.getParameter(param) != null) {
+      return Boolean.valueOf(request.getParameter(param));
+    } else {
+      return defaultValue;
+    }
+  }
+
+  /**
+   * Returns a string param from an HttpServletRequest if set, returns a defualt value
+   * if the parameter is not set.
+   */
+  @Nullable
+  public static String getString(HttpServletRequest request, String param,
+                                 @Nullable String defaultValue) {
+    if (request.getParameter(param) != null) {
+      return request.getParameter(param);
+    } else {
+      return defaultValue;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogConfig.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogConfig.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogConfig.java
new file mode 100644
index 0000000..5520fb6
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogConfig.java
@@ -0,0 +1,132 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.logging.LoggingMXBean;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.inject.Inject;
+
+import org.antlr.stringtemplate.StringTemplate;
+import org.apache.commons.lang.StringUtils;
+
+import org.apache.aurora.common.base.Closure;
+
+/**
+ * Servlet that allows for dynamic adjustment of the logging configuration.
+ *
+ * @author William Farner
+ */
+public class LogConfig extends StringTemplateServlet {
+  private static final List<String> LOG_LEVELS = Lists.newArrayList(
+      Level.SEVERE.getName(),
+      Level.WARNING.getName(),
+      Level.INFO.getName(),
+      Level.CONFIG.getName(),
+      Level.FINE.getName(),
+      Level.FINER.getName(),
+      Level.FINEST.getName(),
+      "INHERIT" // Display value for a null level, the logger inherits from its ancestor.
+  );
+
+  @Inject
+  public LogConfig(@CacheTemplates boolean cacheTemplates) {
+    super("logconfig", cacheTemplates);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    displayPage(req, resp, true);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    displayPage(req, resp, false);
+  }
+
+  protected void displayPage(final HttpServletRequest req, HttpServletResponse resp,
+      final boolean posted) throws ServletException, IOException {
+    writeTemplate(resp, new Closure<StringTemplate>() {
+      @Override public void execute(StringTemplate stringTemplate) {
+        LoggingMXBean logBean = LogManager.getLoggingMXBean();
+
+        if (posted) {
+          String loggerName = req.getParameter("logger");
+          String loggerLevel = req.getParameter("level");
+          if (loggerName != null && loggerLevel != null) {
+            Logger logger = Logger.getLogger(loggerName);
+            Level newLevel = loggerLevel.equals("INHERIT") ? null : Level.parse(loggerLevel);
+            logger.setLevel(newLevel);
+            if (newLevel != null) {
+              maybeAdjustHandlerLevels(logger, newLevel);
+            }
+
+            stringTemplate.setAttribute("configChange",
+                String.format("%s level changed to %s", loggerName, loggerLevel));
+          }
+        }
+
+        List<LoggerConfig> loggerConfigs = Lists.newArrayList();
+        for (String logger : Ordering.natural().immutableSortedCopy(logBean.getLoggerNames())) {
+          loggerConfigs.add(new LoggerConfig(logger, logBean.getLoggerLevel(logger)));
+        }
+
+        stringTemplate.setAttribute("loggers", loggerConfigs);
+        stringTemplate.setAttribute("levels", LOG_LEVELS);
+      }
+    });
+  }
+
+  private void maybeAdjustHandlerLevels(Logger logger, Level newLevel) {
+    do {
+      for (Handler handler : logger.getHandlers()) {
+        Level handlerLevel = handler.getLevel();
+        if (newLevel.intValue() < handlerLevel.intValue()) {
+          handler.setLevel(newLevel);
+        }
+      }
+    } while (logger.getUseParentHandlers() && (logger = logger.getParent()) != null);
+  }
+
+  private class LoggerConfig {
+    private final String name;
+    private final String level;
+
+    public LoggerConfig(String name, String level) {
+      this.name = name;
+      this.level = StringUtils.isBlank(level) ? "INHERIT" : level;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public String getLevel() {
+      return level;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogPrinter.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogPrinter.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogPrinter.java
new file mode 100644
index 0000000..b014e41
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/LogPrinter.java
@@ -0,0 +1,427 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.RandomAccessFile;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.antlr.stringtemplate.StringTemplate;
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+
+import org.apache.aurora.common.base.Closure;
+import org.apache.aurora.common.base.MorePreconditions;
+import org.apache.aurora.common.quantity.Amount;
+import org.apache.aurora.common.quantity.Data;
+
+/**
+ * HTTP handler to page through log files. Supports GET and POST requests.  GET requests are
+ * responsible for fetching chrome and javascript, while the POST requests are used to fetch actual
+ * log data.
+ */
+public class LogPrinter extends StringTemplateServlet {
+  private static final Logger LOG = Logger.getLogger(LogPrinter.class.getName());
+
+  /**
+   * A {@literal @Named} binding key for the log directory to display by default.
+   */
+  public static final String LOG_DIR_KEY =
+        "com.twitter.common.net.http.handlers.LogPrinter.log_dir";
+
+  private static final int DEFAULT_PAGE = 0;
+
+  private static final int PAGE_CHUNK_SIZE_BYTES = Amount.of(512, Data.KB).as(Data.BYTES);
+  private static final int TAIL_START_BYTES = Amount.of(10, Data.KB).as(Data.BYTES);
+  private static final int PAGE_END_BUFFER_SIZE_BYTES = Amount.of(1, Data.KB).as(Data.BYTES);
+
+  private static final String XML_RESP_FORMAT = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                                              + "<logchunk text=\"%s\""
+                                              + " end_pos=\"%d\">"
+                                              + "</logchunk>";
+  private final File logDir;
+
+  @Inject
+  public LogPrinter(@Named(LOG_DIR_KEY) File logDir, @CacheTemplates boolean cacheTemplates) {
+    super("logprinter", cacheTemplates);
+    this.logDir = Preconditions.checkNotNull(logDir);
+  }
+
+  /**
+   * A POST request is made from javascript, to request the contents of a log file.  In order to
+   * fulfill the request, the 'file' parameter must be set in the request.
+   *
+   * If file starts with a '/' then the file parameter will be treated as an absolute file path.
+   * If file does not start with a '/' then the path will be assumed to be
+   * relative to the log directory.
+   *
+   * @param req Servlet request.
+   * @param resp Servlet response.
+   * @throws ServletException If there is a problem with the servlet.
+   * @throws IOException If there is a problem reading/writing data to the client.
+   */
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    resp.setContentType("text/xml; charset=utf-8");
+
+    try {
+      LogViewRequest request = new LogViewRequest(req);
+
+      if (request.file == null) {
+        // The log file is a required parameter for POST requests.
+        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        return;
+      }
+
+      resp.setStatus(HttpServletResponse.SC_OK);
+      PrintWriter responseBody = resp.getWriter();
+
+      String responseXml = fetchXmlLogContents(request);
+      responseBody.write(responseXml);
+      responseBody.close();
+    } catch (Exception e) {
+      LOG.log(Level.SEVERE, "Unknown exception.", e);
+      resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+  /**
+   * Fetches the chrome for the page.  If a file is requested, a page will be returned that uses an
+   * AJAX request to fetch the log contents.  If no file is specified, then a file listing is
+   * displayed.
+   *
+   * @param req Servlet request.
+   * @param resp Servlet response.
+   * @throws ServletException If there is a problem with the servlet.
+   * @throws IOException If there is a problem reading/writing data to the client.
+   */
+  @Override
+  protected void doGet(final HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    final LogViewRequest request = new LogViewRequest(req);
+
+    if (request.download) {
+      if (request.file == null) {
+        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "No file requested for download.");
+        return;
+      }
+
+      if (!request.file.isRegularFile()) {
+        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Only regular files may be downloaded.");
+        return;
+      }
+
+      try {
+        OutputStream out = resp.getOutputStream();
+        ServletContext context  = getServletConfig().getServletContext();
+        String mimetype = context.getMimeType(request.file.getName());
+
+        resp.setContentType(mimetype != null ? mimetype : "application/octet-stream" );
+        resp.setContentLength((int) request.file.getFile().length());
+        resp.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"",
+            request.file.getName()));
+
+        Files.copy(request.file.getFile(), out);
+      } catch (Exception e) {
+        resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to fetch file.");
+        LOG.warning("Failed to download file " + request.file.getPath() + ": " + e.getMessage());
+      }
+    } else {
+      writeTemplate(resp, new Closure<StringTemplate>() {
+        @Override public void execute(StringTemplate template) {
+
+          // TODO(William Farner): Consider using unix file utility to check if the requested file is a
+          //    text file, and allow the user to download the file if it is not.
+          if (request.isFileViewRequest()) {
+            request.sendToTemplate(template);
+
+            if (!request.tailing) {
+              long readStartPos = getReadStartPos(request.file.getFile(), request.page);
+
+              if (readStartPos > 0) template.setAttribute("prev", request.page + 1);
+              if (request.page > 0) template.setAttribute("next", request.page - 1);
+            }
+          } else {
+            // If a file was not requested, show a list of files.
+            File dir = request.getListingDir();
+
+            List<LogFile> logFiles = Lists.newArrayList();
+            for (File file : dir.listFiles()) {
+              logFiles.add(new LogFile(file));
+            }
+
+            // Sort by dir/file, subsort by name.
+            Collections.sort(logFiles, new Comparator<LogFile>() {
+              @Override public int compare(LogFile fileA, LogFile fileB) {
+                if (fileA.isDir() == fileB.isDir()) {
+                  return fileA.file.getName().compareTo(fileB.file.getName());
+                } else {
+                  return fileA.isDir() ? -1 : 1;
+                }
+              }
+            });
+
+            template.setAttribute("dir", dir);
+            template.setAttribute("parent", dir.getParentFile());
+            template.setAttribute("files", logFiles);
+          }
+        }
+      });
+    }
+  }
+
+  /**
+   * Gets the starting position for reading a page from a file.
+   *
+   * @param file The file to find a page within.
+   * @param page The page index, where page 0 is the last page (at the end of the file).
+   * @return The byte index that the page begins on, or 0 if an invalid page number was provided.
+   */
+  private long getReadStartPos(File file, int page) {
+    return page < 0 ? 0 : Math.max(0, file.length() - (page + 1) * PAGE_CHUNK_SIZE_BYTES);
+  }
+
+  /**
+   * Stores request parameters and assigns default values.
+   */
+  private class LogViewRequest {
+    public static final String DIR_PARAM = "dir";
+    public static final String FILE_PARAM = "file";
+    public static final String PAGE_PARAM = "page";
+    public static final String FILTER_PARAM = "filter";
+    public static final String TAIL_PARAM = "tail";
+    public static final String START_POS_PARAM = "start_pos";
+    public static final String DOWNLOAD_PARAM = "download";
+
+    public final File dir;
+    public final LogFile file;
+    public final boolean download;
+    public final int page;
+    public final long startPos;
+    public final String filter;
+    public final boolean tailing;
+
+    public LogViewRequest(HttpServletRequest req) {
+      dir = req.getParameter(DIR_PARAM) == null ? null : new File(req.getParameter(DIR_PARAM));
+      file = req.getParameter(FILE_PARAM) ==  null ? null
+          : new LogFile(req.getParameter(FILE_PARAM));
+      download = HttpServletRequestParams.getBool(req, DOWNLOAD_PARAM, false);
+      tailing = HttpServletRequestParams.getBool(req, TAIL_PARAM, false);
+      page = HttpServletRequestParams.getInt(req, PAGE_PARAM, DEFAULT_PAGE);
+      Preconditions.checkArgument(page >= 0);
+
+      startPos = HttpServletRequestParams.getLong(req, START_POS_PARAM, -1);
+      if (file != null) {
+        Preconditions.checkArgument(startPos >= -1 && startPos <= file.getFile().length());
+      }
+      filter = HttpServletRequestParams.getString(req, FILTER_PARAM, "");
+    }
+
+    public boolean isFileViewRequest() {
+      return file != null && file.isRegularFile();
+    }
+
+    public File getListingDir() {
+      if (file != null && file.getFile().isDirectory()) {
+        return file.getFile();
+      } else if (dir != null) {
+        return dir;
+      } else {
+        return logDir;
+      }
+    }
+
+    public void sendToTemplate(StringTemplate template) {
+      template.setAttribute(FILE_PARAM, file);
+      template.setAttribute(PAGE_PARAM, page);
+      template.setAttribute(FILTER_PARAM, filter);
+      template.setAttribute(TAIL_PARAM, tailing);
+    }
+  }
+
+  /**
+   * Class to wrap a log file and offer functions to StringTemplate via reflection.
+   */
+   @VisibleForTesting
+   class LogFile {
+    private final File file;
+
+    public LogFile(File file) {
+      this.file = file;
+    }
+
+    public LogFile(String filePath) {
+      MorePreconditions.checkNotBlank(filePath, "filePath must not be null or empty");
+      this.file = filePath.startsWith("/") ? new File(filePath) : new File(logDir, filePath);
+    }
+
+    public File getFile() {
+      return file;
+    }
+
+    public boolean isDir() {
+      return !isRegularFile();
+    }
+
+    public boolean isRegularFile() {
+      return file.isFile();
+    }
+
+    public String getPath() {
+      return file.getAbsolutePath();
+    }
+
+    public String getName() {
+      return file.getName();
+    }
+
+    public String getUrlpath() throws UnsupportedEncodingException {
+      return URLEncoder.encode(getPath(), Charsets.UTF_8.name());
+    }
+
+    public String getSize() {
+      Amount<Long, Data> length = Amount.of(file.length(), Data.BYTES);
+
+      if (length.as(Data.GB) > 0) {
+        return length.as(Data.GB) + " GB";
+      } else if (length.as(Data.MB) > 0) {
+        return length.as(Data.MB) + " MB";
+      } else if (length.as(Data.KB) > 0) {
+        return length.as(Data.KB) + " KB";
+      } else {
+        return length.getValue() + " bytes";
+      }
+    }
+  }
+
+  /**
+   * Reads data from a log file and prepares an XML response which includes the (sanitized) log text
+   * and the last position read from the file.
+   *
+   * @param request The request parameters.
+   * @return A string containing the XML-formatted response.
+   * @throws IOException If there was a problem reading the file.
+   */
+  private String fetchXmlLogContents(LogViewRequest request) throws IOException {
+    RandomAccessFile seekFile = new RandomAccessFile(request.file.getFile(), "r");
+    try {
+      // Move to the approximate start of the page.
+      if (!request.tailing) {
+        seekFile.seek(getReadStartPos(request.file.getFile(), request.page));
+      } else {
+        if (request.startPos < 0) {
+          seekFile.seek(Math.max(0, request.file.getFile().length() - TAIL_START_BYTES));
+        } else {
+          seekFile.seek(request.startPos);
+        }
+      }
+
+      byte[] buffer = new byte[PAGE_CHUNK_SIZE_BYTES];
+      int bytesRead = seekFile.read(buffer);
+      long chunkStop = seekFile.getFilePointer();
+      StringBuilder fileChunk = new StringBuilder();
+      if (bytesRead > 0) {
+        fileChunk.append(new String(buffer, 0, bytesRead));
+
+        // Read at most 1 KB more while searching for another line break.
+        buffer = new byte[PAGE_END_BUFFER_SIZE_BYTES];
+        int newlinePos = 0;
+        bytesRead = seekFile.read(buffer);
+        if (bytesRead > 0) {
+          for (byte b : buffer) {
+            newlinePos++;
+            if (b == '\n') break;
+          }
+
+          fileChunk.append(new String(buffer, 0, newlinePos));
+          chunkStop = seekFile.getFilePointer() - (bytesRead - newlinePos);
+        }
+      }
+
+      return logChunkXml(filterLines(fileChunk.toString(), request.filter), chunkStop);
+    } finally {
+      seekFile.close();
+    }
+  }
+
+  private static String sanitize(String text) {
+    text = StringEscapeUtils.escapeHtml(text);
+
+    StringBuilder newString = new StringBuilder();
+    for (char ch : text.toCharArray()) {
+      if ((ch > 0x001F && ch < 0x00FD) || ch == '\t' || ch == '\r') {
+        // Directly include anything from 0x1F (SPACE) to 0xFD (tilde)
+        // as well as tab and carriage-return.
+        newString.append(ch);
+      } else {
+        // Encode everything else.
+        newString.append("&#").append((int) ch).append(";");
+      }
+    }
+    return StringEscapeUtils.escapeXml(newString.toString());
+  }
+
+  private String logChunkXml(String text, long lastBytePosition) {
+    return String.format(XML_RESP_FORMAT, sanitize(text) , lastBytePosition);
+  }
+
+  @VisibleForTesting
+  protected static String filterLines(String text, String filterRegexp) {
+    if (StringUtils.isEmpty(filterRegexp)) return text;
+
+    List<String> lines = Lists.newArrayList(text.split("\n"));
+    final Pattern pattern = Pattern.compile(filterRegexp);
+
+    Iterable<String> filtered = Iterables.filter(lines, new Predicate<String>() {
+      @Override public boolean apply(String line) {
+        return pattern.matcher(line).matches();
+      }
+    });
+
+    return Joiner.on("\n").join(filtered);
+  }
+
+  private class LogConfigException extends Exception {
+    public LogConfigException(String message) {
+      super(message);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/QuitHandler.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/QuitHandler.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/QuitHandler.java
new file mode 100644
index 0000000..4ce3c97
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/QuitHandler.java
@@ -0,0 +1,71 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * A servlet that provides a way to remotely signal the process to initiate a clean shutdown
+ * sequence.
+ */
+public class QuitHandler extends HttpServlet {
+  private static final Logger LOG = Logger.getLogger(QuitHandler.class.getName());
+
+  /**
+   * A {@literal @Named} binding key for the QuitHandler listener.
+   */
+  public static final String QUIT_HANDLER_KEY =
+      "com.twitter.common.net.http.handlers.QuitHandler.listener";
+
+  private final Runnable quitListener;
+
+  /**
+   * Constructs a new QuitHandler that will notify the given {@code quitListener} when the servlet
+   * is accessed.  It is the responsibility of the listener to initiate a clean shutdown of the
+   * process.
+   *
+   * @param quitListener Runnable to notify when the servlet is accessed.
+   */
+  @Inject
+  public QuitHandler(@Named(QUIT_HANDLER_KEY) Runnable quitListener) {
+    this.quitListener = Preconditions.checkNotNull(quitListener);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    LOG.info(String.format("Received quit HTTP signal from %s (%s)",
+        req.getRemoteAddr(), req.getRemoteHost()));
+
+    resp.setContentType("text/plain");
+    PrintWriter writer = resp.getWriter();
+    try {
+      writer.println("Notifying quit listener.");
+      writer.close();
+      new Thread(quitListener).start();
+    } catch (Exception e) {
+      LOG.log(Level.WARNING, "Quit failed.", e);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/StringTemplateServlet.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/StringTemplateServlet.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/StringTemplateServlet.java
new file mode 100644
index 0000000..60e0abb
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/StringTemplateServlet.java
@@ -0,0 +1,96 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.BindingAnnotation;
+
+import org.apache.aurora.common.base.Closure;
+import org.apache.aurora.common.base.MorePreconditions;
+import org.apache.aurora.common.util.templating.StringTemplateHelper;
+import org.apache.aurora.common.util.templating.StringTemplateHelper.TemplateException;
+
+import org.antlr.stringtemplate.StringTemplate;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A base class for servlets that render using the string template templating system.  Subclasses
+ * can call one of the {@link #writeTemplate} methods to render their content with the associated
+ * template.
+ */
+public abstract class StringTemplateServlet extends HttpServlet {
+  private static final String CONTENT_TYPE_TEXT_HTML = "text/html";
+
+  /**
+   * A {@literal @BindingAnnotation} that allows configuration of whether or not
+   * StringTemplateServlets should cache their templates.
+   */
+  @BindingAnnotation
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.PARAMETER, ElementType.METHOD})
+  public @interface CacheTemplates {}
+
+  private static final Logger LOG = Logger.getLogger(StringTemplateServlet.class.getName());
+
+  private final StringTemplateHelper templateHelper;
+
+  /**
+   * Creates a new StringTemplateServlet that expects to find its template located in the same
+   * package on the classpath at '{@code templateName}.st'.
+   *
+   * @param templateName The name of the string template to use.
+   * @param cacheTemplates {@code true} to re-use loaded templates, {@code false} to reload the
+   *     template for each request.
+   */
+  protected StringTemplateServlet(String templateName, boolean cacheTemplates) {
+    templateHelper = new StringTemplateHelper(getClass(), templateName, cacheTemplates);
+  }
+
+  protected final void writeTemplate(
+      HttpServletResponse response,
+      Closure<StringTemplate> parameterSetter) throws IOException {
+
+    writeTemplate(response, CONTENT_TYPE_TEXT_HTML, HttpServletResponse.SC_OK, parameterSetter);
+  }
+
+  protected final void writeTemplate(
+      HttpServletResponse response,
+      String contentType,
+      int status,
+      Closure<StringTemplate> parameterSetter) throws IOException {
+
+    Preconditions.checkNotNull(response);
+    MorePreconditions.checkNotBlank(contentType);
+    Preconditions.checkArgument(status > 0);
+    Preconditions.checkNotNull(parameterSetter);
+
+    try {
+      templateHelper.writeTemplate(response.getWriter(), parameterSetter);
+      response.setStatus(status);
+      response.setContentType(contentType);
+    } catch (TemplateException e) {
+      LOG.log(Level.SEVERE, "Unknown exception.", e);
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/TextResponseHandler.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/TextResponseHandler.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/TextResponseHandler.java
new file mode 100644
index 0000000..23068eb
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/TextResponseHandler.java
@@ -0,0 +1,58 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * A handler that responds to all requests in HTML format.
+ *
+ * @author William Farner
+ */
+public abstract class TextResponseHandler extends HttpServlet {
+  private final String textContentType;
+
+  public TextResponseHandler() {
+    this("text/plain");
+  }
+
+  public TextResponseHandler(String textContentType) {
+    this.textContentType = textContentType;
+  }
+
+  /**
+   * Returns the lines to be printed as the body of the response.
+   *
+   * @return An iterable collection of lines to respond to the request with.
+   */
+  public abstract Iterable<String> getLines(HttpServletRequest request);
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+
+    resp.setContentType(textContentType);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    PrintWriter responseBody = resp.getWriter();
+    for (String line : getLines(req)) {
+      responseBody.println(line);
+    }
+    responseBody.close();
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThreadStackPrinter.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThreadStackPrinter.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThreadStackPrinter.java
new file mode 100644
index 0000000..5dd8804
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThreadStackPrinter.java
@@ -0,0 +1,41 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import com.google.common.collect.Lists;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * HTTP handler to print the stacks of all live threads.
+ *
+ * @author William Farner
+ */
+public class ThreadStackPrinter extends TextResponseHandler {
+  @Override
+  public Iterable<String> getLines(HttpServletRequest request) {
+    List<String> lines = Lists.newLinkedList();
+    for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
+      Thread t = entry.getKey();
+      lines.add(String.format("Name: %s\nState: %s\nDaemon: %s\nID: %d",
+          t.getName(), t.getState(), t.isDaemon(), t.getId()));
+      for (StackTraceElement s : entry.getValue()) {
+        lines.add("    " + s.toString());
+      }
+    }
+    return lines;
+  }
+}

http://git-wip-us.apache.org/repos/asf/aurora/blob/06ddaadb/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThriftServlet.java
----------------------------------------------------------------------
diff --git a/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThriftServlet.java b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThriftServlet.java
new file mode 100644
index 0000000..540ab69
--- /dev/null
+++ b/commons/src/main/java/org/apache/aurora/common/net/http/handlers/ThriftServlet.java
@@ -0,0 +1,70 @@
+/**
+ * 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 org.apache.aurora.common.net.http.handlers;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.apache.aurora.common.base.Closure;
+import org.apache.aurora.common.net.monitoring.TrafficMonitor;
+import org.antlr.stringtemplate.StringTemplate;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Servlet to display live information about registered thrift clients and servers.
+ *
+ * @author William Farner
+ */
+public class ThriftServlet extends StringTemplateServlet {
+
+  /**
+   * {@literal @Named} binding key for client monitor.
+   */
+  public static final String THRIFT_CLIENT_MONITORS =
+      "com.twitter.common.net.http.handlers.ThriftServlet.THRIFT_CLIENT_MONITORS";
+
+  /**
+   * {@literal @Named} binding key for server monitor.
+   */
+  public static final String THRIFT_SERVER_MONITORS =
+      "com.twitter.common.net.http.handlers.ThriftServlet.THRIFT_SERVER_MONITORS";
+
+  private Set<TrafficMonitor> clientMonitors;
+  private Set<TrafficMonitor> serverMonitors;
+
+  @Inject
+  public ThriftServlet(
+      @Named(ThriftServlet.THRIFT_CLIENT_MONITORS) Set<TrafficMonitor> clientMonitors,
+      @Named(ThriftServlet.THRIFT_SERVER_MONITORS) Set<TrafficMonitor> serverMonitors) {
+    super("thrift", true);
+    this.clientMonitors = Preconditions.checkNotNull(clientMonitors);
+    this.serverMonitors = Preconditions.checkNotNull(serverMonitors);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    writeTemplate(resp, new Closure<StringTemplate>() {
+      @Override public void execute(StringTemplate template) {
+        template.setAttribute("clientMonitors", clientMonitors);
+        template.setAttribute("serverMonitors", serverMonitors);
+      }
+    });
+  }
+}