February 18, 2019

NapierというKotlin Multiplatform用のログライブラリを作った

Motivation

Kotlin Multiplatformの構成がDroidKaigi2019 に導入されたことで、日本ではかなり注目度が高まっているように感じる。(嬉しい)

ご存知の通りKotlin Multiplatform ProjectのCommon moduleでは、Kotlinで書かれた(各プラットフォームに依存しない)コードしか使用することが出来ない。 サンプル程度の量であればそこまで問題ないが、プロジェクトレベルの開発となるとどうしても必要となってくるのが、ログライブラリだ。
現状Common moduleで使えるのは println のような標準出力のみとなっているため、表示場所の情報等が一切表示されない問題と、リリース用の配布用アプリにもそのままログが出力されてしまう問題がある。

そこで、Napierという名前のKotlin Multiplatform用のログライブラリを作った。

https://github.com/AAkira/Napier

logo

使い方

Dependencies

jCenterにも上げたので、通常はjCenter経由で取得の方が楽だと思う。
gradleファイルに以下を記述していく。

repositories {
    jCenter()
}

def napierVersion = "0.0.1"

kotlin {
    ...

    sourceSets {
        commonMain {
            dependencies {
                implementation "com.github.aakira:napier:$napierVersion"
            }
        }
        androidMain {
            dependencies {
                implementation "com.github.aakira:napier-android:$napierVersion"
            }
        }
        iosMain {
            dependencies {
                implementation "com.github.aakira:napier-ios:$napierVersion"
            }
        }
        jsMain {
            dependencies {
                implementation "com.github.aakira:napier-js:$napierVersion"
            }
        }
        jvmMain {
            dependencies {
                implementation "com.github.aakira:napier-jvm:$napierVersion"
            }
        }
    }
}

Common

例えばこんな感じのコードをCommonに書いてみる。

import kotlinx.coroutines.delay

class Sample {

    fun hello(): String {
        Napier.v("Hello napier")
        Napier.d("optional tag", tag = "your tag")

        return "Hello Napier"
    }

    suspend fun suspendHello(): String {
        Napier.i("Hello")

        runBlocking {
            delay(3000L)
        }

        Napier.w("Napier!")

        return "Suspend Hello Napier"
    }

    fun handleError() {
        try {
            throw Exception("throw error")
        } catch(e: Exception) {
            Napier.e("Napier Error", e)
        }
    }
}

そうすると各プラットフォームにはこのように表示される。

  • android
android
  • ios
ios

androidではLogcatを使っていて、iOSはprint、jvmではprintln、jsではconsole.logを使っている。

それぞれなんとなく見たことある形式だとは思うが、androidはTimber というログライブラリを真似ている。
ただこのTimber、Multiplatform対応をしようとしている。ただJakeにはつい最近子供が生まれて休職しているみたいだし、彼は物凄く多くのライブラリをメンテしているのでいつ対応されるかわからないので作成した。 Timberがmpp対応したら潔く乗り換えたいと思っているので、それまでの開発に使って欲しいというお気持ちですw (interfaceはなるべく揃えたので移行はとてもしやすいと思います)
iosに関してはioserに聞いてSwifty Beaver の形式を真似た。 Log Levelに若干差異があったが、そこはandroid側に合わせてしまった。 日付のフォーマット等は変えられるようにしているので、適宜プロジェクトに合わせて変えて欲しい。

Initialize

プロダクト開発をしていると、Debug buildではログを出したいけどRelease buildではログは吐かずにエラーログだけCrashlyticsや自前のログサーバに送りたくなる。
そこでNapierでは、Napier.base()Antilogを指定することで挙動を変えられるようにしている。
DebugAntilog というのは例にあるようにログの書いてある場所を表示する機能で、ライブラリ側で提供している。CrashlyticsAntilogに関しては、各々やりたい事が異なると思うので、サンプルを参考にしてプロダクトの要望に合う形にカスタマイズして使って頂ければ。
サンプルのCrashlyticsAntilogでは、エラーログ以上をCrashlyticsに送っている。このやり方は、前に担当していたプロジェクトでcatch出来たエラーに関してはアプリを落とさずにCrashlyticsで送信してstacktraceを見て都度対応していたので、参考になれば。

  • android

Application classで初期化をしている。例ではわかりやすいようにBuildConfigを用いているが、通常はDIで切り分けるのが定石だと思います。

if (BuildConfig.DEBUG) {
    // Debug build

    // init napier
    Napier.base(DebugAntilog())
} else {
    // Others(Release build)

    // init firebase crashlytics
    Fabric.with(this, Crashlytics())
    // init napier
    Napier.base(CrashlyticsAntilog(this))
}
  • ios

ios側もAppDelegateで初期化します。ios側はandroidとは異なりNapierの初期化コードをKotlin側にも書く必要があります。

fun releaseBuild(antilog: Antilog) {
    Napier.base(antilog)
}
#if DEBUG
// Debug build

// init napier
NapierProxyKt.debugBuild()

#else
// Others(Release build)

// init firebase crashlytics
FirebaseApp.configure()
Fabric.with([Crashlytics.self])

// init napier
NapierProxyKt.releaseBuild(antilog: CrashlyticsAntilog(
    crashlyticsAddLog: { priority, tag, message in
        let args = [tag, message].compactMap { $0 }
        CLSLogv("%@", getVaList(args))
        return .init()
},
    crashlyticsSendLog: { throwable in
        Crashlytics.sharedInstance().recordError(throwable)
        return .init()
}))
#endif

以上の初期化の設定が終われば、Debug時にはCommon moduleに書いたログが各プラットフォームに良い感じに表示され、Release時には致命的なエラーのみCrashlyticsに送信され、ログ表示は行われなくなる。

名前の由来

Timberというライブラリは木材とLogがかかっていて、初期化は

Timber.plant(tree)

で行う。 Treeはカスタマイズしたログの振る舞いを記述するクラスとなっている。
なんともオシャレ。

そこで、こちらは対数のLogとかけ対数の発見者であるジョン・ネイピア から名前を頂戴した。 Napierの初期化は

Napier.base(antilog)

となっており、Napierの底にantilogarithmを指定する形式にした。Timber程意味は噛み合っていないが、これはこれで気に入っている。

また、ロゴは自然対数の底ネイピア数eを逆にしている。
何故かと言うと
ご存知の通り
y = e^x
の逆関数は
y = logx
そうLogになるのだ。オシャレ

Kotlin Multiplatform Project開発する方は是非使ってみてフィードバック、PRをして頂けると助かる⭐️

© AAkira 2023