Scala
Kamon
ScalaDay 21

Kamon v1.0系を触ってみた

この記事は、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に送ります。

MyMetrics.scala
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を作成します。

Mycollector.scala
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といった外部サービスにメトリクスを転送するのですが、ここでは簡単にログに吐くことで確認してみようと思います。

Mycollector.scala
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を呼び出す場合はこんな感じです。

Main.scala
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でスレッドを使っていて止まらないので終了させる
  }

}

ログはこんな感じ

application.log
$ ./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対応させていきたいですね)

https://github.com/uryyyyyyy/kamon-stackdriver