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

kamon.jmx.extension.ExportedMBeanQuery.scala Maven / Gradle / Ivy

/*
 * =========================================================================================
 * Copyright © 2013-2015 the kamon project 
 *
 * 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 kamon.jmx.extension

import java.lang.Thread
import java.lang.management.ManagementFactory
import java.util.concurrent.{ Executors, ScheduledExecutorService, TimeUnit }
import java.util.logging.Level

import scala.concurrent.ExecutionContext
import scala.collection.mutable.Buffer
import scala.collection.immutable.Set
import scala.concurrent.duration.FiniteDuration

import akka.actor.{ Actor, ActorSystem, ExtendedActorSystem, Props }
import akka.event.Logging

import javax.management._

import com.typesafe.config.{ Config, ConfigValueType }

import kamon.metric.{
  GenericEntityRecorder,
  MetricsModule,
  EntityRecorderFactory
}
import kamon.metric.SubscriptionsDispatcher.TickMetricSnapshot
import kamon.metric.instrument._
import kamon.metric.instrument.Gauge.CurrentValueCollector
import kamon.jmx.extension.MetricDefinition._

class AcceptAllPredicate extends QueryExp {

  def apply(name: ObjectName): Boolean = true

  def setMBeanServer(s: MBeanServer): Unit = {}
}

object ExportedMBeanQuery {

  val server: MBeanServer = ManagementFactory.getPlatformMBeanServer()
  val pred: QueryExp = new AcceptAllPredicate()

  /**
   * register queries to find new dynamic mbeans
   * @param system actor system used by kamon
   * @param metricsExtension kamon object for registering metrics
   * @param config configuration of this kamon extension
   */
  def register(
    system: ExtendedActorSystem, metricsExtension: MetricsModule,
    config: Config): Unit = {

    println("registering jmx exporter")
    Thread.currentThread().setName("JMX exporting thread")
    import collection.JavaConverters._
    val identifyDelayInterval: Long =
      config.getLong("identify-delay-interval-ms")
    val identifyInterval: Long = config.getLong("identify-interval-ms")
    val checkInterval: Long = config.getLong("value-check-interval-ms")

    config.getObjectList("mbeans").asScala.foreach { confObj ⇒
      val nameObj = confObj.get("name")
      val queryObj = confObj.get("jmxQuery")
      val attrListObj = confObj.get("attributes")

      require(
        nameObj.valueType() == ConfigValueType.STRING, "name must be a string")
      require(
        queryObj.valueType() == ConfigValueType.STRING,
        "jmxQuery must be a string")
      require(
        attrListObj.valueType() == ConfigValueType.LIST,
        "mbeans must be an array")

      val name: String = nameObj.unwrapped().asInstanceOf[String]
      val query: String = queryObj.unwrapped().asInstanceOf[String]
      val attrList: Seq[Map[String, Any]] =
        attrListObj.unwrapped().asInstanceOf[java.util.List[Any]].asScala.map {
          import scala.collection.JavaConversions._
          attr ⇒ attr.asInstanceOf[java.util.HashMap[String, Any]].toMap
        }

      val jmxQuery: ObjectName = new ObjectName(query)
      if (metricsExtension.shouldTrack(name, "kamon-mxbeans")) {

        val subscriber = system.actorOf(
          Props(classOf[ExportedMBeanQuery], system, jmxQuery, attrList,
            identifyDelayInterval, identifyInterval, checkInterval),
          name)
      }
    }
  }

  /**
   * @param system actor system used by kamon
   * @param jmxQuery query to lookup dynamic mbeans from the JVM
   * @param attributeConfigs configuration for each attribute
   * @param identifyDelayInterval how long to wait to look for mbeans the
   * first time
   * @param identifyInterval how often to look for new mbeans
   * @param checkInterval how often to check for new values for metrics
   */
  def apply(
    system: ExtendedActorSystem,
    jmxQuery: ObjectName, attributeConfigs: Seq[Map[String, Any]],
    identifyDelayInterval: Long, identifyInterval: Long,
    checkInterval: Long): ExportedMBeanQuery =
    new ExportedMBeanQuery(
      system, jmxQuery, attributeConfigs,
      identifyDelayInterval, identifyInterval, checkInterval)
}

/**
 * Uses a jmx query to find dynamic mbeans and registers them to be listened
 * to by kamon.  This is the object that actually moves metrics from JMX
 * into Kamon.
 *
 * @param system actor system used by kamon
 * @param jmxQuery query to use to look for new dynamic mbeans
 * @param attributeConfig configuration of each metric for this kind of
 * mbean
 * @param identifyDelayInterval how long to wait to look for mbeans the
 * first time
 * @param identifyInterval how often to look for new mbeans
 * @param checkInterval how often to check for new values for metrics
 */
class ExportedMBeanQuery(
    val system: ExtendedActorSystem,
    val jmxQuery: ObjectName, val attributeConfigs: Seq[Map[String, Any]],
    val identifyDelayInterval: Long, val identifyInterval: Long,
    val checkInterval: Long) extends Actor {

  import context.dispatcher // ExecutionContext for the futures and scheduler
  val log = Logging(system, getClass)

  require(
    identifyInterval >= checkInterval,
    "not checking JMX values often enough: " +
      identifyInterval + " < " + checkInterval)
  import ExportedMBeanQuery.{ server, pred }
  import ExportedMBean.{ apply }

  val monitoredBeanNames: collection.mutable.Set[ObjectName] =
    collection.mutable.Set[ObjectName]()
  var lastCheck: Long = 0

  val attributeNames: Array[String] = attributeConfigs.map { m ⇒
    m("name").asInstanceOf[String]
  }.toArray
  val configMap: Map[String, Map[String, Any]] = attributeConfigs.map { conf ⇒
    (conf("name").asInstanceOf[String], conf)
  }.toMap

  // queries the current set of dynamic mbeans and returns their identifing
  // names
  def getMBeanNames(): Set[ObjectName] = {
    import collection.JavaConverters._
    server.queryNames(jmxQuery, pred).asScala.toSet
  }

  /**
   * looks for new dynamic mbeans and registers an object to watch those
   * new mbeans for changes in their metric values and send them into kamon
   * metrics
   */
  def rebuildRecorders(): Unit = {

    import collection.JavaConverters._
    val names: Set[ObjectName] = getMBeanNames()
    log.debug("found JMX ObjectNames " + names)
    names.filterNot { name ⇒
      monitoredBeanNames.exists(_.equals(name))
    }.foreach { name ⇒
      val attrList: AttributeList =
        server.getAttributes(name, attributeNames)
      val attrs: Seq[Attribute] = attrList.asList().asScala
      val values: Map[String, Attribute] =
        attrs.map { attr ⇒ (attr.getName(), attr) }.toMap

      val definitions: Seq[MetricDefinition] = values.map {
        case (name, attr) ⇒
          val config: Map[String, Any] = configMap(name)
          if ("gauge".equalsIgnoreCase(
            config("type").asInstanceOf[String])) {
            new MetricDefinition(
              config, None, Some(new CurrentValueCollector {
                def currentValue: Long = ExportedMBean.toLong(attr.getValue())
              }))
          } else {
            new MetricDefinition(config, None, None)
          }
      }.toSeq

      val metricsExtension = kamon.Kamon.metrics
      metricsExtension.entity(
        EntityRecorderFactory(
          "kamon-mxbeans",
          apply(
            system, _, definitions, name, attributeNames, checkInterval)),
        name.getKeyProperty("name"))

      monitoredBeanNames += name
    }
  }

  def receive: PartialFunction[Any, Unit] = {
    case tick: TickMetricSnapshot ⇒ rebuildRecorders()
  }

  system.scheduler.schedule(
    FiniteDuration(identifyDelayInterval, TimeUnit.MILLISECONDS),
    FiniteDuration(identifyInterval, TimeUnit.MILLISECONDS),
    new Runnable() {
      def run(): Unit = rebuildRecorders()
    })
}

/**
 * An object that manages a specific dynamic mbean for kamon.
 * @param system actor system used by kamon
 * @param instrumentFactory kamon facade for making metrics
 * @param definitions representations of each kamon metric in this mbean
 * @param objName name of the JMX object to monitor
 * @param attrNames jmx attributes to query for metric values
 * @param checkInterval how often to check for new metric values from this
 *  mbean
 */
class ExportedMBean(
  val system: ExtendedActorSystem,
  val instrumentFactory: InstrumentFactory,
  val definitions: Seq[MetricDefinition], val objName: ObjectName,
  val attrNames: Array[String], val checkInterval: Long)(
    implicit ec: ExecutionContext)
    extends GenericEntityRecorder(instrumentFactory) {

  import ExportedMBean._

  val log = Logging(system, getClass)
  import kamon.jmx.extension.ExportedMBeanQuery.server

  // metrics we created for this mbean
  val counters: Map[String, Counter] =
    definitions.filter(_.metricType == MetricTypeEnum.COUNTER).map(mdef ⇒
      (mdef.name, makeCounter(mdef))).toMap
  val histograms: Map[String, Histogram] =
    definitions.filter(_.metricType == MetricTypeEnum.HISTOGRAM).map(mdef ⇒
      (mdef.name, makeHistogram(mdef))).toMap
  val minMaxCounters: Map[String, MinMaxCounter] =
    definitions.filter(_.metricType == MetricTypeEnum.MIN_MAX_COUNTER).map(
      mdef ⇒ (mdef.name, makeMinMaxCounter(mdef))).toMap
  val gauges: Map[String, Gauge] =
    definitions.filter(_.metricType == MetricTypeEnum.GAUGE).map(
      mdef ⇒ (mdef.name, makeGauge(mdef))).toMap

  def gatherMetrics(): Unit = {
    import collection.JavaConverters._

    try {
      val attrList: AttributeList = server.getAttributes(objName, attrNames)
      attrList.asList().asScala.foreach { attr ⇒
        val attrName: String = attr.getName()
        if (counters.contains(attrName)) {
          counters(attrName).increment(toLong(attr.getValue()))
        } else if (histograms.contains(attrName)) {
          histogram(attrName).record(toLong(attr.getValue()))
        } else if (minMaxCounters.contains(attrName)) {
          val value: Long = toLong(attr.getValue())
          minMaxCounters(attrName).increment(value)
        }
      }
    } catch {
      case e: javax.management.JMException ⇒ log.error(e, e.getMessage())
      case e1: Throwable                   ⇒ log.error(e1, e1.getMessage())
    }
  }

  protected def makeCounter(mdef: MetricDefinition): Counter = {
    require(mdef.metricType == MetricTypeEnum.COUNTER)
    require(mdef.name != null, "a metric should have a name")
    require(!mdef.range.isDefined, "a counter can't define a range")
    require(
      !mdef.refreshInterval.isDefined,
      "a counter can't define a refresh interval")
    require(
      !mdef.valueCollector.isDefined,
      "a counter can't define a value collector")
    counter(mdef.name, mdef.unitOfMeasure)
  }

  protected def makeHistogram(mdef: MetricDefinition): Histogram = {
    require(mdef.metricType == MetricTypeEnum.HISTOGRAM)
    require(mdef.name != null, "a histogram should have a name")
    require(
      !mdef.refreshInterval.isDefined,
      "a histogram can't have a refresh interval")
    require(
      !mdef.valueCollector.isDefined,
      "a histogram can't have a value collector")
    if (!mdef.range.isDefined) {
      histogram(mdef.name, mdef.unitOfMeasure)
    } else {
      histogram(mdef.name, mdef.range.get, mdef.unitOfMeasure)
    }
  }

  protected def makeMinMaxCounter(mdef: MetricDefinition): MinMaxCounter = {
    require(mdef.metricType == MetricTypeEnum.MIN_MAX_COUNTER)
    require(mdef.name != null, "a min-max counter must have a name")
    require(
      !mdef.valueCollector.isDefined,
      "a min-max counter can't define a value collector")
    if (!mdef.range.isDefined && !mdef.refreshInterval.isDefined) {
      minMaxCounter(mdef.name, mdef.unitOfMeasure)
    } else if (!mdef.refreshInterval.isDefined) {
      minMaxCounter(mdef.name, mdef.range.get, mdef.unitOfMeasure)
    } else if (!mdef.range.isDefined) {
      minMaxCounter(mdef.name, mdef.refreshInterval.get, mdef.unitOfMeasure)
    } else {
      minMaxCounter(
        mdef.name, mdef.range.get, mdef.refreshInterval.get, mdef.unitOfMeasure)
    }
  }

  protected def makeGauge(mdef: MetricDefinition): Gauge = {
    require(mdef.metricType == MetricTypeEnum.GAUGE)
    require(mdef.name != null, "a gauge must have a name")
    require(
      mdef.valueCollector.isDefined, "a gauge must have a value collector")
    if (mdef.range.isDefined && !mdef.refreshInterval.isDefined) {
      gauge(mdef.name, mdef.unitOfMeasure, mdef.valueCollector.get)
    } else if (!mdef.range.isDefined) {
      gauge(
        mdef.name, mdef.refreshInterval.get, mdef.unitOfMeasure,
        mdef.valueCollector.get)
    } else if (!mdef.refreshInterval.isDefined) {
      gauge(
        mdef.name, mdef.range.get, mdef.unitOfMeasure, mdef.valueCollector.get)
    } else {
      gauge(
        mdef.name, mdef.range.get, mdef.refreshInterval.get, mdef.unitOfMeasure,
        mdef.valueCollector.get)
    }
  }

  system.scheduler.schedule(
    FiniteDuration(checkInterval, TimeUnit.MILLISECONDS),
    FiniteDuration(checkInterval, TimeUnit.MILLISECONDS),
    new Runnable() {
      def run(): Unit = gatherMetrics()
    })
  log.debug("scheduled reading of JMX metrics from " + objName)
}

object ExportedMBean {

  protected[extension] def toLong(v: Any): Long = v match {
    case x: Long   ⇒ x
    case n: Number ⇒ n.asInstanceOf[Number].longValue
    case _         ⇒ throw new IllegalArgumentException(s"$v is not a number.")
  }

  /**
   * @param system actor system used by kamon
   * @param instrumentFactory kamon facade for making metrics
   * @param definitions the configuration of metrics to push into kamon
   * @param objName the name of the jmx object we are monitoring
   * @param attrNames the names of the attributes we are monitoring
   * @param checkInterval how often to check for new mbeans
   */
  def apply(
    system: ExtendedActorSystem,
    instrumentFactory: InstrumentFactory,
    definitions: Seq[MetricDefinition],
    objName: ObjectName, attrNames: Array[String],
    checkInterval: Long)(implicit ec: ExecutionContext): ExportedMBean =
    new ExportedMBean(
      system, instrumentFactory, definitions, objName, attrNames,
      checkInterval)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy