2023-12-04 に公開
前回紹介したKotlin製ログツールの、もう一歩踏み込んだ使い方紹介みたいな感じのなにかです。
SlackではIncomming Webhookを使って、HTTP経由で投稿をすることができます。 今回は、ログツールをカスタムしてSlackに同時にログを流し込んでみます。
さて、まずは必要な依存関係を解決していきます。
build.gradle.ktsplugins {
kotlin("jvm") version "1.9.21"
// 追加
kotlin("plugin.serialization") version "1.9.21"
}
repositories {
mavenCentral()
// 追加
maven(url = "https://jitpack.io" )
}
dependencies {
// 追加
// colotok本体
implementation("com.github.milkcocoa0902:colotok:0.1.6")
// 追加
// HTTP#POST
implementation("com.github.kittinunf.fuel:fuel:2.3.1")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.3.1")
// 追加
// シリアライザ用
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
kotlinx-serialization周りのライブラリ、本来は必要ありませんが、今回SlackProviderを作る上で必要になります。
筆者のKotlin力が低いため。。。。
KSerializer<T>が解決できれば良いので、pluginはいらなくない?それにJSON用のを入れなくても KSerializer<T>あたりのIFだけ公開しているものがありそう。
colotok標準ではSlackにログを吐き出すことはできません。が、Providerを継承してあげることで、Slackに吐き出すことが可能になります。
便宜上、SlackProviderと呼ぶことにします。その実装はおおよそ以下のようになります。
SlackProviderclass SlackProvider(config: SlackProviderConfig): Provider {
constructor(config: SlackProviderConfig.() -> Unit): this(SlackProviderConfig().apply(config))
class SlackProviderConfig() : ProviderConfig {
var webhook_url: String = ""
override var logLevel: LogLevel = LogLevel.DEBUG
override var formatter: Formatter = DetailTextFormatter
/**
* no effect
*/
override var colorize: Boolean = false
}
private val webhookUrl = config.webhook_url
private val logLevel = config.logLevel
private val formatter = config.formatter
// テキストログ用のフォーマッタを指定したときに呼び出される
override fun write(name: String, msg: String, level: LogLevel) {
if(level.isEnabledFor(logLevel).not()){
return
}
kotlin.runCatching {
webhookUrl.httpPost()
.appendHeader("Content-Type" to "application/json")
.body("""
{"text": "${formatter.format(msg, level)}"}
""".trimIndent())
.response { _, _, _ -> }
}.getOrElse { println(it) }
}
// 構造化ログ用のフォーマッタを指定したときに呼び出される
override fun <T : LogStructure> write(name: String, msg: T, serializer: KSerializer<T>, level: LogLevel) {
if(level.isEnabledFor(logLevel).not()){
return
}
kotlin.runCatching {
webhookUrl.httpPost()
.appendHeader("Content-Type" to "application/json")
.body("""
{"text": "${formatter.format(msg, serializer, level)}"}
""".trimIndent())
.response { _, _, _ -> }
}.getOrElse { println(it) }
}
}
さて、要点を見ていきましょう。
コンフィグとは、以下の部分を指します。
SlackProvider.kt#SlackProviderConfigclass SlackProviderConfig() : ProviderConfig {
var webhook_url: String = ""
override var logLevel: LogLevel = LogLevel.DEBUG
override var formatter: Formatter = DetailTextFormatter
/**
* no effect
*/
override var colorize: Boolean = false
}
ある程度一般化した ProviderConfig インターフェースがあるので、それを使うと便利かもしれません。使わなくても問題ありません。
colotokでは、こういった実装を推奨しています。
ここでは、デフォルトのものに加えて、 webhook_urlを受け取れるようにしています。
続いて、実際にSlackに投稿する部分を書いていきます。具体的には、writeメソッドの中身を書いていきます。
テキストログも構造化ログもだいたい同じような内容になるので、片方だけ抽出すると、以下のようになります。
SlackProvider.kt#write()// テキストログ用のフォーマッタを指定したときに呼び出される
override fun write(name: String, msg: String, level: LogLevel) {
if(level.isEnabledFor(logLevel).not()){
return
}
kotlin.runCatching {
webhookUrl.httpPost()
.appendHeader("Content-Type" to "application/json")
.body("""
{"text": "${formatter.format(msg, level)}"}
""".trimIndent())
.response { _, _, _ -> }
}.getOrElse { println(it) }
}
さて、特に難しいことはしていません。
LogLevelクラスにはisEnabledFor(level: LogLevel)というメソッドが生えており、これを使うことで、今回の呼び出しが有効かどうか判断することができます。
指定したログレベル以上のレベルの呼び出しであればtrueを、そうでなければfalseを返すので、falseであればなにもする必要がないですね。
そして、肝心のSlackに投稿する部分ですが、こちらは webhook_urlに対して {"text": "something text to post"} のような形式のJSONを送ってあげることで実現することができます。
今回実装した SlackProviderは、以下のように使用することができます。
ここでは、DEBUGレベル以上のログをコンソールに、WARNレベル以上であればSlackにも流すようにしてみます。
Main.kt
fun main(){
val logger = LoggerFactory()
.addProvider(ConsoleProvider{
formatter = SimpleTextFormatter
logLevel = LogLevel.DEBUG
})
.addProvider(SlackProvider{
webhook_url = "<your slack webhook url>"
formatter = DetailTextFormatter
logLevel = LogLevel.WARN
})
.getLogger()
logger.trace("TRACE LEVEL LOG")
logger.debug("DEBUG LEVEL LOG")
logger.info("INFO LEVEL LOG")
logger.warn("WARN LEVEL LOG")
logger.error("ERROR LEVEL LOG")
Thread.sleep(5_000)
}
使い方は簡単ですね。他のProviderと同じように addProvider(provider: Provider)を呼び出してあげるだけです。
これを実行すると、以下のようになります。
| コンソール出力 | Slack出力 |
|---|---|
![]() |
![]() |
コンソールにはDEBUG以上のものが流れ、SlackにはWARNレベル以上のものが流れていることがわかります。
mainの最後で Thread.sleep(5_000)をしているのは、SlackへのHTTP#POSTを待ち合わせるための適当な時間です。
これをしないと、今回の場合はSlackに流れる前にプログラムが終了してしまいます。
今回のサンプルはこちらにおいています。
ツールはまだまだ開発途上ですが、よかったらスターをいただけると嬉しいです。
開発のモチベーションは、コードベースでログの設定が書けると嬉しいなぁ です。
xmlやらの外部リソースでログの振る舞いを変えるの、差し替えるときに再コンパイルの必要がないなどのメリットもあると思っていて、プロジェクトが肥大化していくほどその恩恵は大きいでしょうが、最近は並列ビルドだったりビルドキャシュだったり、ビルドシステムそのものがだいぶ優秀になってきているので、そのメリットを差し置いてもコードで設定が色々書けると嬉しいなと、個人的に思っています。