← На главную

Простой пример записи метрик в StatsD на Scala

Не так давно мы с вами поднимали связку из Graphite, StatsD и CollectD. Сегодня же мы посмотрим, как писать какие-нибудь метрики во все это хозяйство из программы на Scala (или Java, разницы почти никакой). Также будет рассмотрена пара несложных приемов, которые вы можете найти полезными.

В build.sbt дописываем:

libraryDependencies ++= Seq( "com.timgroup" % "java-statsd-client" % "3.0.1" )

Создаем трейт MetricsClient и класс MetricsClientImpl:

package me.eax.examples.statsd.client import com.timgroup.statsd.NonBlockingStatsDClient trait MetricsClient { def recordTime(name: String, timeMs: Long): Unit def recordValue(name: String, value: Long): Unit def incrementCounter(name: String, delta: Long = 1L): Unit } class MetricsClientImpl extends MetricsClient { // TODO: read from config! private val prefix = "me.eax" private val host = "10.110.0.10" private val port = 8125 private val client = new NonBlockingStatsDClient(prefix, host, port) def recordTime(name: String, timeMs: Long): Unit = { client.recordExecutionTime(name, timeMs) } def recordValue(name: String, value: Long): Unit = { client.recordGaugeValue(name, value) } def incrementCounter(name: String, delta: Long = 1L): Unit = { client.count(name, delta) } }

Вообще, это очень полезная практика – разбивать интерфейсы и конкретные реализации. Например, при тестировании приложения становится очень просто подсунуть вместо настоящей реализации какой-то мок. А если вы решите добавить в конфиг параметр, писать ли метрики в StatsD, или во что-то другое, понадобится написать совсем немного кода. Главное здесь не перегибать палку, так как разбивать на интерфейсы и реализацию вообще все классы, пожалуй, имеет мало смысла.

Используемый здесь NonBlockingStatsDClient гарантированно не блокируется и не бросает никаких исключений. По крайней мере, в JavaDoc прямым текстом так и написано. StatsD поддерживает несколько типов метрик, которые в силу очевидных причин агрегируются и пишутся в Graphite немного по-разному. Time замеряет какое-то время, например, время выполнения запроса к БД. Counter считает какие-то события, например, запросы пользователя или попадания в кэш. Gauge – это как бы конкретные значения, например, процент занятых ниток в тредпуле или длина очереди у актора. Эти значения либо проверяются и пишутся раз в какой-то промежуток времени, либо как-то хитро агрегируются на стороне самого приложения, а в StatsD пишется готовая метрика.

Используется все это хозяйство как-то так:

package me.eax.examples.statsd.client import scala.util._ import scala.concurrent._ import me.eax.examples.statsd.client.utils._ import scala.concurrent.ExecutionContext.Implicits.global object StatsDClientExample extends App { val client = new MetricsClientImpl for(i <- 1 to 500) { val inc = (1 + Random.nextInt(5)).toLong val time = (1 + Random.nextInt(100)).toLong val value = (1 + Random.nextInt(1000)).toLong client.incrementCounter("test.counter", inc) client.recordTime("test.time", time) client.recordValue("test.value", value) recordTimeF("thread.sleep.future") { Future { Thread.sleep(100) } } recordTime("thread.sleep") { Thread.sleep(100) } } }

Функции recordTime и recordTimeF реализованы следующим образом:

package me.eax.examples.statsd.client import scala.compat._ import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global package object utils { private val client = new MetricsClientImpl def recordTime[T](metric: String)(f: => T): T = { val startTimeMs = Platform.currentTime val result = f val endTimeMs = Platform.currentTime client.synchronized { client.recordTime(metric, endTimeMs - startTimeMs) } result } def recordTimeF[T](metric: String)(f: => Future[T]): Future[T] = { val startTimeMs = Platform.currentTime val fResult = f // TODO: check if future is completed successfully fResult.onComplete { case _ => val endTimeMs = Platform.currentTime client.synchronized { client.recordTime(metric, endTimeMs - startTimeMs) } } fResult } }

Обратите внимание на то, какой классный код мы здесь получили при помощи каррирования и call by name. Это полезный и широко распространенный прием в мире Scala. Следует однако отметить, что если вы по ошибке вызовите recordTime вместо recordTimeF, код успешно тайпчекнется и скомпилируется. Этот, а также кое-какие другие факты, в последнее время наводят меня на мысли, что если в проекте используются футуры, то нужно вообще везде и всегда использовать футуры, даже в чистом коде. Мало того, что описанная ошибка будет невозможна, так еще и не придется писать две версии одной функции, как сделано в примере выше. Плюс не придется переписывать половину проекта, когда где-то внутри функции, которая вроде была чистой, потребовалось вызвать код, возвращающий футуру. Также есть другие соображения по этой теме, но, пожалуй, я приберегу их до следующего раза.

Напоследок хотелось бы рассмотреть преимущества использования обычного клиента к StatsD перед прикручиванием к проекту Kamon, который тоже умеет писать в StatsD:

  • Никакой зависимости от Akka – не тащить же ее ради метрик в небольшие микросервисы, где используется только Finagle;
  • Не нужно пробрасывать implicit’ом ActorSystem по всему проекту;
  • Никаких AspectJ с этими его «weaver is missing» при прогоне тестов;
  • В коде нет никакого неявного поведения, сделанного через аспекты;
  • Чуть проще собрать fat jar или deb-пакет – берешь и делаешь;

Ну и пара ссылки по теме:

А как вы считаете, лучше собирать метрики Kamon’ом, или описанным клиентом?