All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.ostrich.admin.AdminHttpService.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2009 Twitter, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.twitter.ostrich.admin

import com.sun.net.httpserver.{HttpExchange, HttpHandler, HttpServer}
import com.twitter.concurrent.NamedPoolThreadFactory
import com.twitter.conversions.time._
import com.twitter.json.Json
import com.twitter.ostrich.stats.{StatsCollection, Stats}
import com.twitter.logging.Logger
import com.twitter.util.{Duration, NonFatal, Return, Throw}
import com.twitter.util.registry.{Library, GlobalRegistry, Formatter, Registry}
import java.io.{InputStream, OutputStream}
import java.net.{InetSocketAddress, Socket, URI}
import java.util.concurrent.Executors
import java.util.Properties
import scala.io.Source

/**
 * Custom handler interface for the admin web site. The standard `render` calls are implemented in
 * terms of a single `handle` call. For more functionality, check out subclasses like
 * `FolderResourceHandler` and `CgiRequestHandler`.
 */
abstract class CustomHttpHandler extends HttpHandler {
  private val log = Logger.get(getClass)
  private val properties = new Properties()
  properties.load(getClass.getResourceAsStream("/ostrich.properties"))

  def render(body: String, exchange: HttpExchange) {
    render(body, exchange, 200)
  }

  def render(body: String, exchange: HttpExchange, code: Int) {
    render(body, exchange, code, "text/html")
  }

  def render(body: String, exchange: HttpExchange, code: Int, contentType: String) {
    val input: InputStream = exchange.getRequestBody()
    val output: OutputStream = exchange.getResponseBody()
    exchange.getResponseHeaders.set("Content-Type", contentType)
    exchange.getResponseHeaders.set("X-Ostrich-Version", properties.getProperty("version"))
    val data = body.getBytes
    exchange.sendResponseHeaders(code, data.size)
    output.write(data)
    output.flush()
    output.close()
    exchange.close()
  }

  def loadResource(name: String) = {
    log.debug("Loading resource from file: %s", name)
    val stream = getClass.getResourceAsStream(name)
    try {
      Source.fromInputStream(stream).mkString
    } catch {
      case e: Throwable =>
        log.error(e, "Unable to load Resource from Classpath: %s", name)
        throw e
    }
  }

  def handle(exchange: HttpExchange): Unit
}

class MissingFileHandler extends CustomHttpHandler {
  def handle(exchange: HttpExchange) {
    render("no such file", exchange, 404)
  }
}

class PageResourceHandler(path: String) extends CustomHttpHandler {
  lazy val page = loadResource(path)

  def handle(exchange: HttpExchange) {
    render(page, exchange)
  }
}

/**
 * Serve static pages as java resources.
 */
class FolderResourceHandler(staticPath: String) extends CustomHttpHandler {
  /**
   * Given a requestPath (e.g. /static/digraph.js), break it up into the path and filename
   */
  def getRelativePath(requestPath: String): String = {
    if (requestPath.startsWith(staticPath)) {
      requestPath.substring(staticPath.length + 1)
    } else {
      requestPath
    }
  }

  def buildPath(relativePath: String) = staticPath + "/" + relativePath

  def handle(exchange: HttpExchange) {
    val requestPath = exchange.getRequestURI().getPath()
    val relativePath = getRelativePath(requestPath)

    val contentType = if (relativePath.endsWith(".js")) {
      "text/javascript"
    } else if (relativePath.endsWith(".html")) {
      "text/html"
    } else if (relativePath.endsWith(".css")) {
      "text/css"
    } else {
      "application/unknown"
    }

    render(loadResource(buildPath(relativePath)), exchange, 200, contentType)
  }
}

object CgiRequestHandler {
  def exchangeToParameters(exchange: HttpExchange): List[(String, String)] =
    Option(exchange.getRequestURI) match {
      case Some(uri) => uriToParameters(uri)
      case None      => Nil
    }

  def uriToParameters(uri: URI): List[(String, String)] = {
    Option(uri.getQuery).getOrElse("").split("&").toList.filter { _.contains("=") }.map { param =>
      param.split("=", 2).toList match {
        case k :: v :: Nil => (k, v)
        case k :: Nil => (k, "")
        case _ => ("", "") // won't happen, but stops the compiler from whining.
      }
    }
  }
}

abstract class CgiRequestHandler extends CustomHttpHandler {
  import CgiRequestHandler._

  private val log = Logger(getClass.getName)

  def handle(exchange: HttpExchange) {
    try {
      val requestURI = exchange.getRequestURI
      val path       = requestURI.getPath.split('/').toList.filter { _.length > 0 }
      val parameters = exchangeToParameters(exchange)

      handle(exchange, path, parameters)
    } catch {
      case NonFatal(e) =>
        render("exception while processing request: " + e, exchange, 500)
        log.error(e, "Exception processing admin http request")
    }
  }

  def handle(exchange: HttpExchange, path: List[String], parameters: List[(String, String)])
}

/**
 * Deal with requests from the mesos executor
 */
object MesosRequestHandler {
  type SystemExitImpl = Int=>Unit
  object SystemExitImpl {
    val GracePeriod: Duration = 10.seconds

    val Default: SystemExitImpl = { code =>
      // NB: there is a race here between exiting cleanly and logging about not exiting cleanly
      Thread.sleep(GracePeriod.inMilliseconds)
      System.err.println(
        "Did not exit cleanly after %s: now calling System.exit.".format(GracePeriod)
      )
      System.exit(code)
    }
  }
}

case class MesosRequestHandler(
  systemExitImpl: MesosRequestHandler.SystemExitImpl
) extends CgiRequestHandler {

  def handle(exchange: HttpExchange, path: List[String], parameters: List[(String, String)]) {
    val response = path match {
      case List("health") =>
        "OK\n"
      case List("quitquitquit") =>
        // attempt to shutdown cleanly. note that this may leave the server in an unrecoverable
        // state: any untracked daemon threads will result in the server becoming half dead
        BackgroundProcess.spawn("admin:quiesce") {
          Thread.sleep(100)
          ServiceTracker.quiesce()
        }
        "quitting\n"
      case List("abortabortabort") =>
        // attempt to shutdown cleanly before eventually System.exit()'ing
        BackgroundProcess.spawn("admin:shutdown", true) {
          Thread.sleep(100)
          ServiceTracker.shutdown()
          systemExitImpl(-1)
        }
        "aborting\n"
      case _ =>
        "unknown command"
    }
    render(response, exchange, 200, "text/plain")
  }
}

class HeapResourceHandler extends CgiRequestHandler {
  private val log = Logger(getClass.getName)
  case class Params(pause: Duration, samplingPeriod: Int, forceGC: Boolean)

  def handle(exchange: HttpExchange, path: List[String], parameters: List[(String, String)]) {
    if (!Heapster.instance.isDefined) {
      render("heapster not loaded!", exchange)
      return
    }
    val heapster = Heapster.instance.get

    val params =
      parameters.foldLeft(Params(10.seconds, 10 << 19, true)) {
        case (params, ("pause", pauseVal)) =>
          params.copy(pause = pauseVal.toInt.seconds)
        case (params, ("sample_period", sampleVal)) =>
          params.copy(samplingPeriod = sampleVal.toInt)
        case (params, ("force_gc", "no")) =>
          params.copy(forceGC = false)
        case (params, ("force_gc", "0")) =>
          params.copy(forceGC = false)
        case (params, _) =>
          params
      }

    log.info("collecting heap profile for %s seconds".format(params.pause))
    val profile = heapster.profile(params.pause, params.samplingPeriod, params.forceGC)

    // Write out the profile verbatim. It's a pprof "raw" profile.
    exchange.getResponseHeaders.set("Content-Type", "pprof/raw")
    exchange.sendResponseHeaders(200, profile.size)
    val output: OutputStream = exchange.getResponseBody()
    output.write(profile)
    output.flush()
    output.close()
    exchange.close()
  }
}

class ProfileResourceHandler(which: Thread.State) extends CgiRequestHandler {
  import com.twitter.jvm.CpuProfile

  private val log = Logger(getClass.getName)
  case class Params(pause: Duration, frequency: Int)

  def handle(exchange: HttpExchange, path: List[String], parameters: List[(String, String)]) {
    val params =
      parameters.foldLeft(Params(10.seconds, 100)) {
        case (params, ("seconds", pauseVal)) =>
          params.copy(pause = pauseVal.toInt.seconds)
        case (params, ("hz", hz)) =>
          params.copy(frequency = hz.toInt)
        case (params, _) =>
          params
      }

    log.info("collecting CPU profile (%s) for %s seconds at %dHz".format(
      which, params.pause, params.frequency))
    CpuProfile.recordInThread(params.pause, params.frequency, which) respond {
      case Return(prof) =>
        // Write out the profile verbatim. It's a pprof "raw" profile.
        exchange.getResponseHeaders.set("Content-Type", "pprof/raw")
        exchange.sendResponseHeaders(200, 0)
        val output = exchange.getResponseBody()
        prof.writeGoogleProfile(output)
        output.close()
        exchange.close()

      case Throw(exc) =>
        exchange.sendResponseHeaders(500, 0)
        val output = exchange.getResponseBody()
        output.write(exc.toString.getBytes)
        output.close()
        exchange.close()
    }
  }
}

/**
 * Can turn trace recording on and off for the entire service.
 */
class TracingHandler extends CgiRequestHandler {
  private val log = Logger(getClass.getName)

  def handle(exchange: HttpExchange, path: List[String], params: List[(String, String)]) {
    try {
      if (!FinagleTracing.instance.isDefined) {
        render("Finagle tracing not found!", exchange)
        return
      }
    } catch {
      case NonFatal(_) =>
        render("Could not initialize Finagle tracing classes. Possibly old version of Finagle.",
          exchange)
        return
    }

    val tracing = FinagleTracing.instance.get
    val paramsMap = params.toMap
    val msg = if (paramsMap.get("enable").equals(Some("true"))) {
      tracing.enable()
      "Enabled Finagle tracing"
    } else if (paramsMap.get("disable").equals(Some("true"))) {
      tracing.disable()
      "Disabling Finagle tracing"
    } else {
      "Could not figure out what you wanted to do with tracing. " +
        "Either enable or disable it. This is what we got: " + params
    }

    log.info(msg)
    render(msg, exchange)
  }
}

/**
 * Displays registry data for the service.
 */
class RegistryHandler(registry: Registry) extends CgiRequestHandler {
  def handle(exchange: HttpExchange, path: List[String], params: List[(String, String)]) {
    val obj = Formatter.asMap(registry)
    val json = Json.build(obj).toString + "\n"
    render(json, exchange, 200, "application/json")
  }
}

class CommandRequestHandler(commandHandler: CommandHandler) extends CgiRequestHandler {
  def handle(exchange: HttpExchange, path: List[String], parameters: List[(String, String)]) {
    if (path == Nil) {
      render(loadResource("/static/index.html"), exchange)
      return
    }

    val command = path.last.split('.').head
    val format: Format = path.last.split('.').last match {
      case "txt" => Format.PlainText
      case _ => Format.Json
    }

    val parameterMap = Map(parameters: _*)
    try {
      val response = {
        val commandResponse = commandHandler(command, parameterMap, format)

        if (parameterMap.keySet.contains("callback") && (format == Format.Json)) {
          val callbackName = parameterMap.get("callback") match {
              case Some("true") => "ostrichCallback"
              case Some("") => "ostrichCallback" // Just in case callback= shows up
              case Some(x) => x
              case None => "ostrichCallBack" // This shouldn't happen
          }
          "%s(%s)".format(callbackName,commandResponse)
        } else {
          commandResponse
        }
      }

      val contentType = if (format == Format.PlainText) "text/plain" else "application/json"
      render(response, exchange, 200, contentType)
    } catch {
      case e: UnknownCommandError =>
        render("no such command\n", exchange, 404)
      case e: InvalidCommandOptionError =>
        render(e.getMessage + '\n', exchange, 400)
      case NonFatal(unknownException) =>
        render("error processing command: " + unknownException, exchange, 500)
        unknownException.printStackTrace()
    }
  }
}

object AdminHttpService {
  private val defaultExecutor = Executors.newCachedThreadPool(
    new NamedPoolThreadFactory("ostrichAdmin", makeDaemons = true))
}

class AdminHttpService private[ostrich](
  val port: Int,
  backlog: Int,
  statsCollection: StatsCollection,
  serverInfo: ServerInfoHandler,
  statsListenerMinPeriod: Duration,
  systemExitImpl: MesosRequestHandler.SystemExitImpl,
  registry: Registry
) extends Service {

  def this(
    port: Int,
    backlog: Int,
    statsCollection: StatsCollection,
    serverInfo: ServerInfoHandler
  ) = this(
    port,
    backlog,
    statsCollection,
    serverInfo,
    1.minute,
    MesosRequestHandler.SystemExitImpl.Default,
    GlobalRegistry.get
  )

  def this(
    port: Int,
    backlog: Int,
    statsCollection: StatsCollection,
    serverInfo: ServerInfoHandler,
    statsListenerMinPeriod: Duration,
    systemExitImpl: MesosRequestHandler.SystemExitImpl
  ) = this(
    port,
    backlog,
    statsCollection,
    serverInfo,
    statsListenerMinPeriod,
    systemExitImpl,
    GlobalRegistry.get
  )

  @deprecated("Runtime evaluation of scala code will not be supported going forward. " +
    "Please switch to flags for configuration.", "2015-03-12")
  def this(
    port: Int,
    backlog: Int,
    statsCollection: StatsCollection,
    runtime: RuntimeEnvironment,
    statsListenerMinPeriod: Duration = 1.minute,
    systemExitImpl: MesosRequestHandler.SystemExitImpl = MesosRequestHandler.SystemExitImpl.Default
  ) = this(
    port,
    backlog,
    statsCollection,
    runtime.serverInfo,
    statsListenerMinPeriod,
    systemExitImpl,
    GlobalRegistry.get
  )

  @deprecated("Runtime evaluation of scala code will not be supported going forward. " +
    "Please switch to flags for configuration.", "2015-03-12")
  def this(port: Int, backlog: Int, runtime: RuntimeEnvironment) =
    this(port, backlog, Stats, runtime)

  val log = Logger(getClass)
  val httpServer: HttpServer = HttpServer.create(new InetSocketAddress(port), backlog)
  val commandHandler = new CommandHandler(serverInfo, statsCollection, statsListenerMinPeriod)
  val mesosHandler = new MesosRequestHandler(systemExitImpl)

  def address = httpServer.getAddress

  addContext("/", new CommandRequestHandler(commandHandler))
  addContext("/report/", new PageResourceHandler("/report_request_handler.html"))
  addContext("/favicon.ico", new MissingFileHandler())
  addContext("/static", new FolderResourceHandler("/static"))
  addContext("/pprof/heap", new HeapResourceHandler)
  addContext("/pprof/profile", new ProfileResourceHandler(Thread.State.RUNNABLE))
  addContext("/pprof/contention", new ProfileResourceHandler(Thread.State.BLOCKED))
  addContext("/tracing", new TracingHandler)
  addContext("/admin/registry", new RegistryHandler(registry))
  addContext("/health", mesosHandler)
  addContext("/quitquitquit", mesosHandler)
  addContext("/abortabortabort", mesosHandler)

  private[this] def setPropertyIfNull(property: String, value: Int): Unit = {
    if (System.getProperty(property) == null)
      System.setProperty(property, value.toString)
  }
  // See meaning of those properties here:
  // http://www.docjar.com/html/api/sun/net/httpserver/ServerConfig.java.html
  setPropertyIfNull("sun.net.httpserver.clockTick", 1000)
  setPropertyIfNull("sun.net.httpserver.timerMillis", 1000)
  setPropertyIfNull("sun.net.httpserver.maxReqTime", 25)
  setPropertyIfNull("sun.net.httpserver.maxIdleConnections", 10)
  setPropertyIfNull("sun.net.httpserver.maxRspTime", 60)
  httpServer.setExecutor(AdminHttpService.defaultExecutor)

  def addContext(path: String, handler: HttpHandler) = httpServer.createContext(path, handler)

  def addContext(path: String)(generator: () => String) = {
    val handler = new CustomHttpHandler {
      def handle(exchange: HttpExchange) {
        render(generator(), exchange)
      }
    }
    httpServer.createContext(path, handler)
  }

  def handleRequest(socket: Socket) { }

  def start() {
    ServiceTracker.register(this)
    httpServer.start()
    log.info("Admin HTTP interface started on port %d.", address.getPort)
    Library.register("ostrich", Map.empty)
  }

  override def shutdown(): Unit = {
    httpServer.stop(0) // argument is in seconds
  }

  override def quiesce(): Unit = {
    httpServer.stop(1) // argument is in seconds
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy