BLOGサブスレッドの日常

2022.03.16

Swiftのロギングライブラリ SwiftLogの紹介

s.kono

むおおおお!
気になる技術はもりもりあるものの、あまりそれらについて調べたり触ったりできていないむおおおおです。
気になるものから調べてブログにまとめていこうと思っています。

今回はSwiftのロギングライブラリであるSwiftLogについて書きます。

SwiftLog

何これ?

https://github.com/apple/swift-log

Swift言語向けのログライブラリです。
コミュニティベースで開発されているOSSライブラリですが、ほぼApple公式です。

下記のようにプログラムを記述することでログ出力ができます。(公式サンプルより抜粋)

import Logging

let logger = Logger(label: "com.example.BestExampleApp.main")
logger.info("Hello World!") # "2019-03-13T15:46:38+0000 info: Hello World!" のように出力される

特徴

  • 採取したログを出力する方法は、自由にカスタマイズ可能
    • デフォルトの出力先は標準出力
    • 例えばログの出力先を標準出力でなく、ログファイルとして出力する、どこかに送信する、等が可能
    • 採取したログをどのように扱うかは、SwiftLogが用意しているLogHandlerプロトコルに準拠するクラスをSwiftLogに与えることでカスタマイズ可能
  • SwiftLogはiOSやmacOSアプリはもちろんのこと、LinuxやWindows等(Androidも?)上でSwiftを動かすことも視野に入っている

このライブラリの目的は、ログに関して共通のAPIを提供することです。
あくまで共通のAPIを提供することが目的なので、標準出力に出す、ということ自体がSwiftLogの範疇外とも言えそうです。なのでカスタマイズ可能というのはライブラリのコンセプトそのものと言えます。

SwiftLog is an API package which tries to establish a common API the ecosystem can use.

SwiftLogを紹介しようと思った理由

以前よりiOSアプリ開発は行っていたのですが、Swiftにおいてロギングライブラリはどれを使うべきなのか、という結論が自分の中で出ていませんでした。
しかし、やっとかなり答えに近いんじゃないかなーと(個人的に)思ったのが見つかりました。それがSwiftLogでした。

以下が検討してそれぞれ微妙だなーと思っていたところです。

Swift言語標準のprint()/NSLog()等

  • print関数は単に文字列を出力するだけですので、ログと呼ぶには少々力不足と感じる
    • 少なくともログレベルは指定したい
    • 後でログをもっとリッチに扱いたい時には対応しづらいため、不安は残る
  • print関数はオーバーライドすることが可能なので、普段の開発ではこれだけで事足りる可能性はある
    • ただし通常プログラムを読む側はprint文はオーバーライドされていることを期待しない
      • print文をオーバーライドするコードを書いた人以外からすると「予期しない振る舞いをするプログラム」となり得る

Apple公式のロギングFramework

  • 種類が多く、迷走している感が否めない
    • iOS10より前は標準的なログライブラリ/フレームワークは用意されていない
      • (と思っています…知らないだけかもしれないです)
    • iOS 10からos_logが使えるになった
    • iOS 15からOSLogが使えるようになった
      • os_logとは似て非なるものが登場しました
      • あまり確認していないのですが、これはSwiftLogが標準として取り入れられたもの?

OSSロギングライブラリ

  • 結構デファクトスタンダードがどれかと言いづらい
    • 有名所は以下かなと思います
      • CocoaLumberjack
        • ObjC時代からあるものでSwiftに最適化されていない印象
        • 少々記述が冗長と感じる(個人の感想)
      • SwiftyBeaver
        • かなりリッチなロギングライブラリ
        • リッチすぎて、そこまで必要だろうか…と感じる
    • 他にも多くのGitHub Starsを集めた人気ロギングライブラリも多い
      • 「これを使えば堅い」といったものはない印象です

僕はライブラリをプロジェクトに導入するときには結構頭を捻ります。ライブラリを入れたことによるメリットの大きさとライブラリを更新していくためのメンテナンスコストを天秤にかける必要があるからです。
アプリに根付いたメンテナンスライブラリのマイグレーションコストばかりを払うことになるのは避けたいので、機能もそこまで多くなく安定した仕組みが欲しいと思っています。

いろいろ考えた結果、シンプルで必要なAPIのみを提供していて、ログ出力を自由にカスタマイズできるSwiftLogを選んだ、という感じです。

SwiftLogの使い方

基本的な使い方は冒頭の通りです。
実際の利用を想定すると、下記のようになると思います。

class MyLog {
    static let `default` = Logger(label: Bundle.main.bundleIdentifier!)
}

class MyViewController: UIViewController {
    private let logger = MyLog.default

    override func viewDidLoad() {
        super.viewDidLoad()
        logger.trace("viewDidLoad: called") // ログ出力
    }
}

(SwiftUIまだ慣れてないのでViewControllerとか書いててすみません)
実際に自分のアプリケーションで使うときは、デフォルトで使うLoggerインスタンスを一つstatic letで持っておくと共通化できて良いかもしれません。
Loggerインスタンスを各クラスでそれぞれ持っておくかは方は悩ましいのですが。。MyLog.default.trace()のように直接呼び出しても良いかもしれません。

staticにloggerを持っておいてそれを使い回すこと自体は設計上も問題ないかなーと思います。
(ロガーは、唯一Singletonデザインパターンが適していると言われる仕組みですね)

ログ出力方法をカスタマイズする (LogHandler)

SwiftLogは標準で標準出力にログを吐きます。
LogHandlerプロトコルに準拠したクラスを作り、それを指定すれば標準出力以外に出力することができます。

例えばLogHandlerプロトコルに準拠したMyLogHandlerクラスを作り、アプリ起動時(AppDelegate内など)で下記のように命令を呼び出しておくと、出力先を変更することができます。

LoggingSystem.bootstrap(MyLogHandler.init)

SwiftLogに含まれるMultiplexLogHandlerを使えば、一つのログを複数のLogHandlerに出力することもできます。

LoggingSystem.bootstrap { label in
    MultiplexLogHandler([
        MyLogHandler(label: label),
        StreamLogHandler.standardOutput(label: label)
    ])
}

サンプル:Firebaseにログを送信するLogHandler

試しに、SwiftLog経由でFirebase Crashlyticsにログを送るLogHandlerを書いてみました。
Firebase Crashlyticsにカスタムログを送るメソッドを使ってます。Firebase Crashlyticsが入っている前提ですが、Crashlytics.crashlytics().log()をprint()に置き換えても良きです。

LogHandler

import Logging
import FirebaseCrashlytics

public struct FirebaseLogHandler: LogHandler {
    private let label: String
    public var logLevel: Logger.Level = .info
    public var metadata = Logger.Metadata()

    public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
        get {
            return self.metadata[metadataKey]
        }
        set {
            self.metadata[metadataKey] = newValue
        }
    }

    // internal for testing only
    internal init(label: String) {
        self.label = label
    }

    public func log(level: Logger.Level,
                    message: Logger.Message,
                    metadata: Logger.Metadata?,
                    source: String,
                    file: String,
                    function: String,
                    line: UInt) {
        // trace/debugは記録しない
        if level == .trace || level == .debug {
            return
        }

        Crashlytics.crashlytics().log("\(level)/\(function)#L\(line) :\(metadata.map { " \($0)" } ?? "") \(message)")
    }
}

ログ出力のコード

// LogHandlerを設定する
LoggingSystem.bootstrap { label in
    MultiplexLogHandler([
        FirebaseLogHandler(label: label),
        StreamLogHandler.standardOutput(label: label)
    ])
}

// ログ出力
let logger = Logger(label: Bundle.main.bundleIdentifier!)
logger.info("example")

let userId = 12345
logger.info("example with metadata", metadata: [
    "user_id": "\(userId)"
])

出力結果

info/testLog()#L20 : example
info/testLog()#L23 : ["user_id": 12345] example with metadata

自作のLogHandlerは、上記のようにシンプルに記述することができます。
公式のStreamLogHandlerクラスのソースコードをコピーし、少し弄るとサンプルとなるLogHandlerクラスを用意できるので、もう少しリッチなものにする場合はそちらを参照するのが良いと思います。

まとめ

  • SwiftLogを使えばシンプルなAPIでリッチなログ出力が可能
  • ログ出力は自由にカスタマイズ可能
  • iOS/macOSはもちろん、他のプラットフォーム上でも同じAPIでログが取れるぞ
  • SwiftLogはいいぞ

この記事を書いた人

s.kono