この記事は、Scalaアドベントカレンダー21日目の記事になります。
Kamonとは
Kamon is a monitoring toolkit for applications running on the JVM. It gives you Metrics, Tracing and Context Propagation APIs without locking you to any specific vendor. All Kamon APIs are completely decoupled from the services that can receive the data, be it StatsD, Prometheus, Kamino, Datadog, Zipkin, Jaeger or any other supported reporter, with Kamon you instrument your application once and report anywhere you want.
KamonはJVMで動くアプリケーションのモニタリングのためのツールキットで、多様なサービスと連携することが出来るとあります。
SystemMetrics的なものを始め、Akkaのメールボックスの溜まり具合や、JDBCのリクエストのレイテンシといったメトリクスを取得するプラグインなどがあり、出力先もDatadogやPrometheusなど様々あります。
現行の最新版は0.6系なのですが、別ブランチで1.0系を開発中しており、RC7まで進んでいたのでこれを機に少しだけ試してみます。
ソースコードはこちら。
https://github.com/uryyyyyyy/kamonSample/tree/advent21
環境はこちら
- Kamon: 1.0.0-RC7
- Scala: 2.12
やること
Kamonやプラグインの仕組みを知るために、データの収集(Collector)とデータの報告(Reporter)をそれぞれ作ってみます。
どちらも様々なプラグインが存在しますが、基本的な流れを理解するとコードも読みやすいと思います。
データを収集してみる
試しに、固定値を定期的に取得するCollectorを作ってみます。
まず、データを作るところはこんな感じ。 update
メソッドが呼ばれるたびにメトリクスをKamonに送ります。
package com.github.uryyyyyyy.kamon.simple.collector
import kamon.Kamon
import org.slf4j.LoggerFactory
class MyMetrics {
private val logger = LoggerFactory.getLogger(classOf[MyMetrics])
// メトリクス名・メトリクスのタイプを決める。
val hist1 = Kamon.histogram("my-reporter.my-metrics.hist1")
val counter1 = Kamon.counter("my-reporter.my-metrics.counter1")
val sampler1 = Kamon.rangeSampler("my-reporter.my-metrics.sampler1")
def update() = {
logger.info("MyMetrics update")
// メトリクスのタイプに応じたAPIでデータが計測される。
hist1.record(10)
counter1.increment(1)
sampler1.increment(2)
}
}
次に、その計測を定期的に行うSchedulerを作成します。
package com.github.uryyyyyyy.kamon.simple.collector
import java.time.Duration
import java.util.concurrent.{ScheduledFuture, TimeUnit}
import kamon.Kamon
import org.slf4j.LoggerFactory
object MyCollector {
private val logger = LoggerFactory.getLogger("com.github.uryyyyyyy.kamon.simple.collector.MyCollector")
private var scheduledCollection: ScheduledFuture[_] = null
// 計測開始
def startCollecting() = {
val myMetrics = new MyMetrics()
val updaterSchedule = new Runnable {
override def run(): Unit = myMetrics.update()
}
// 1s毎に、メトリクスを収集するためにmyMetrics.update()を呼ぶ
scheduledCollection = Kamon.scheduler().scheduleAtFixedRate(
updaterSchedule,
Duration.ofSeconds(1).toMillis,
Duration.ofSeconds(1).toMillis,
TimeUnit.MILLISECONDS
)
logger.info("startCollecting done")
}
// Schedulerを止める
def stopCollecting():Boolean = {
val b = scheduledCollection.cancel(false)
scheduledCollection = null
b
}
}
これで、1s毎にKamonの方でデータが収集出来るようになります。
集めたデータを報告してみる
本来であればDatadogやPrometheusといった外部サービスにメトリクスを転送するのですが、ここでは簡単にログに吐くことで確認してみようと思います。
package com.github.uryyyyyyy.kamon.simple.reporter
import com.typesafe.config.Config
import kamon.MetricReporter
import kamon.metric.PeriodSnapshot
import org.slf4j.LoggerFactory
// kamon.MetricReporterを継承
class MyReporter extends MetricReporter {
private val logger = LoggerFactory.getLogger(classOf[MyReporter])
// メトリクスが収集されるとこのメソッドが呼ばれる
override def reportPeriodSnapshot(snapshot: PeriodSnapshot): Unit = {
logger.info("reportTickSnapshot")
snapshot.metrics.counters.foreach(metric => {
logger.info(s"name: ${metric.name}, tags: ${metric.tags}, unit: ${metric.unit}, value: ${metric.value}")
})
snapshot.metrics.histograms.foreach(metric => {
logger.info(s"name: ${metric.name}, tags: ${metric.tags}, unit: ${metric.unit}, value-sum: ${metric.distribution.sum}")
})
snapshot.metrics.rangeSamplers.foreach(metric => {
logger.info(s"name: ${metric.name}, tags: ${metric.tags}, unit: ${metric.unit}, value-max: ${metric.distribution.max}")
})
}
// 前処理
override def start(): Unit = {
logger.info("MyReporter start")
}
// 後処理
override def stop(): Unit = {
logger.info("MyReporter stop")
}
// 再設定
override def reconfigure(config: Config): Unit = {
logger.info("MyReporter reconfigure")
}
}
ほぼ最小構成ですがこのような形で、メトリクスを取得すると reportPeriodSnapshot
が呼ばれるので、メトリクスの種類に応じてそれぞれ何を実施するかを実装していきます。
ここではログの属性と取得したデータをログに吐き出すような処理を書いていますが、外部サービスに送るプラグインを書くときは start()
などで初期設定をすることになるでしょう。
実行してみる
上記のCollectorとReporterを呼び出す場合はこんな感じです。
package com.github.uryyyyyyy.kamon.simple
import com.github.uryyyyyyy.kamon.simple.collector.MyCollector
import kamon.Kamon
object Main {
def main(args: Array[String]): Unit = {
// reporterの作成
Kamon.loadReportersFromConfig()
// collectorの実行
MyCollector.startCollecting()
Thread.sleep(10000)
// collectorの終了
MyCollector.stopCollecting()
// reporterの終了
Kamon.stopAllReporters()
Kamon.scheduler().shutdown()
System.exit(0) // 何かkamonでスレッドを使っていて止まらないので終了させる
}
}
ログはこんな感じ
$ ./bin/simple
2017-12-21 23:13:46,667 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - MyReporter start
2017-12-21 23:13:46,668 [INFO] from kamon.ReporterRegistry in main - Loaded metric reporter [com.github.uryyyyyyy.kamon.simple.reporter.MyReporter]
2017-12-21 23:13:46,672 [INFO] from com.github.uryyyyyyy.kamon.simple.collector.MyCollector in main - startCollecting done
2017-12-21 23:13:47,009 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - reportTickSnapshot
2017-12-21 23:13:47,673 [INFO] from com.github.uryyyyyyy.kamon.simple.collector.MyMetrics in kamon-scheduler-1 - MyMetrics update
2017-12-21 23:13:48,013 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - reportTickSnapshot
2017-12-21 23:13:48,038 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - name: my-reporter.my-metrics.counter1, tags: Map(), unit: MeasurementUnit(Dimension(none),Magnitude(none,1.0)), value: 1
2017-12-21 23:13:48,044 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - name: my-reporter.my-metrics.hist1, tags: Map(), unit: MeasurementUnit(Dimension(none),Magnitude(none,1.0)), value-sum: 10
2017-12-21 23:13:48,045 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - name: my-reporter.my-metrics.sampler1, tags: Map(), unit: MeasurementUnit(Dimension(none),Magnitude(none,1.0)), value-max: 2
2017-12-21 23:13:48,672 [INFO] from com.github.uryyyyyyy.kamon.simple.collector.MyMetrics in kamon-scheduler-2 - MyMetrics update
2017-12-21 23:13:49,002 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - reportTickSnapshot
2017-12-21 23:13:49,002 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - name: my-reporter.my-metrics.counter1, tags: Map(), unit: MeasurementUnit(Dimension(none),Magnitude(none,1.0)), value: 1
2017-12-21 23:13:49,002 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - name: my-reporter.my-metrics.hist1, tags: Map(), unit: MeasurementUnit(Dimension(none),Magnitude(none,1.0)), value-sum: 10
2017-12-21 23:13:49,003 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - name: my-reporter.my-metrics.sampler1, tags: Map(), unit: MeasurementUnit(Dimension(none),Magnitude(none,1.0)), value-max: 4
2017-12-21 23:13:49,672 [INFO] from com.github.uryyyyyyy.kamon.simple.collector.MyMetrics in kamon-scheduler-1 - MyMetrics update
2017-12-21 23:13:50,004 [INFO] from com.github.uryyyyyyy.kamon.simple.reporter.MyReporter in loaded-from-config: com.github.uryyyyyyy.kamon.simple.reporter.MyReporter - reportTickSnapshot
...
まとめ
とりあえず動かすことは出来ました。
が、メトリクスタイプがCounterの時に数字が毎回resetされているのがわかりますね。0.6系では無かった分岐が入っていたりしてちょっと謎です。。
また、RCになってからAPIがころころ変わっていたり、コミット履歴を見るとちゃんとレビューされているのか不安になったりで、本当にRCか?と疑いたくなるクオリティですが、様子を見守っていこうと思います。
(ちなみに筆者はStackdriver Minitoringへ送るプラグインが無かったので作りました。こちらもそろそろv1.0対応させていきたいですね)