Kotlin/NativeでHTTPサーバーを作るアドベントカレンダー(24日目:アクセスログファイル)

この記事はkdnaktの1人 Advent Calendar 2020の24日目の記事です(2日遅れ)。

 

2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。

 

 

[Kotlin/Native用ログライブラリ]

Kotlin/Nativeでアクセスログをファイルに書き込むにあたって、ちょうどよいライブラリがないか探してみました。

 

2020年12月現在、AAkira/Napierflorent37/Multiplatform-LogMicroUtils/kotlin-loggingなど、いくつかKotlin Multiplatform対応のログライブラリはあるのですが、Kotlin/Nativeをサポートしているものは見つかりませんでした。

 

仕方がないのでログをファイルに書き込む仕組みを自作してみます。

 

[アクセスログをファイルに書き込む]

ファイルへの書き込みはこちらのブログ記事を参考に実装しました。C言語同様に、fopen()でファイルを開き、fputs()でファイルへログを出力します。

www.nequalsonelifestyle.com

 

Logger.ktを以下の内容で作成します。

package com.kdnakt.kttpd

import kotlinx.cinterop.memScoped
import platform.posix.EOF
import platform.posix.fclose
import platform.posix.fopen
import platform.posix.fputs
import kotlinx.datetime.*

enum class LogLevel(val logPrefix: String) {
    ERROR("[ERROR]"),
    INFO("[INFO ]"),
    DEBUG("[DEBUG]");
}

class Logger(val path: String,
             val level: LogLevel = LogLevel.INFO)

fun Logger.error(log: String) {
    write(log, LogLevel.ERROR)
}

fun Logger.info(log: String) {
    write(log, LogLevel.INFO)
}

fun Logger.debug(log: String) {
    write(log, LogLevel.DEBUG)
}

private fun Logger.write(log: String, minLevel: LogLevel) {
    if (level < minLevel) return
    val logString = "${minLevel.logPrefix} ${now()} $log"
    println(logString)
    val file = fopen(path, "a") ?:
        throw IllegalArgumentException("Cannot open output file $path")
    try {
        memScoped {
            if(fputs("$logString\n", file) == EOF) throw Error("File write error")
        }
    } finally {
        fclose(file)
    }
}

private fun now() = Clock.System.now().toLocalDateTime(TimeZone.of("Asia/Tokyo"))

 

三段階でログレベルを設定できるようにしています。KotlinではEnumの定義順でordinal(順序)が0始まりでプロパティに追加され、比較演算が可能です。

Enum Classes - Kotlin Programming Language

 

また、ログ出力時の接頭辞としてログレベルを出力したかったので、LogLevelに直接文字列のプロパティとして持たせています。Kotlin/NativeではString.format()関数が使えず、パディングを調整するのが面倒だったので、このような雑な実装になっています。

ファイルへの書き込み部分は毎回ファイルを開くようになっておりパフォーマンスがやや心配ですが、今後の改善課題としておきます。

 

main.ktの実装を以下のように修正し、起動時引数でログレベルを指定できるようにします。

fun main(args: Array<String>) {
    val argsParser = ArgParser("kttpd")
    val port by argsParser.option(ArgType.Int, shortName="p").default(8080)
    val logLevel by argsParser.option(ArgType.Choice(
            listOf("error", "info", "debug")
    ), shortName = "l").default("info")
    argsParser.parse(args)

    val log = Logger("access_log", LogLevel.valueOf(logLevel.toUpperCase()))
    log.info("kttpd start: localhost:$port")
    // 省略
}

 

gradle buildでビルドし、ログレベルを指定せずに起動し、curl localhost:8080/index.htmlでアクセスすると以下のようなログが残ります。

f:id:kidani_a:20201226153225p:plain

 

起動時引数で--logLevel debugと指定すると、以下のようにデバッグログがターミナルに出力されます。

f:id:kidani_a:20201226153307p:plain

 

ログファイルにも同じ内容が記録されています。

f:id:kidani_a:20201226153528p:plain

  

[まとめ]

  • Kotlin/Native用のログライブラリがなかったのでログ出力の仕組みを自作した
  • 実装中のコードは以下のリポジトリにまとめてある

github.com