Простой пример записи метрик в StatsD на Scala
26 августа 2015
Не так давно мы с вами поднимали связку из Graphite, StatsD и CollectD. Сегодня же мы посмотрим, как писать какие-нибудь метрики во все это хозяйство из программы на Scala (или Java, разницы почти никакой). Также будет рассмотрена пара несложных приемов, которые вы можете найти полезными.
В build.sbt дописываем:
"com.timgroup" % "java-statsd-client" % "3.0.1"
)
Создаем трейт MetricsClient и класс MetricsClientImpl:
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 пишется готовая метрика.
Используется все это хозяйство как-то так:
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 реализованы следующим образом:
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’ом, или описанным клиентом?
Метки: Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.