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);
+ }
+ });
+ }
+}