Dartのログ出力に呼び出し元の情報を含めてクリックで飛べるようにする

simple_logger というパッケージにもしました🎯

mono 
Flutter 🇯🇵
11 min readDec 17, 2018

--

何気なく呟いたこのツイートが反応良かったので記事にしました。

一連のツイートに書いた通り、やっていること自体は全く大したことないのと、後述の通りログクリックでジャンプできるのはAndroid Studio(とおそらく他のJetBrains製IDEも)だけで、Visual Studio Code(以後VS Codeと表記)では少なくとも標準では目視で確認できるだけです(それでもログの場所が分からないよりはずっと良いと思いますが)。

サブタイトルに書いた通りパッケージ化した(二番煎じかもしれませんが)ので、使うだけならこちらをぜひ使ってみてください。READMEにざっと利用例書いてありますが、シンプルなので読まずとも勘で使えるレベルだと思います( ´・‿・`)

呼び出し元の情報をログに含めるやり方

気になる実装は、一番シンプルに書くと次のようになります。stack_trace標準パッケージTraceクラスTrace.current([int level = 0]) を使って得たトレース情報に含まれる、Dartコードの位置情報を持つFrameのリストから先頭を取得して print に渡す文字列に足すだけです。

level に1を指定しているのは、この場合はトップレベルの関数であり、直前の呼び出し元に遡れば良いからです。関数を分割したりクラスに含めたりしてネストされるとそれに応じてlevelを増やすように調節しないと log 関数を呼び出している行とズレてしまうので注意です。

ちなみに、上の例では適当に log という名前にしていますが、mathパッケージlog と被るのであまり良い名前では無いと思います。

重めの処理なので注意

loggingパッケージのソース にも似たような、スタックトレース扱うコード(該当行だけではなく全体が返ってくる)がありますが、重めなようなので注意です。同様に、リリースビルドでは呼び出し行取得周りの処理はカットするようにした方が良いと思います。

loggingパッケージのソース

Android Studio ではクリックすると関数を呼んだ箇所にジャンプする

Android Studio では、次のようにFrameを出力した箇所がリンク化されて、クリックするだけでその箇所にジャンプできてとても便利です。

つまり、やったこととしては上のたった数行のコードの関数から出力されたログをクリックする様子をキャプチャーしてツイートしただけでした( ´・‿・`)

青い部分をクリックすると該当行へジャンプ

VS Codeではそのような機能が標準では無かったですが、もしかしたら対応した拡張があるかもしれません( ´・?・`)

Flutterの初期学習には古き良きprintデバッグも有用な気がする

Flutterは宣言的なUIフレームワークであり、ライフサイクル周りの把握が初めは少し戸惑いがちだと思います。

次のように適当に色々なメソッドにログを仕込んで観察するとそのあたりの把握が少し捗るかもしれません。

起動してボタンを2回押した時の様子

慣れたら、ログを仕込まなくともパフォーマンスタブで大体動きが掴めますが。

せっかくなのでパッケージ化

個人的にも色々なアプリでサクッと使いたいなと思っていて、その度にログコードをコピペするのも微妙なのでパッケージ化しました。

すでに似たものがあるかなと思って手短に探したものの、シンプルかつ呼び出し行表示に対応したものが見当たらなかったのですが、よく探せばあるかもしれません。

パッケージにするとどうしても丁寧なコードを書くため、いろいろ細かいところが勉強になりますし、二番煎じでも良いかと思いました。

simple_loggerパッケージのコンセプト

以下を心がけて作りました。

  • シンプル
  • ログ出力した行付きで出力
  • デフォルトでログレベルに絵文字を添えて見やすくする(個人の好み・変更可能)
  • 標準loggingパッケージとインターフェースを揃える
  • ログ出力フォーマットを変えられる
  • リリースビルド時にパフォーマンスへの影響・副作用の発生を抑えられるように
  • ログ出力後に任意の処理を挿入できる
  • これ以上の機能を付けるのはとても慎重に(実装的にも機能的にもこれ以上複雑にしたく無い)

以下、少し補足していきます。

シンプル

重厚なログライブラリーがよくありますが、simple_loggerはシングルトンで初期化方法も SimpleLogger() だけです。

ちなみにDartだと以下のように Factory constructor でシングルトンを定義するのが一般的のようです。

定義はあまり違和感無いですが、使うときに SimpleLogger() となるのは毎回インスタンス生成しているような印象ですしシングルトンかどうか区別付かないので、正直あまりしっくり来ないです🤔

ログ出力した行付きで出力

本記事の主題ですね。ただ、上述の通り重めの処理っぽいのでデフォルトで無効化してしまっています。次のように任意のログレベル設定ついでに適宜オンにして活用してください。

logger.setLevel(
Level.FINEST, // 適当なレベルを設定
includeCallerInfo: true, // リリースビルドではfalseにする
);

標準loggingパッケージとインターフェースを揃える

次のようにインスタンスを得たとして、

final logger = SimpleLogger();

次のような感じでログレベルに応じたメソッドでログ出力できます。

logger.info('Hello info!');
logger.shout('Hello shout!');

標準loggingパッケージのLevel が低レベル順に以下のようになっていて、それぞれに対応したメソッドを用意しているのと合わせてます。

  • FINEST
  • FINER
  • FINE
  • CONFIG
  • INFO
  • WARNING
  • SEVERE
  • SHOUT

なので、アプリのソースコード内でsimple_logger を使ってlogger.shout('( ´・‿・`)'); など適当に各所に書いてても、後から標準loggingパッケージやそのログレベルに準拠した他のライブラリーに乗り換えても書き換えせずに済むはずです。

このLevelクラス参照のためだけに標準loggingパッケージに依存していますが、内部実装は自前です。

リリースビルド時にパフォーマンスへの影響・副作用の発生を抑えられるように

以下はソースコードのうちログメッセージを出力用のメッセージに変換している部分です。

simple_logger.dart

messagedynamic 型で文字列以外も許容していて、特にFunctionだった場合はその実行結果を出力するようにしています。

つまり、次のように書いておいておくと、リリースビルドではログレベルを上げておけばその前の isLoggable チェックで弾かれるため関数内の処理は一切実行されません。

logger.finest(() { 
// 重い処理
// return '計算結果';
});

Swift なら @autoclosure がある ので、呼び出し側で気をつけずとも以下のようにそのまま文字列の書き方でも遅延評価されて便利なのですが、Dartには同等の言語機能が無いため、スキップされるログレベルの文字列評価を避けたい場合は上のように呼び出し側でクロージャーとして渡す必要があります。

// Dartの場合、以下の文字列は即時評価されてしまう
logger.finest('${/* 重い処理 */}');

ログ出力後に任意の処理を挿入できる

シンプルコンセプトとは少し離れますが、とはいえ最低限実用的に使えるように入れた機能です。

実際のアプリ開発では、例えば「ログレベルがSEVERE以上の時は単に標準出力を吐くだけではなく Crashlytics にも送りたい」などのシーンがあります。

そういう場合、loggerのonLogged に予め次のように処理を挟み込んでおくと対応できます。

logger.onLogged = (log, info) {
if (info.level >= Level.SEVERE) {
// Crashlyticsにエラーを送る。
}
};

これ以上の機能を付けるのはとても慎重に(実装的にも機能的にもこれ以上複雑にしたく無い)

高機能なロガーが欲しくなったら、他の良さそうなログパッケージを探したり、標準loggingパッケージ使ってアプリ要件にあったものを自作したりすることをお勧めします。上述の通り、乗り換えのしやすさにも配慮しています。

ただ、大抵のアプリは今回作った simple_logger 程度で十分だと思っていて、僕も自分に選定権がある場合は基本的に simple_logger を使っていくつもりです。

オススメの使い方

最後に simple_logger のオススメの使い方を置いておきます。

インストール

まず、当たり前ですが、 pubspec.yaml に以下を記述しておいて pub getflutter packages get でインストールします。

dependencies:
simple_logger: ^0.7.2

logger.dartファイルを追加

用途に合わせて、次のようにグローバルにアクセスできるようなインスタンスを置きます。

先ほどはシングルトンの書き方がしっくり来ないと書きましたが、この Cascade notation (..)は最高だと思っています。これのおかげでクラス提供側は何の工夫もすることなく、利用者側はインスタンスを取得ついでにメソッドチェーン的に設定を適用することができます。

色を付けるとさらに見やすく

Grep Consoleというプラグインを使うと、ログレベルの文字列によってログを色分け表示してくれるので、さらに見やすくなります✨

例えば、このようにできます:

この場合、次のように設定しています。
(さらに「SHOUTの時は音を出す」などの設定も可能です。)

--

--