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

26 августа 2015

Не так давно мы с вами поднимали связку из 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’ом, или описанным клиентом?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.