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

kamon.statsd.StatsDReporter.scala Maven / Gradle / Ivy

/*
 * =========================================================================================
 * Copyright © 2013-2017 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.statsd

import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel
import java.text.{DecimalFormat, DecimalFormatSymbols}
import java.util.Locale

import com.typesafe.config.Config
import kamon.metric.MeasurementUnit.Dimension.{Information, Time}
import kamon.metric.MeasurementUnit.{information, time}
import kamon.metric.{MeasurementUnit, _}
import kamon.statsd.StatsDReporter.{Configuration, MetricDataPacketBuffer}
import kamon.util.DynamicAccess
import kamon.{Kamon, MetricReporter}
import org.slf4j.LoggerFactory


class StatsDReporter extends MetricReporter {
  private val logger = LoggerFactory.getLogger(classOf[StatsDReporter])

  private var configuration: Option[Configuration] = None

  val symbols: DecimalFormatSymbols = DecimalFormatSymbols.getInstance(Locale.US)
  symbols.setDecimalSeparator('.') // Just in case there is some weird locale config we are not aware of.

  // Absurdly high number of decimal digits, let the other end lose precision if it needs to.
  val samplingRateFormat = new DecimalFormat("#.################################################################", symbols)
  val clientChannel: DatagramChannel = DatagramChannel.open()

  override def start(): Unit = {
    logger.info("Started the Kamon StatsD reporter")
    configuration = Some(readConfiguration(Kamon.config()))
  }

  override def stop(): Unit = {}

  override def reconfigure(config: Config): Unit = {
    configuration = Some(readConfiguration(config))
    logger.info("The configuration was reloaded successfully.")
  }

  private def readConfiguration(config: Config): Configuration = {
    val statsDConfig = config.getConfig("kamon.statsd")
    val agentAddress = new InetSocketAddress(statsDConfig.getString("hostname"), statsDConfig.getInt("port"))
    val maxPacketSize = statsDConfig.getBytes("max-packet-size")
    val timeUnit = readTimeUnit(statsDConfig.getString("time-unit"))
    val informationUnit = readInformationUnit(statsDConfig.getString("information-unit"))
    val keyGenerator = loadKeyGenerator(statsDConfig.getString("metric-key-generator"), config)

    Configuration(agentAddress, maxPacketSize, timeUnit, informationUnit, keyGenerator)
  }

  private def loadKeyGenerator(keyGeneratorFQCN: String, config:Config): MetricKeyGenerator = {
    new DynamicAccess(getClass.getClassLoader).createInstanceFor[MetricKeyGenerator](keyGeneratorFQCN, (classOf[Config], config) :: Nil).get
  }

  override def reportPeriodSnapshot(snapshot: PeriodSnapshot): Unit = {
    configuration.foreach { config =>
      val keyGenerator = config.keyGenerator
      val packetBuffer = new MetricDataPacketBuffer(config.maxPacketSize, clientChannel, config.agentAddress)

      for (counter <- snapshot.metrics.counters) {
        packetBuffer.appendMeasurement(keyGenerator.generateKey(counter.name, counter.tags), encodeStatsDCounter(config, counter.value, counter.unit))
      }

      for (gauge <- snapshot.metrics.gauges) {
        packetBuffer.appendMeasurement(keyGenerator.generateKey(gauge.name, gauge.tags), encodeStatsDGauge(config, gauge.value, gauge.unit))
      }

      for (metric <- snapshot.metrics.histograms ++ snapshot.metrics.rangeSamplers;
           bucket <- metric.distribution.bucketsIterator) {

        val bucketData = encodeStatsDTimer(config, bucket.value, bucket.frequency, metric.unit)
        packetBuffer.appendMeasurement(keyGenerator.generateKey(metric.name, metric.tags), bucketData)
      }

      packetBuffer.flush()
    }
  }

  private def encodeStatsDCounter(config: Configuration, count: Long, unit: MeasurementUnit): String = s"${scale(config, count, unit)}|c"

  private def encodeStatsDGauge(config: Configuration, value:Long, unit: MeasurementUnit): String = s"${scale(config, value, unit)}|g"

  private def encodeStatsDTimer(config: Configuration, level: Long, count: Long, unit: MeasurementUnit): String = {
    val samplingRate: Double = 1D / count
    val sampled = if (samplingRate != 1D) "|@" + samplingRateFormat.format(samplingRate) else ""
    s"${scale(config, level, unit)}|ms$sampled"
  }

  private[statsd] def scale(config: Configuration, value: Long, unit: MeasurementUnit): Double = unit.dimension match {
    case Time         if unit.magnitude != config.timeUnit.magnitude => MeasurementUnit.scale(value, unit, config.timeUnit)
    case Information  if unit.magnitude != config.informationUnit.magnitude  => MeasurementUnit.scale(value, unit, config.informationUnit)
    case _ => value
  }

}

object StatsDReporter {

  private[statsd] class MetricDataPacketBuffer(maxPacketSizeInBytes: Long, channel: DatagramChannel, remote: InetSocketAddress) {
    val metricSeparator = "\n"
    val measurementSeparator = ":"

    var lastKey = ""
    var buffer = new StringBuilder()

    def appendMeasurement(key: String, measurementData: String): Unit = {
      if (key == lastKey) {
        val dataWithoutKey = measurementSeparator + measurementData
        if (fitsOnBuffer(dataWithoutKey))
          buffer.append(dataWithoutKey)
        else {
          flush()
          buffer.append(key).append(dataWithoutKey)
        }
      } else {
        lastKey = key
        val dataWithoutSeparator = key + measurementSeparator + measurementData
        if (fitsOnBuffer(metricSeparator + dataWithoutSeparator)) {
          val mSeparator = if (buffer.nonEmpty) metricSeparator else ""
          buffer.append(mSeparator).append(dataWithoutSeparator)
        } else {
          flush()
          buffer.append(dataWithoutSeparator)
        }
      }
    }

    private def fitsOnBuffer(data: String): Boolean = (buffer.length + data.length) <= maxPacketSizeInBytes

    def flush(): Unit = {
      flushToUDP(buffer.toString)
      buffer.clear()
    }

    private def flushToUDP(data: String): Unit =  {
      channel.send(ByteBuffer.wrap(data.getBytes), remote)
    }

  }

  private case class Configuration(agentAddress: InetSocketAddress,
                                   maxPacketSize: Long,
                                   timeUnit: MeasurementUnit,
                                   informationUnit: MeasurementUnit,
                                   keyGenerator: MetricKeyGenerator)

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy