User guide
Creating metric factory
To start creating your metrics, first you need to create a MetricFactory[F[_]]
. Currently, the only implementation is
in statsd
package. MetricFactory is purely functional, so it requires you to provide type constructor which
implements cats.effect.Sync
. For the simplicity, we will use cats.effect.IO
in these examples.
To create an instance, we need to provide it with configuration which contains a few basic fields, like address of the StatsD server, prefix etc. For more information see scaladoc of the config class.
The instance is wrapped in Resource
because of the underlying StatsD
client.
import java.net.InetSocketAddress
import cats.effect.*
import com.avast.datadog4s.api.*
import com.avast.datadog4s.api.metric.*
import com.avast.datadog4s.*
val statsDServer = InetSocketAddress.createUnresolved("localhost", 8125)
val config = StatsDMetricFactoryConfig(Some("my-app-name"), statsDServer)
val factoryResource: Resource[IO, MetricFactory[IO]] = StatsDMetricFactory.make(config)
Creating metrics
Once you have a metrics factory, creating metrics is straight-forward. Note that all metric operations return side-effecting actions.
factoryResource.use { factory =>
val count: Count[IO] = factory.count("hits")
val histogram: Histogram[IO, Long] = factory.histogram.long("my-histogram")
for {
_ <- count.inc() // increase count by one
_ <- histogram.record(1337, Tag.of("username", "xyz")) // record a value to histogram with Tag
} yield {
()
}
}
Timers
In addition to basic datadog metrics, we provide a Timer[F]
abstraction which has proved to be very useful is
practice. Timers provide you with .time[A](fa: F[A]): F[A]
method, which will measure how long it took to run
provided fa
. In addition, it tags the metric with success:true
or success:false
and exception:<<throwable class name>>
in case the fa
failed.
In addition to .time[A]
method it also allows for recording of raw values that represent elapsed time or even raw time
data. You can see example of such calls below.
Optionally, when creating a Timer
, you can also set the time units which will be used for reporting. By default, all
timers create with microsecond
granularity. You can provide your own time unit if you need more or less precision.
Histogram vs Distribution
There are two versions of timers, one backed by Histogram
and one backed by Distribution
. You can read scaladoc for
more details and links to datadog documentation.
Long story short, histogram
backed timers are aggregated per datadog agent, while the distributions
are computed on
datadog server. The implications are that distribution
based timers, and it’s buckets (50th, 75th, 95th percentile
etc) are more correct and in general it’s the implementation that we’d suggest to use.
Example
import java.util.concurrent.TimeUnit
factoryResource.use { factory =>
val timer = factory.timer.distribution("request-latency")
timer.time(IO.delay(println("success"))) // tagged with success:true
timer.time(IO.raiseError(new NullPointerException("error"))) // tagged with success:false and exception:NullPointerException
val nanoTimer = factory.timer.distribution("nano-timer", timeUnit = TimeUnit.NANOSECONDS)
nanoTimer.time(IO.delay(println("success"))) // metric will be recorded with 'nanoseconds' precision
// timer.record works for all types that implement `ElapsedTime` typeclass, out of the box we provide implementation
// for java.time.Duration and scala.concurrent.duration.FiniteDuration
import java.time.Duration
timer.record(Duration.ofNanos(1000))
import scala.concurrent.duration.FiniteDuration
timer.record(FiniteDuration(1000, TimeUnit.MILLISECONDS))
timer.recordTime(1000L, TimeUnit.MILLISECONDS)
}
Tagging
There are two ways to create a Tag
instances. One way is using of
method of Tag
object, like so:
import com.avast.datadog4s.api.Tag
Tag.of("endpoint", "admin/login")
// res2: String = "endpoint:admin/login"
This is simple and straight-forward, but in some cases it leaves your code with Tag
keys scattered around and forces
you to repeat it - making it prone to misspells etc. The better way is to use Tagger
.
Tagger
Tagger[T]
is basically a factory interface for creating tags based on provided value of type T
- as long as
implicit TagValue[T]
exists in scope. This instance is used for converting T
into String
. By using Tagger
, you
get a single value that you can use in multiple places in your code to create Tag
s without repeating yourself.
Example:
import com.avast.datadog4s.api.tag.{TagValue, Tagger}
val pathTagger: Tagger[String] = Tagger.make[String]("path")
// pathTagger: Tagger[String] = com.avast.datadog4s.api.tag.Tagger$$anon$1@40da105b
assert(Tag.of("path", "admin/login") == pathTagger.tag("admin/login"))
// tagger also supports taging using custom types using TagValue typeclass
case class StatusCode(value: Int)
implicit val statusCodeTagValue: TagValue[StatusCode] = TagValue[Int].contramap[StatusCode](sc => sc.value)
// statusCodeTagValue: TagValue[StatusCode] = com.avast.datadog4s.api.tag.TagValue$$Lambda$8429/0x000000010247e040@111de2ee
val statusCodeTagger: Tagger[StatusCode] = Tagger.make[StatusCode]("statusCode")
// statusCodeTagger: Tagger[StatusCode] = com.avast.datadog4s.api.tag.Tagger$$anon$1@239191fb
assert(Tag.of("statusCode", "200") == statusCodeTagger.tag(StatusCode(200)))
Extensions
Extensions are packages that monitor some functionality for you - without you having to do much.
Http4s
Http4s package (datadog4s-http4s
) provides implementation of MetricsOps that is used
by http4s to report both client and server metrics.
import com.avast.datadog4s.extension.http4s.*
factoryResource.use { metricFactory =>
// create metrics factory and use it as you please
DatadogMetricsOps.builder[IO](metricFactory).build().flatMap { metricOps =>
// setup http4s Metrics middleware here
val _ = metricOps
IO.unit
}
}
// res5: IO[Unit] = Uncancelable(
// body = cats.effect.IO$$$Lambda$8425/0x000000010247b840@5c5ed3d4,
// event = cats.effect.tracing.TracingEvent$StackTrace
// )
Jvm monitoring
JVM monitoring package (datadog4s-jvm
) collects a bunch of JVM metrics that we found useful over last 5 or so years
running JVM apps in Avast. Those metrics can be found in JvmReporter and are hopefully
self-explanatory. We tried to match reported metrics to datadog JVM runtime metrics
Usage can not be simpler (unless you want to configure things like collection-frequency etc.). Simply add following to your initialization code. Resource is returned, because a fiber is started in the background and has to be terminated eventually.
import com.avast.datadog4s.extension.jvm.*
val jvmMonitoring: Resource[IO, Unit] = factoryResource.flatMap {
factory => JvmMonitoring.default[IO](factory)
}
jvmMonitoring.use { _ =>
// your application is in here
IO.unit
}
Note on Java compatibility:
Starting with Java 16, applications that use our jvm monitoring need to
add --add-opens=java.management/sun.management=ALL-UNNAMED
as JVM parameter when starting the application. This is
because JVM monitoring uses internal java APIs to obtain certain metrics.