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