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

kamon.instrumentation.system.jvm.JvmMetricsCollector.scala Maven / Gradle / Ivy

There is a newer version: 2.7.5
Show newest version
/*
 * Copyright 2013-2020 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.instrumentation.system.jvm

import java.lang.management.{BufferPoolMXBean, ManagementFactory, MemoryUsage}
import java.util.concurrent.TimeUnit

import com.sun.management.GarbageCollectionNotificationInfo
import com.sun.management.GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION
import com.typesafe.config.Config
import javax.management.openmbean.CompositeData
import javax.management.{Notification, NotificationEmitter, NotificationListener}
import kamon.Kamon
import kamon.instrumentation.system.jvm.JvmMetrics.{ClassLoadingInstruments, GarbageCollectionInstruments, MemoryUsageInstruments, ThreadsInstruments}
import kamon.instrumentation.system.jvm.JvmMetricsCollector.MemoryPool.sanitize
import kamon.instrumentation.system.jvm.JvmMetricsCollector.{Collector, MemoryPool}
import kamon.module.{Module, ModuleFactory}
import kamon.tag.TagSet

import scala.collection.JavaConverters.{collectionAsScalaIterableConverter, mapAsScalaMapConverter}
import scala.collection.concurrent.TrieMap
import scala.concurrent.ExecutionContext
import scala.util.matching.Regex

class JvmMetricsCollector(ec: ExecutionContext) extends Module {
  private val _defaultTags = TagSet.of("component", "jvm")
  private val _gcListener = registerGcListener(_defaultTags)
  private val _memoryUsageInstruments = new MemoryUsageInstruments(_defaultTags)
  private val _threadsUsageInstruments = new ThreadsInstruments()
  private val _classLoadingInstruments = new ClassLoadingInstruments(_defaultTags)
  private val _jmxCollectorTask = new JmxMetricsCollectorTask(_memoryUsageInstruments, _threadsUsageInstruments, _classLoadingInstruments)
  private val _jmxCollectorSchedule = Kamon.scheduler().scheduleAtFixedRate(_jmxCollectorTask, 1, 10, TimeUnit.SECONDS)

  override def stop(): Unit = {
    _jmxCollectorSchedule.cancel(false)
    deregisterGcListener()
  }

  override def reconfigure(newConfig: Config): Unit = {}

  private def registerGcListener(defaultTags: TagSet): NotificationListener = {
    val gcInstruments = new GarbageCollectionInstruments(defaultTags)
    val gcListener = new GcNotificationListener(gcInstruments)

    ManagementFactory.getGarbageCollectorMXBeans().asScala.foreach(gcBean => {
      if(gcBean.isInstanceOf[NotificationEmitter])
        gcBean.asInstanceOf[NotificationEmitter].addNotificationListener(gcListener, null, null)
    })

    gcListener
  }

  private def deregisterGcListener(): Unit = {
    ManagementFactory.getGarbageCollectorMXBeans().asScala.foreach(gcBean => {
      if(gcBean.isInstanceOf[NotificationEmitter]) {
        gcBean.asInstanceOf[NotificationEmitter].removeNotificationListener(_gcListener)
      }
    })
  }

  class GcNotificationListener(val gcInstruments: GarbageCollectionInstruments) extends NotificationListener {
    private val _previousUsageAfterGc = TrieMap.empty[String, MemoryUsage]

    override def handleNotification(notification: Notification, handback: Any): Unit = {
      if(notification.getType() == GARBAGE_COLLECTION_NOTIFICATION) {
        val compositeData = notification.getUserData.asInstanceOf[CompositeData]
        val info = GarbageCollectionNotificationInfo.from(compositeData)
        val collector = Collector.find(info.getGcName)

        gcInstruments.garbageCollectionTime(collector).record(info.getGcInfo.getDuration)

        val usageBeforeGc = info.getGcInfo.getMemoryUsageBeforeGc.asScala
        val usageAfterGc = info.getGcInfo.getMemoryUsageAfterGc.asScala

        usageBeforeGc.foreach {
          case (regionName, regionUsageBeforeGc) =>
            val region = MemoryPool.find(regionName)

            // We assume that if the old generation grew during this GC event then some data was promoted to it and will
            // record it as promotion to the old generation.
            if(region.usage == MemoryPool.Usage.OldGeneration) {
              val regionUsageAfterGc = usageAfterGc(regionName)
              val diff = regionUsageAfterGc.getUsed - regionUsageBeforeGc.getUsed

              if(diff > 0)
                gcInstruments.promotionToOld.record(diff)

            }

            // We will record the growth of Eden spaces in between GC events as the allocation rate.
            if(region.usage == MemoryPool.Usage.Eden) {
              _previousUsageAfterGc.get(regionName).fold({

                // We wont have the previous GC value the first time an Eden region is processed so we can assume
                // that all used space before GC was just allocation.
                gcInstruments.allocation.increment(regionUsageBeforeGc.getUsed): Unit

              })(previousUsageAfterGc => {
                val currentUsageBeforeGc = regionUsageBeforeGc
                val allocated = currentUsageBeforeGc.getUsed - previousUsageAfterGc.getUsed
                if(allocated > 0)
                  gcInstruments.allocation.increment(allocated)
              })

              _previousUsageAfterGc.put(regionName, usageAfterGc(regionName))
            }
        }
      }
    }
  }

  class JmxMetricsCollectorTask(memoryUsageInstruments: MemoryUsageInstruments, threadsInstruments: ThreadsInstruments, classLoadingInstruments: ClassLoadingInstruments)
      extends Runnable {

    private val _heapUsage = memoryUsageInstruments.regionInstruments("heap")
    private val _nonHeapUsage = memoryUsageInstruments.regionInstruments("non-heap")
    private val _classLoading = classLoadingInstruments

    override def run(): Unit = {
      val threadsMxBen = ManagementFactory.getThreadMXBean()
      threadsInstruments.total.update(threadsMxBen.getThreadCount())
      threadsInstruments.peak.update(threadsMxBen.getPeakThreadCount())
      threadsInstruments.daemon.update(threadsMxBen.getDaemonThreadCount())

      val currentHeapUsage = ManagementFactory.getMemoryMXBean.getHeapMemoryUsage
      val freeHeap = Math.max(0L, currentHeapUsage.getMax -  currentHeapUsage.getUsed)
      _heapUsage.free.record(freeHeap)
      _heapUsage.used.record(currentHeapUsage.getUsed)
      _heapUsage.max.update(currentHeapUsage.getMax)
      _heapUsage.committed.update(currentHeapUsage.getCommitted)

      val currentNonHeapUsage = ManagementFactory.getMemoryMXBean.getNonHeapMemoryUsage
      val freeNonHeap = Math.max(0L, currentNonHeapUsage.getMax -  currentNonHeapUsage.getUsed)
      _nonHeapUsage.free.record(freeNonHeap)
      _nonHeapUsage.used.record(currentNonHeapUsage.getUsed)
      _nonHeapUsage.max.update(currentNonHeapUsage.getMax)
      _nonHeapUsage.committed.update(currentNonHeapUsage.getCommitted)

      val classLoadingBean = ManagementFactory.getClassLoadingMXBean
      _classLoading.loaded.update(classLoadingBean.getTotalLoadedClassCount)
      _classLoading.unloaded.update(classLoadingBean.getUnloadedClassCount)
      _classLoading.currentlyLoaded.update(classLoadingBean.getLoadedClassCount)

      ManagementFactory.getMemoryPoolMXBeans.asScala.foreach(memoryBean => {
        val poolInstruments = memoryUsageInstruments.poolInstruments(MemoryPool.find(memoryBean.getName))
        val memoryUsage = memoryBean.getUsage
        val freeMemory = Math.max(0L, memoryUsage.getMax -  memoryUsage.getUsed)

        poolInstruments.free.record(freeMemory)
        poolInstruments.used.record(memoryUsage.getUsed)
        poolInstruments.max.update(memoryUsage.getMax)
        poolInstruments.committed.update(memoryUsage.getCommitted)
      })

      ManagementFactory.getPlatformMXBeans(classOf[BufferPoolMXBean]).asScala.toList.map { bean =>
        val bufferPoolInstruments = memoryUsageInstruments.bufferPoolInstruments(
          MemoryPool.sanitize(bean.getName))

        bufferPoolInstruments.count.update(bean.getCount)
        bufferPoolInstruments.used.update(bean.getMemoryUsed)
        bufferPoolInstruments.capacity.update(bean.getTotalCapacity)
      }
    }
  }
}

object JvmMetricsCollector {

  class Factory extends ModuleFactory {
    override def create(settings: ModuleFactory.Settings): Module =
      new JvmMetricsCollector(settings.executionContext)
  }

  case class Collector (
    name: String,
    alias: String,
    generation: Collector.Generation
  )

  object Collector {

    sealed trait Generation
    object Generation {
      case object Young extends Generation { override def toString: String = "young" }
      case object Old extends Generation { override def toString: String = "old" }
      case object Unknown extends Generation { override def toString: String = "unknown" }
    }

    def find(collectorName: String): Collector =
      _collectorMappings.get(collectorName).getOrElse (
        Collector(collectorName, sanitizeCollectorName(collectorName), Collector.Generation.Unknown)
      )

    private val _collectorMappings: Map[String, Collector] = Map (
      "Copy"                -> Collector("Copy", "copy", Generation.Young),
      "ParNew"              -> Collector("ParNew", "par-new", Generation.Young),
      "MarkSweepCompact"    -> Collector("MarkSweepCompact", "mark-sweep-compact", Generation.Old),
      "ConcurrentMarkSweep" -> Collector("ConcurrentMarkSweep", "concurrent-mark-sweep", Generation.Old),
      "PS Scavenge"         -> Collector("PS Scavenge", "ps-scavenge", Generation.Young),
      "PS MarkSweep"        -> Collector("PS MarkSweep", "ps-mark-sweep", Generation.Old),
      "G1 Young Generation" -> Collector("G1 Young Generation", "g1-young", Generation.Young),
      "G1 Old Generation"   -> Collector("G1 Old Generation", "g1-old", Generation.Old)
    )

    private def sanitizeCollectorName(name: String): String =
      name.replaceAll("""[^\w]""", "-").toLowerCase
  }

  case class MemoryPool (
    name: String,
    alias: String,
    usage: MemoryPool.Usage
  )

  object MemoryPool {

    sealed trait Usage
    object Usage {
      case object Eden extends Usage
      case object YoungGeneration extends Usage
      case object OldGeneration extends Usage
      case object CodeCache extends Usage
      case object Metaspace extends Usage
      case object Unknown extends Usage
    }

    def find(poolName: String): MemoryPool =
      _memoryRegionMappings.getOrElse(poolName,
        MemoryPool(poolName,
          sanitize(poolName), if (poolName.endsWith("Eden Space")) Usage.Eden else Usage.Unknown))

    private val _memoryRegionMappings: Map[String, MemoryPool] = Map (
      "Metaspace"               -> MemoryPool("Metaspace", "metaspace", Usage.Metaspace),
      "Code Cache"              -> MemoryPool("Code Cache", "code-cache", Usage.CodeCache),
      "Compressed Class Space"  -> MemoryPool("Compressed Class Space", "compressed-class-space", Usage.CodeCache),
      "PS Eden Space"           -> MemoryPool("PS Eden Space", "eden", Usage.Eden),
      "PS Survivor Space"       -> MemoryPool("PS Survivor Space", "survivor", Usage.YoungGeneration),
      "PS Old Gen"              -> MemoryPool("PS Old Gen", "old", Usage.OldGeneration)
    )

    private val _invalidChars: Regex = """[^a-z0-9]""".r

    def sanitize(name: String): String =
      _invalidChars.replaceAllIn(name.toLowerCase, "-")
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy