Skip to content

Commit

Permalink
twitter-server: Restructure LoadBalancersHandler Logic
Browse files Browse the repository at this point in the history
Problem / Solution

LoadBalancer data can be rendered as either JSON or HTML, and
all the logic for doing so is contained within the `LoadBalancerHandler`.
As we restructure endpoints within the Twitter Server Admin Interface, let's
separate out the action, from the data model, from the rendered content to
make it easier to work with moving forward.

JIRA Issues: CSL-9014

Differential Revision: https://phabricator.twitter.biz/D392168
  • Loading branch information
ryanoneill authored and jenkins committed Oct 31, 2019
1 parent 4e8df0a commit a5bce43
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import com.twitter.server.model.ClientProfile
import com.twitter.server.util.HtmlUtils.escapeHtml
import com.twitter.server.util.HttpUtils.{parse, new404, newResponse}
import com.twitter.server.util.MetricSource
import com.twitter.server.view.{EndpointRegistryView, StackRegistryView}
import com.twitter.server.view.{BalancerHtmlView, EndpointRegistryView, StackRegistryView}
import com.twitter.util.Future

private object ClientRegistryHandler {
Expand Down Expand Up @@ -146,7 +146,10 @@ class ClientRegistryHandler(
val scope = findClientScope(client.name)
val stackHtml = StackRegistryView.render(client, scope)

val loadBalancerHtml = LoadBalancersHandler.renderHtml(name)
val loadBalancerData = LoadBalancersHandler.getBalancer(Some(name))
val loadBalancerView =
new BalancerHtmlView(loadBalancerData, LoadBalancersHandler.RoutePath)
val loadBalancerHtml = loadBalancerView.render

val endpointEntry = EndpointRegistry.registry.endpoints(name)
val endpointHtml = EndpointRegistryView.render(endpointEntry)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package com.twitter.server.handler

import com.twitter.finagle.Service
import com.twitter.finagle.http.{Request, Response}
import com.twitter.finagle.loadbalancer.BalancerRegistry
import com.twitter.server.util.HtmlUtils.escapeHtml
import com.twitter.finagle.loadbalancer.{BalancerRegistry, Metadata}
import com.twitter.server.util.HttpUtils.newOk
import com.twitter.server.util.JsonConverter
import com.twitter.server.view.BalancersJsonView
import com.twitter.util.Future

/**
Expand All @@ -15,72 +14,27 @@ import com.twitter.util.Future
* e.g. "/admin/balancers.json?label=cool_service"
*/
final class LoadBalancersHandler extends Service[Request, Response] {
def apply(request: Request): Future[Response] = {
val labelFilter = request.params.get("label")
newOk(jsonResponse(labelFilter))
}

private def jsonResponse(labelFilter: Option[String]): String = {
val filtered = labelFilter match {
case None =>
BalancerRegistry.get.allMetadata
case Some(label) =>
BalancerRegistry.get.allMetadata.filter(_.label == label)
}
val mds = filtered.map { md =>
Map(
"label" -> md.label,
"info" -> Map(
"balancer_class" -> md.balancerClass,
"status" -> md.status,
"number_available" -> md.numAvailable,
"number_busy" -> md.numBusy,
"number_closed" -> md.numClosed,
"total_pending" -> md.totalPending,
"total_load" -> md.totalLoad,
"size" -> md.size,
"additional_info" -> md.additionalInfo
)
)
}
import LoadBalancersHandler._

val asMap: Map[String, Object] = Map("clients" -> mds)
JsonConverter.writeToString(asMap)
def apply(request: Request): Future[Response] = {
val filter = request.params.get("label")
val balancers = getBalancers(filter)
val view = new BalancersJsonView(balancers)
val content = view.render
newOk(content)
}

}

private[server] object LoadBalancersHandler {
val RoutePath: String = "/admin/balancers.json"

def renderHtml(clientLabel: String): String = {
val details = BalancerRegistry.get.allMetadata.find(_.label == clientLabel) match {
case None =>
"Load balancer not found in registry."
case Some(md) =>
s"""
|<table>
| <tr><td>Label</td><td>${escapeHtml(md.label)}</td></tr>
| <tr><td>Balancer Class</td><td>${escapeHtml(md.balancerClass)}</td></tr>
| <tr><td>Status</td><td>${escapeHtml(md.status)}</td></tr>
| <tr><td>Number Nodes Available</td><td>${md.numAvailable}</td></tr>
| <tr><td>Number Nodes Busy</td><td>${md.numBusy}</td></tr>
| <tr><td>Number Nodes Closed</td><td>${md.numClosed}</td></tr>
| <tr><td>Total pending requests</td><td>${md.totalPending}</td></tr>
| <tr><td>Total load</td><td>${md.totalLoad}</td></tr>
| <tr><td>Size</td><td>${md.size}</td></tr>
| <tr><td><a href="$RoutePath?label=${escapeHtml(md.label)}">More details</a></td><td></td></tr>
|</table>
""".stripMargin
}

s"""
|<div class="row">
| <div class="col-md-12">
| <a name="load_balancer"></a>
| <h3>Load Balancer</h3>
| $details
| </div>
|</div>
""".stripMargin
def getBalancers(filter: Option[String]): Seq[Metadata] = filter match {
case None => BalancerRegistry.get.allMetadata
case Some(label) => BalancerRegistry.get.allMetadata.filter(_.label == label)
}

def getBalancer(filter: Option[String]): Option[Metadata] = getBalancers(filter).headOption

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.twitter.server.view

import com.twitter.finagle.loadbalancer.Metadata
import com.twitter.server.util.HtmlUtils.escapeHtml

private[server] class BalancerHtmlView(balancer: Option[Metadata], routePath: String) extends View {

private[view] def renderNoBalancerDetails: String =
"Load balancer not found in registry."

private[view] def renderBalancerDetails(md: Metadata): String =
s"""
|<table>
| <tr><td>Label</td><td>${escapeHtml(md.label)}</td></tr>
| <tr><td>Balancer Class</td><td>${escapeHtml(md.balancerClass)}</td></tr>
| <tr><td>Status</td><td>${escapeHtml(md.status)}</td></tr>
| <tr><td>Number Nodes Available</td><td>${md.numAvailable}</td></tr>
| <tr><td>Number Nodes Busy</td><td>${md.numBusy}</td></tr>
| <tr><td>Number Nodes Closed</td><td>${md.numClosed}</td></tr>
| <tr><td>Total pending requests</td><td>${md.totalPending}</td></tr>
| <tr><td>Total load</td><td>${md.totalLoad}</td></tr>
| <tr><td>Size</td><td>${md.size}</td></tr>
| <tr><td><a href="$routePath?label=${escapeHtml(md.label)}">More details</a></td><td></td></tr>
|</table>
""".stripMargin

private[view] def renderDetails: String = balancer match {
case None => renderNoBalancerDetails
case Some(metadata) => renderBalancerDetails(metadata)
}

def render: String =
s"""
|<div class="row">
| <div class="col-md-12">
| <a name="load_balancer"></a>
| <h3>Load Balancer</h3>
| ${renderDetails}
| </div>
|</div>
""".stripMargin
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.twitter.server.view

import com.twitter.finagle.loadbalancer.Metadata
import com.twitter.server.util.JsonConverter

private[server] class BalancersJsonView(balancers: Seq[Metadata]) extends View {

private[view] def renderBalancer(balancer: Metadata): Map[String, Object] =
Map(
"label" -> balancer.label,
"info" -> Map(
"balancer_class" -> balancer.balancerClass,
"status" -> balancer.status,
"number_available" -> balancer.numAvailable,
"number_busy" -> balancer.numBusy,
"number_closed" -> balancer.numClosed,
"total_pending" -> balancer.totalPending,
"total_load" -> balancer.totalLoad,
"size" -> balancer.size,
"additional_info" -> balancer.additionalInfo
)
)

def render: String = {
val clients = balancers.map(renderBalancer)
val asMap: Map[String, Object] = Map("clients" -> clients)
JsonConverter.writeToString(asMap)
}

}

0 comments on commit a5bce43

Please sign in to comment.