Return-Path: X-Original-To: apmail-aurora-commits-archive@minotaur.apache.org Delivered-To: apmail-aurora-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 8079D1825C for ; Tue, 25 Aug 2015 18:19:17 +0000 (UTC) Received: (qmail 74033 invoked by uid 500); 25 Aug 2015 18:19:17 -0000 Delivered-To: apmail-aurora-commits-archive@aurora.apache.org Received: (qmail 73956 invoked by uid 500); 25 Aug 2015 18:19:17 -0000 Mailing-List: contact commits-help@aurora.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@aurora.apache.org Delivered-To: mailing list commits@aurora.apache.org Received: (qmail 72877 invoked by uid 99); 25 Aug 2015 18:19:16 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 25 Aug 2015 18:19:16 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 535EAE362D; Tue, 25 Aug 2015 18:19:16 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: zmanji@apache.org To: commits@aurora.apache.org Date: Tue, 25 Aug 2015 18:19:42 -0000 Message-Id: <875b794aaa6f4c0f956d12adce5c7ac2@git.apache.org> In-Reply-To: <2d86d301903d4a1c81757199842a5e58@git.apache.org> References: <2d86d301903d4a1c81757199842a5e58@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [28/37] aurora git commit: Import of Twitter Commons. http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/net/http/GuiceServletConfig.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/GuiceServletConfig.java b/commons/src/main/java/com/twitter/common/net/http/GuiceServletConfig.java new file mode 100644 index 0000000..98bc200 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/GuiceServletConfig.java @@ -0,0 +1,44 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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/86a547b9/commons/src/main/java/com/twitter/common/net/http/HttpServerDispatch.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/HttpServerDispatch.java b/commons/src/main/java/com/twitter/common/net/http/HttpServerDispatch.java new file mode 100644 index 0000000..efe9134 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/HttpServerDispatch.java @@ -0,0 +1,126 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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 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 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/JettyHttpServerDispatch.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/JettyHttpServerDispatch.java b/commons/src/main/java/com/twitter/common/net/http/JettyHttpServerDispatch.java new file mode 100644 index 0000000..d8c1bdf --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/JettyHttpServerDispatch.java @@ -0,0 +1,286 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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.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 com.twitter.common.base.MorePreconditions; +import com.twitter.common.net.http.handlers.TextResponseHandler; + +/** + * 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 registeredEndpoints = Sets.newHashSet(); + + private final Optional 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 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 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 getLines(HttpServletRequest request) { + List lines = Lists.newArrayList(); + lines.add(""); + for (String handler : Ordering.natural().sortedCopy(registeredEndpoints)) { + lines.add(String.format("%s
", handler, handler)); + } + lines.add(""); + return lines; + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/net/http/RequestLogger.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/RequestLogger.java b/commons/src/main/java/com/twitter/common/net/http/RequestLogger.java new file mode 100644 index 0000000..f9c702c --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/RequestLogger.java @@ -0,0 +1,117 @@ +package com.twitter.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 com.twitter.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/86a547b9/commons/src/main/java/com/twitter/common/net/http/filters/AbstractHttpFilter.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/filters/AbstractHttpFilter.java b/commons/src/main/java/com/twitter/common/net/http/filters/AbstractHttpFilter.java new file mode 100644 index 0000000..a4a4d31 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/filters/AbstractHttpFilter.java @@ -0,0 +1,50 @@ +package com.twitter.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/86a547b9/commons/src/main/java/com/twitter/common/net/http/filters/HttpStatsFilter.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/filters/HttpStatsFilter.java b/commons/src/main/java/com/twitter/common/net/http/filters/HttpStatsFilter.java new file mode 100644 index 0000000..3af2e8d --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/filters/HttpStatsFilter.java @@ -0,0 +1,145 @@ +package com.twitter.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 com.twitter.common.collections.Pair; +import com.twitter.common.stats.SlidingStats; +import com.twitter.common.stats.Stats; +import com.twitter.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, SlidingStats> requestCounters = + CacheBuilder.newBuilder() + .build(new CacheLoader, SlidingStats>() { + @Override + public SlidingStats load(Pair identifierAndStatus) { + return new SlidingStats("http_" + identifierAndStatus.getFirst() + "_" + + identifierAndStatus.getSecond() + "_responses", "nanos"); + } + }); + + @Context private HttpServletRequest servletRequest; + + @VisibleForTesting + final LoadingCache statusCounters = CacheBuilder.newBuilder() + .build(new CacheLoader() { + @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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/AbortHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/AbortHandler.java b/commons/src/main/java/com/twitter/common/net/http/handlers/AbortHandler.java new file mode 100644 index 0000000..bfacf4d --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/AbortHandler.java @@ -0,0 +1,74 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/AssetHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/AssetHandler.java b/commons/src/main/java/com/twitter/common/net/http/handlers/AssetHandler.java new file mode 100644 index 0000000..3345932 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/AssetHandler.java @@ -0,0 +1,192 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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 com.twitter.common.quantity.Amount; +import com.twitter.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 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 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 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/ContentionPrinter.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/ContentionPrinter.java b/commons/src/main/java/com/twitter/common/net/http/handlers/ContentionPrinter.java new file mode 100644 index 0000000..823d651 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/ContentionPrinter.java @@ -0,0 +1,91 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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 getLines(HttpServletRequest request) { + List lines = Lists.newLinkedList(); + ThreadMXBean bean = ManagementFactory.getThreadMXBean(); + + Map threadStacks = Maps.newHashMap(); + for (Map.Entry entry : Thread.getAllStackTraces().entrySet()) { + threadStacks.put(entry.getKey().getId(), entry.getValue()); + } + + Set 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 getThreadInfo(ThreadInfo t, StackTraceElement[] stack) { + List 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/HealthHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/HealthHandler.java b/commons/src/main/java/com/twitter/common/net/http/handlers/HealthHandler.java new file mode 100644 index 0000000..360af9a --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/HealthHandler.java @@ -0,0 +1,85 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.common.net.http.handlers; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.twitter.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 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 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/HttpServletRequestParams.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/HttpServletRequestParams.java b/commons/src/main/java/com/twitter/common/net/http/handlers/HttpServletRequestParams.java new file mode 100644 index 0000000..51cde9f --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/HttpServletRequestParams.java @@ -0,0 +1,74 @@ +package com.twitter.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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/LogConfig.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/LogConfig.java b/commons/src/main/java/com/twitter/common/net/http/handlers/LogConfig.java new file mode 100644 index 0000000..88c6de1 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/LogConfig.java @@ -0,0 +1,135 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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 com.twitter.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 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() { + @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 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/LogPrinter.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/LogPrinter.java b/commons/src/main/java/com/twitter/common/net/http/handlers/LogPrinter.java new file mode 100644 index 0000000..56ef2ab --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/LogPrinter.java @@ -0,0 +1,414 @@ +package com.twitter.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 com.twitter.common.base.Closure; +import com.twitter.common.base.MorePreconditions; +import com.twitter.common.quantity.Amount; +import com.twitter.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 = "" + + "" + + ""; + 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() { + @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 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() { + @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 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 lines = Lists.newArrayList(text.split("\n")); + final Pattern pattern = Pattern.compile(filterRegexp); + + Iterable filtered = Iterables.filter(lines, new Predicate() { + @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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/QuitHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/QuitHandler.java b/commons/src/main/java/com/twitter/common/net/http/handlers/QuitHandler.java new file mode 100644 index 0000000..3e82e99 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/QuitHandler.java @@ -0,0 +1,74 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/StringTemplateServlet.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/StringTemplateServlet.java b/commons/src/main/java/com/twitter/common/net/http/handlers/StringTemplateServlet.java new file mode 100644 index 0000000..816d274 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/StringTemplateServlet.java @@ -0,0 +1,83 @@ +package com.twitter.common.net.http.handlers; + +import com.google.common.base.Preconditions; +import com.google.inject.BindingAnnotation; + +import com.twitter.common.base.Closure; +import com.twitter.common.base.MorePreconditions; +import com.twitter.common.util.templating.StringTemplateHelper; +import com.twitter.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 parameterSetter) throws IOException { + + writeTemplate(response, CONTENT_TYPE_TEXT_HTML, HttpServletResponse.SC_OK, parameterSetter); + } + + protected final void writeTemplate( + HttpServletResponse response, + String contentType, + int status, + Closure 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/TextResponseHandler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/TextResponseHandler.java b/commons/src/main/java/com/twitter/common/net/http/handlers/TextResponseHandler.java new file mode 100644 index 0000000..c2ef473 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/TextResponseHandler.java @@ -0,0 +1,61 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/ThreadStackPrinter.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/ThreadStackPrinter.java b/commons/src/main/java/com/twitter/common/net/http/handlers/ThreadStackPrinter.java new file mode 100644 index 0000000..79965f6 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/ThreadStackPrinter.java @@ -0,0 +1,44 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.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 getLines(HttpServletRequest request) { + List lines = Lists.newLinkedList(); + for (Map.Entry 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/86a547b9/commons/src/main/java/com/twitter/common/net/http/handlers/ThriftServlet.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/net/http/handlers/ThriftServlet.java b/commons/src/main/java/com/twitter/common/net/http/handlers/ThriftServlet.java new file mode 100644 index 0000000..f2eef26 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/net/http/handlers/ThriftServlet.java @@ -0,0 +1,73 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ================================================================================================= + +package com.twitter.common.net.http.handlers; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import com.twitter.common.base.Closure; +import com.twitter.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 clientMonitors; + private Set serverMonitors; + + @Inject + public ThriftServlet( + @Named(ThriftServlet.THRIFT_CLIENT_MONITORS) Set clientMonitors, + @Named(ThriftServlet.THRIFT_SERVER_MONITORS) Set 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() { + @Override public void execute(StringTemplate template) { + template.setAttribute("clientMonitors", clientMonitors); + template.setAttribute("serverMonitors", serverMonitors); + } + }); + } +}