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

xitrum.metrics.MetricsAction.scala Maven / Gradle / Ivy

The newest version!
package xitrum.metrics

import akka.actor.{Actor, ActorRef, Terminated}
import akka.cluster.metrics.{ClusterMetricsChanged, ClusterMetricsExtension, NodeMetrics}
import akka.cluster.metrics.StandardMetrics.{Cpu, HeapMemory}

import io.netty.handler.codec.http.HttpResponseStatus

import xitrum.{FutureAction, Config, SockJsAction, SockJsText}
import xitrum.annotation.{GET, Last, SOCKJS}
import xitrum.util.SeriDeseri
import xitrum.view.DocType

/** JMX metrics collector for single node application. */
class MetricsCollector(localPublisher: ActorRef) extends Actor {
  def receive: Receive = {
    case Collect => localPublisher ! MetricsManager.jmx.sample()
    case _ =>
  }
}

/** JMX metrics collector for clustered node application. */
class ClusterMetricsCollector(localPublisher: ActorRef) extends Actor {
  private val extension = ClusterMetricsExtension(context.system)

  override def preStart(): Unit = {
    extension.subscribe(self)
  }

  override def postStop(): Unit = {
    extension.unsubscribe(self)
  }

  def receive: Receive = {
    case m: ClusterMetricsChanged =>
      localPublisher ! m

    case UnSubscribe =>
      extension.unsubscribe(self)

    case _ =>
  }
}

/**
 * Example: XitrumMetricsChannel
 */
class MetricsPublisher extends Actor {
  private var clients                                = Seq[ActorRef]()
  private var lastPublished                          = System.currentTimeMillis()
  private var cachedNodeMetrics:    NodeMetrics      = _
  private var cachedClusterMetrics: Set[NodeMetrics] = _
  private var cachedRegistryAsJson: String           = _

  def receive: Receive = {
    case ClusterMetricsChanged(clusterMetrics) =>
      cachedClusterMetrics = clusterMetrics

    case nodeMetrics: NodeMetrics =>
      cachedNodeMetrics = nodeMetrics

    case Publish(registryAsJson) =>
      cachedRegistryAsJson = registryAsJson
      clients.foreach { client =>
        client ! Publish(registryAsJson)
        if (cachedNodeMetrics != null) client ! cachedNodeMetrics
        // Ignore if clusterd metricsManager sent duplicated requests
        if (System.currentTimeMillis - lastPublished > Config.xitrum.metrics.get.collectActorInterval.toMillis) {
          if (cachedClusterMetrics != null) client ! cachedClusterMetrics.toList
          lastPublished = System.currentTimeMillis
        }
      }

    case Pull =>
      sender() ! Publish(MetricsManager.registryAsJson)
      if (cachedNodeMetrics != null)    sender() ! cachedNodeMetrics
      if (cachedClusterMetrics != null) sender() ! cachedClusterMetrics.toList

    case Subscribe =>
      clients = clients :+ sender()
      context.watch(sender())

    case UnSubscribe =>
      clients =  clients.filterNot(_ == sender())
      context.unwatch(sender())

    case Terminated(client) =>
      clients = clients.filterNot(_ == client)

    case _ =>
  }
}

/** Javascript fragment for establish metrics JSON sockJS channel. */
trait MetricsViewer extends FutureAction {
  def jsAddMetricsNameSpace(namespace: String = null): Unit = {
    jsAddToView(s"""
(function (namespace) {
  var ns  = namespace || window;
  var url = '${sockJsUrl[XitrumMetricsChannel]}';
  var pullTimer;
  var initMetricsChannel = function(onMessageFunc) {
    var socket;
    socket = new SockJS(url);
    socket.onopen = function(event) {
      socket.send('${MetricsManager.metrics.apiKey}');
      pullTimer = setInterval(function() { socket.send('pull') }, 2000);
    };
    socket.onclose = function(e) {
      clearInterval(pullTimer);
      // Reconnect
      setTimeout(function() { initMetricsChannel(onMessageFunc) }, 2000);
    };
    socket.onmessage = function(e) { onMessageFunc(e.data); }
  };
  ns.initMetricsChannel = initMetricsChannel;
})($namespace);
"""
    )
  }
}

/**
 * Default metrics viewer page.
 * This page could be overwritten in user application with any style.
 */
@Last
@GET("xitrum/metrics/viewer")
class XitrumMetricsViewer extends FutureAction with MetricsViewer {
  beforeFilter {
    val apiKey  = param("api_key")
    if (apiKey != MetricsManager.metrics.apiKey) {
      response.setStatus(HttpResponseStatus.UNAUTHORIZED)
      respondHtml(

Unauthorized

) } } lazy val html: String = DocType.html5( {xitrumCss} {jsDefaults} Xitrum Default Metrics Viewer

Xitrum Default Metrics Viewer

NodeMetrics Status

NodeMetrics(HeapMemory)
TimeNodeCommitted(MB)Used(MB)Max(MB)

NodeMetrics(CPU)
TimeNodeProcessorsLoad Average

Application Metrics Status

Histograms
NodeKeyCountMin(ms)Max(ms)Mean(ms)
{jsForView} ) lazy val focusHtml: String = DocType.html5( {xitrumCss} {jsDefaults} Xitrum Default Metrics Viewer

{jsForView} ) def execute(): Unit = { jsAddMetricsNameSpace("window") paramo("focusAction") match { case Some(key) => jsAddToView("initMetricsChannel(channelOnMessageWithKey('"+ key +"'));") respondHtml(focusHtml) case None => jsAddToView("initMetricsChannel(channelOnMessage);") respondHtml(html) } } } /** SockJS channel for metrics JSON. */ @SOCKJS("xitrum/metrics/channel") class XitrumMetricsChannel extends SockJsAction with PublisherLookUp { def execute(): Unit = { checkAPIKey() } private def checkAPIKey(): Unit = { context.become { case SockJsText(text) if text == MetricsManager.metrics.apiKey => lookUpPublisher() case SockJsText(key) => respondSockJsText("Wrong apikey") respondSockJsClose() case ignore => log.warn("Unexpected message: " + ignore) } } override def doWithPublisher(publisher: ActorRef): Unit = { publisher ! Subscribe context.watch(publisher) context.become { case msg: Seq[_] => msg.asInstanceOf[Seq[NodeMetrics]].foreach { nodeMetrics => sendHeapMemory(nodeMetrics) sendCpu(nodeMetrics) } case nodeMetrics: NodeMetrics => sendHeapMemory(nodeMetrics) sendCpu(nodeMetrics) case Publish(registryAsJson) => respondSockJsText(registryAsJson) case SockJsText(text) => if (text == "pull" ) publisher ! Pull case Terminated(`publisher`) => lookUpPublisher() case _ => } } private def sendHeapMemory(nodeMetrics: NodeMetrics): Unit = { nodeMetrics match { case HeapMemory(address, timestamp, used, committed, max) => respondSockJsText(SeriDeseri.toJson(Map( "TYPE" -> "heapMemory", "SYSTEM" -> address.system, "HOST" -> address.host, "PORT" -> address.port, "HASH" -> address.hashCode, "TIMESTAMP" -> timestamp, "USED" -> used, "COMMITTED" -> committed, "MAX" -> max ))) case _ => } } private def sendCpu(nodeMetrics: NodeMetrics):Unit = { nodeMetrics match { case Cpu(address, timestamp, Some(systemLoadAverage), cpuCombined, cpuStolen, processors) => respondSockJsText(SeriDeseri.toJson(Map( "TYPE" -> "cpu", "SYSTEM" -> address.system, "HOST" -> address.host, "PORT" -> address.port, "HASH" -> address.hashCode, "TIMESTAMP" -> timestamp, "SYSTEMLOADAVERAGE" -> systemLoadAverage, "CPUCOMBINED" -> cpuCombined, "PROCESSORS" -> processors ))) case _ => } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy