kamon.datadog.DatadogAPIReporter.scala Maven / Gradle / Ivy
/*
* Copyright 2013-2021 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.datadog
import java.lang.StringBuilder
import java.nio.charset.StandardCharsets
import java.text.{DecimalFormat, DecimalFormatSymbols}
import java.time.Duration
import java.util.Locale
import com.typesafe.config.Config
import kamon.metric.MeasurementUnit.Dimension.{Information, Time}
import kamon.metric.{MeasurementUnit, MetricSnapshot, PeriodSnapshot}
import kamon.tag.{Tag, TagSet}
import kamon.util.{EnvironmentTags, Filter}
import kamon.{Kamon, module}
import kamon.datadog.DatadogAPIReporter.Configuration
import kamon.module.{MetricReporter, ModuleFactory}
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import scala.collection.JavaConverters._
import scala.util.{Failure, Success}
class DatadogAPIReporterFactory extends ModuleFactory {
override def create(settings: ModuleFactory.Settings): DatadogAPIReporter = {
val config = DatadogAPIReporter.readConfiguration(settings.config)
new DatadogAPIReporter(config, new HttpClient(config.httpConfig, usingAgent = false))
}
}
class DatadogAPIReporter(
@volatile private var configuration: Configuration,
@volatile private var httpClient: HttpClient
) extends MetricReporter {
import DatadogAPIReporter._
private val logger = LoggerFactory.getLogger(classOf[DatadogAPIReporter])
private val symbols = DecimalFormatSymbols.getInstance(Locale.US)
symbols.setDecimalSeparator('.') // Just in case there is some weird locale config we are not aware of.
private val valueFormat = new DecimalFormat("#0.#########", symbols)
logger.info("Started the Datadog API reporter.")
override def stop(): Unit = {
logger.info("Stopped the Datadog API reporter.")
}
override def reconfigure(config: Config): Unit = {
val newConfiguration = readConfiguration(config)
configuration = newConfiguration
httpClient = new HttpClient(configuration.httpConfig, usingAgent = false)
}
override def reportPeriodSnapshot(snapshot: PeriodSnapshot): Unit = {
httpClient.doPost("application/json; charset=utf-8", buildRequestBody(snapshot)) match {
case Failure(e) =>
logger.logAtLevel(configuration.failureLogLevel, e.getMessage)
case Success(response) =>
logger.trace(response)
}
}
private[datadog] def buildRequestBody(snapshot: PeriodSnapshot): Array[Byte] = {
val timestamp = snapshot.from.getEpochSecond.toString
val host = Kamon.environment.host
val interval = Math.round(Duration.between(snapshot.from, snapshot.to).toMillis() / 1000d)
val seriesBuilder = new StringBuilder()
@inline
def doubleToPercentileString(double: Double) = {
if (double == double.toLong) f"${double.toLong}%d"
else f"$double%s"
}
def addDistribution(metric: MetricSnapshot.Distributions): Unit = {
val unit = metric.settings.unit
metric.instruments.foreach { d =>
val dist = d.value
val average = if (dist.count > 0L) (dist.sum / dist.count) else 0L
addMetric(metric.name + ".avg", valueFormat.format(scale(average, unit)), gauge, d.tags)
addMetric(metric.name + ".count", valueFormat.format(dist.count), count, d.tags)
addMetric(metric.name + ".median", valueFormat.format(scale(dist.percentile(50d).value, unit)), gauge, d.tags)
configuration.percentiles.foreach { p =>
addMetric(
metric.name + s".${doubleToPercentileString(p)}percentile",
valueFormat.format(scale(dist.percentile(p).value, unit)),
gauge,
d.tags
)
}
addMetric(metric.name + ".max", valueFormat.format(scale(dist.max, unit)), gauge, d.tags)
addMetric(metric.name + ".min", valueFormat.format(scale(dist.min, unit)), gauge, d.tags)
}
}
def addMetric(metricName: String, value: String, metricType: String, tags: TagSet): Unit = {
val customTags = (configuration.extraTags ++ tags.iterator(_.toString).map(p => p.key -> p.value).filter(t =>
configuration.tagFilter.accept(t._1)
)).map { case (k, v) ⇒ quote"$k:$v" }
val allTagsString = customTags.mkString("[", ",", "]")
if (seriesBuilder.length() > 0) seriesBuilder.append(",")
seriesBuilder
.append(
s"""{"metric":"$metricName","interval":$interval,"points":[[$timestamp,$value]],"type":"$metricType","host":"$host","tags":$allTagsString}"""
)
}
snapshot.counters.foreach { snap =>
snap.instruments.foreach { instrument =>
addMetric(
snap.name,
valueFormat.format(scale(instrument.value, snap.settings.unit)),
count,
instrument.tags
)
}
}
snapshot.gauges.foreach { snap =>
snap.instruments.foreach { instrument =>
addMetric(
snap.name,
valueFormat.format(scale(instrument.value, snap.settings.unit)),
gauge,
instrument.tags
)
}
}
(snapshot.histograms ++ snapshot.rangeSamplers ++ snapshot.timers).foreach(addDistribution)
seriesBuilder
.insert(0, "{\"series\":[")
.append("]}")
.toString()
.getBytes(StandardCharsets.UTF_8)
}
private def scale(value: Double, unit: MeasurementUnit): Double = unit.dimension match {
case Time if unit.magnitude != configuration.timeUnit.magnitude =>
MeasurementUnit.convert(value, unit, configuration.timeUnit)
case Information if unit.magnitude != configuration.informationUnit.magnitude =>
MeasurementUnit.convert(value, unit, configuration.informationUnit)
case _ => value
}
}
private object DatadogAPIReporter {
val count = "count"
val gauge = "gauge"
case class Configuration(
httpConfig: Config,
percentiles: Set[Double],
timeUnit: MeasurementUnit,
informationUnit: MeasurementUnit,
extraTags: Seq[(String, String)],
tagFilter: Filter,
failureLogLevel: Level
)
implicit class QuoteInterp(val sc: StringContext) extends AnyVal {
def quote(args: Any*): String = "\"" + sc.s(args: _*) + "\""
}
def readConfiguration(config: Config): Configuration = {
val datadogConfig = config.getConfig("kamon.datadog")
// Remove the "host" tag since it gets added to the datadog payload separately
val extraTags = EnvironmentTags
.from(Kamon.environment, datadogConfig.getConfig("environment-tags"))
.without("host")
.all()
.map(p => p.key -> Tag.unwrapValue(p).toString)
Configuration(
datadogConfig.getConfig("api"),
percentiles = datadogConfig.getDoubleList("percentiles").asScala.toList.map(_.toDouble).toSet,
timeUnit = readTimeUnit(datadogConfig.getString("time-unit")),
informationUnit = readInformationUnit(datadogConfig.getString("information-unit")),
// Remove the "host" tag since it gets added to the datadog payload separately
extraTags = extraTags,
tagFilter = Kamon.filter("kamon.datadog.environment-tags.filter"),
failureLogLevel = readLogLevel(datadogConfig.getString("failure-log-level"))
)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy