Kotlinでロギング用のライブラリを実装し始めたので、その紹介をします。
ひとまず、以下の機能を備えています。
TRACE
, DEBUG
, INFO
, WARN
, ERROR
の順に5レベルPlain
Simple
Detail
val logger = LoggerFactory()
.addProvider(ConsoleProvider())
.addProvider(FileProvider("./test.log"))
.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")
ざっくりこんな感じになります。
ちなみにデフォルトのログレベルは DEBUG
なのでTRACE
レベルのログは出てきません。
気になるキーワードがいくつかありますね
名前の通り、コンソール出力をするためのものです
これも見たままですね、ファイルに対して出力をしてくれます
val fileProvider: FileProvider
val logger = LoggerFactory()
.addProvider(ConsoleProvider{
logLevel = LogLevel.INFO
})
.addProvider(FileProvider("./test.log"){
logLevel = LogLevel.TRACE
enableBuffer = true
bufferSize = 2048
rotation = SizeBaseRotation(size = 4096)
}.apply {
fileProvider = this
})
.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")
fileProvider.flush()
例えばこのように設定します。なんか設定が増えましたね。。。。
ConsoleProvider{
logLevel = LogLevel.INFO
}
FileProvider("./test.log"){
logLevel = LogLevel.TRACE
enableBuffer = true
bufferSize = 2048
rotation = SizeBaseRotation(size = 4096)
}
プロバイダ単位のログレベルの設定をします。
コンソールはINFO
レベルに設定したので、TRACE
とDEBUG
は無視されます。
ファイルの方は TRACE
になっているので、すべてのログが吐き出されることになります。
ファイル出力のバッファを有効にして、そのサイズを指定しています。今回は、2048にしているので、 2KiB単位でファイルに出力することになります。ようするにディスクI/Oを減らすための機能です。
これはログローテーションですね。ひとまずサイズベースのローテーションを実装しています。ファイル書き込み後、ここで指定したサイズを上回っていた場合にファイルをローテーとします。
と、ここまで読むと、バッファを有効にしたとき、それを上回らない限りログは出力されないのかと疑問に思うかと。
正解です。なので、そうならないように flush()
を呼び出してあげることで、バッファに溜まっているものをファイルに書き出してあげることができます。
ログ出力の色は、ANSIカラーコードを使って色を変えています。具体的にそれがなんなのかの説明はここでは省きますが、以下のようにして色を変えることができます。
ConsoleProvider{
traceLevelColor = AnsiColor.BLACK
}
ここでは、 TRACE
レベルの色を黒に変えています。デフォルトは白です
現在、ログのフォーマットの種類をビルトインで何種類か作っています。
なにも付加情報を与えず、単純に書き出します。
ERROR LEVEL LOG
どこがシンプルなのかはわかりませんが、
2023/10/09 16:37:58 [ERROR] - ERROR LEVEL LOG
のような出力になります。
SimpleFormatter
よりちょっとだけ詳しくなります。
2023/10/09 16:39:27 (main)[ERROR] - ERROR LEVEL LOG
スレッド名が付きました。だけです。
こんな感じのフォーマッターを実装して、プロバイダに食わせます。
object CustomFormatter : Formatter(
"""
${Element.YEAR}/${Element.MONTH}/${Element.DAY}
${Element.HOUR}:${Element.MINUTE}:${Element.SECOND}
[${Element.LEVEL}] - ${Element.MESSAGE}
""".trimIndent()
.replace("\n", "")
)
ConsoleProvider{
formatter = CustomFormatter
}
ある程度パッと見でできるようになっていますが、中の実装を見るとがっかりすると思います。
単にreplace
でどんどんチェーンしているだけです。
abstract class Formatter(private val fmt: String) {
fun format(msg: String, level: LogLevel): String {
val dt = LocalDateTime.now()
return fmt.replace(Element.YEAR.toString(), String.format("%4d", dt.year))
.replace(Element.MONTH.toString(), String.format("%02d", (dt.monthValue + 1)))
.replace(Element.DAY.toString(), String.format("%02d", dt.dayOfMonth))
.replace(Element.HOUR.toString(), String.format("%02d", dt.hour))
.replace(Element.MINUTE.toString(), String.format("%02d", dt.minute))
.replace(Element.SECOND.toString(), String.format("%02d", dt.second))
.replace(Element.MESSAGE.toString(), String.format("%s", msg))
.replace(Element.LEVEL.toString(), String.format("%s", level))
.replace(Element.THREAD.toString(), String.format("%s", Thread.currentThread().name))
}
}
このログツールのキモみたいな箇所です。
デフォルトで用意しているのはコンソール出力とファイル出力ですが、それ以外の出力も簡単に作れます。
ログレベルとか、フォーマットとかなにも考えなければ
class NetworkProvider: Provider{
override fun write(name: String, str: String, level: LogLevel) {
// ネットワーク越しにログを投げる
api.post(str)
}
}
みたいなクラスを作って食わせたら投げられます。 Config
を生やしたい場合はもう少しちゃんとハンドルしてあげないといけませんが。
FileProvider
がスレッドセーフになっていないので、おそらく衝突します。なんとかしないとね。
いまさら必要?