User guide

Quick start

Installation

To start monitoring your code, first you need to add this library as a dependency to your project. This project is composed of multiple packages to make it easy for you to pick and choose what you require.

You need to add datadog4s-api which contains classes defining our API. You also need to add its implementation. Currently, we only support metric delivery using StatsD in package datadog4s which already contains api. We are going to assume you are using sbt:

libraryDependencies += "com.avast.cloud" %% "datadog4s-api" % "0.12.0"
libraryDependencies += "com.avast.cloud" %% "datadog4s-statsd" % "0.12.0"

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.

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




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
}

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: Tag = "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 Tags 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@573b5b83
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$$anonfun$contramap$2@43af64b4

val statusCodeTagger: Tagger[StatusCode] = Tagger.make[StatusCode]("statusCode")
// statusCodeTagger: Tagger[StatusCode] = com.avast.datadog4s.api.tag.Tagger$$anon$1@1dab27d1

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
    }
}

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._
import scala.concurrent.ExecutionContext

implicit val ec = ExecutionContext.global // please don't use global EC in production
implicit val contextShift = IO.contextShift(ec)
implicit val timer = IO.timer(ec)

val jvmMonitoring: Resource[IO, Unit] = factoryResource.flatMap {
  factory => JvmMonitoring.default[IO](factory)
}

jvmMonitoring.use { _ => 
    // your application is in here
    IO.unit
}