🐷

シンプルでカラフルで拡張しやすい FirebaseCrashlytics 連携可能な Dart 製ロガーパッケージを公開した話

2022/05/06に公開

はじめに

シンプルでカラフルで拡張しやすい FirebaseCrashlytics 連携可能な Dart 製ロガーパッケージを作成して pub.dev に公開しました。パッケージ名は roggle です(「ログル」と呼びます)。roggle は logger のアナグラムです🐷

https://pub.dev/packages/roggle

このパッケージを作成するに至った経緯、パッケージの使い方、パッケージを作成する際に工夫したことをまとめました。

なぜパッケージを作成したのか?

少し前に次の記事を書きました。

https://zenn.dev/susatthi/articles/20220413-153500-flutter-logger

記事を書く中でいろいろなロガーパッケージをお試しで使ってみたのですが、自分が欲しい機能をすべて満たすものがなかったので、自分で作ることにしました。はじめは公開するつもりは無かったのですが、自分が複数のプロジェクトで使う可能性もあるし、FirebaseCrashlytics 連携をしたロガーは他になくニーズがあるかもわからないので公開することにしました。

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

  • ログメッセージに必要十分な以下の情報が含まれており、かつカスタマイズが可能
    • 心を賑やかにしてくれる絵文字💡
    • フィルタに便利なロガー名
    • 視覚的にわかりやすいログレベルのラベル表示
    • ロガーにはほぼ必須のタイムスタンプ
    • 呼び出し場所(ファイル名、行、メソッド名)
  • 見やすくするためにメッセージがカラフル
  • 1メッセージ1行で出力する
  • デバッグモード時、深刻なログレベルの場合に処理を停止できる
  • リリースモード時、深刻なログレベルの場合に FirebaseCrashlytics にエラーレポートを送信できる
  • logger パッケージの高い拡張性をそのまま継承
  • logger パッケージから容易に乗り換えが可能

他のロガーパッケージとの違い

logger は自分が欲しいロガーパッケージに一番近かったのですが、ログが複数行に出力されて少しうるさい感じがしました。あと logger パッケージ内で try-catch されてしまってログ出力内で例外を投げても意図的に処理を停止させることができませんでした。

simple_logger も好きなロガーです。2021 年に作ったアプリではこのロガーを使っていました。しかしカラフルではありませんでした。ログが埋もれてしまうので、どうしてもカラフルにしたかったです。あとログレベルの I/F がちょっと好みでは無かったです。

また、FirebaseCrashlytics 連携を意図したロガーは探してみた限り見つかりませんでした。デバッグビルド時はコンソールに出力して、リリースビルド時に深刻なエラーが発生したら FirebaseCrashlytics にエラーを送信したい、というニーズはあるかなぁと思ったのですが。。

roggle パッケージの設計方針

まったくの自作をするか、機能的に一番近い logger パッケージに依存させるか悩みましたが、大枠の設計が同じで、メッセージのカラフル化の機能を流用したかったので logger パッケージに依存させることにしました。

https://github.com/susatthi/roggle/blob/0db7fd28278ee26c63e699ee77f4d80e0457f2d0/pubspec.yaml#L10-L11

roggle パッケージの使い方

インストール

pubspec.yaml を次のように変更して flutter pub get を実行してインストールしてください。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.4
+  roggle: <latest version>

すぐに使い始める

logger パッケージと同じく、カスタマイズなしでもいい感じにすぐに使い始められます。必要なことは final logger = Roggle(); だけです。これで、デバッグビルド時はコンソールにログを出力して、リリースビルド時はどこにも出力しません。

import 'package:roggle/roggle.dart';

final logger = Roggle();

void main() {
  logger.v('Hello roggle!');
  logger.d(1000);
  logger.i(true);
  logger.i([1, 2, 3]);
  logger.i({'key': 'key', 'value': 'value'});
  logger.i({'apple', 'banana'});
  logger.i(() => 'function message');
  logger.w(Exception('some exception'));
  logger.e(NullThrownError());

  try {
    throw Exception('some exception');
  } on Exception catch (e, s) {
    logger.wtf('wtf...', e, s);
  }
}

カスタマイズ無し

ログの出力内容をカスタマイズする

以下はログメッセージの構成です。

めちゃくちゃシンプルにした例です。メッセージしか表示しません。

import 'package:roggle/roggle.dart';

final logger = Roggle(
  printer: SinglePrettyPrinter(
    colors: false,
    printCaller: false,
    printEmojis: false,
    printLabels: false,
    printTime: false,
  ),
);

void main() {
  logger.i('Hello roggle!');
}

シンプル

絵文字はやめて、タイムスタンプは年月日も表示した例です。

import 'package:intl/intl.dart';
import 'package:roggle/roggle.dart';

final logger = Roggle(
  printer: SinglePrettyPrinter(
    printEmojis: false,
    timeFormatter: (now) => DateFormat('yyyy/MM/dd HH:mm:ss.SSS').format(now),
  ),
);

void main() {
  logger.i('Hello roggle!');
}

絵文字なし

他にも、levelColors でカラーパターンを変更したり、levelEmojis で絵文字パターンを変更したり、levelLabels でラベルパターンを変更できます。

スタックトレースを出力する

特定のログレベル以上(例えば Level.warning 以上)の場合にログメッセージとともにスタックトレースも出力したい場合は次のようにします。

import 'package:roggle/roggle.dart';

final logger = Roggle(
  printer: SinglePrettyPrinter(
    stackTraceLevel: Level.warning,
  ),
);

void main() {
  logger.w(Exception('some exception'));
}

スタックトレース

表示するスタックトレースの最大メソッド数を変更したり、スタックトレースのプレフィックス(デフォルトは |)を変更することができます。

import 'package:roggle/roggle.dart';

final logger = Roggle(
  printer: SinglePrettyPrinter(
    stackTraceLevel: Level.warning,
    stackTraceMethodCount: 2,
    stackTracePrefix: '>>>',
  ),
);

## 
void main() {
  logger.w(Exception('some exception'));
}

スタックトレース

FirebaseCrashlytics と連携する

FirebaseCrashlytics を導入すると、キャッチできなかった Exception や Error を捕捉して Firebase コンソールで確認ができます。

Firebaseコンソール
Firebase Crashlytics コンソール画面

さらに、FirebaseCrashlytics.instance.recordError() を呼ぶと、キャッチした Exception や Error を手動で FirebaseCrashlytics に送信することができます。これを利用して、例えばリリースビルドで Level.error 以上のログが発生した場合は FirebaseCrashlytics にエラーレポートを送信する、といったことができます。

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:roggle/roggle.dart';

final logger = kReleaseMode
    // リリースビルド時は FirebaseCrashlytics にエラーレポートを送信する
    ? Roggle.crashlytics(
        printer: CrashlyticsPrinter(
          errorLevel: Level.error, // error 以上のログはエラーレポートを送信する
          onError: (event) {
            FirebaseCrashlytics.instance.recordError(
              event.exception,
              event.stack,
              fatal: true, // true にするとエラーレポートを即時送信する
            );
          },
          onLog: (event) {
            // ここで記録したログは、firebase コンソールのログタブに表示される
            FirebaseCrashlytics.instance.log(event.message);
          },
        ),
      )
    // デバッグビルド時はコンソールに出力する
    : Roggle();

FirebaseCrashlytics の導入方法は以下を参考にしてください。

https://firebase.flutter.dev/docs/crashlytics/overview/

任意のエラーレベルのときに処理を停止する

デバッグビルド時に、予期せぬ動作をしたときに意図的に処理を止めたいときがあります。
例えば、ログレベルが Level.error 以上のログメッセージが表示された場合に AssertionError を投げて意図的に処理を止めるのは次のようにします。

import 'package:roggle/roggle.dart';

final logger = Roggle(
  output: AssertionOutput(),
);

class AssertionOutput extends ConsoleOutput {
  
  void output(OutputEvent event) {
    super.output(event);
    if (event.level.index >= Level.error.index) {
      throw AssertionError('Stopped by logger.');
    }
  }
}

void main() {
  logger.i('Hello roggle!');
  logger.e('ERROR!');
  logger.i('呼ばれない');
}

処理を止める

サンプル実装

ここまでに紹介した使い方の実装があるサンプルを用意しました。参考にしてください。

https://github.com/susatthi/flutter-sample-roggle

パッケージを作成する際に工夫したこと

CI の単体テストを充実させた

GitHub Actions を使って自動テストを実施しています。自動テストは OS (Ubuntu / macOS / Windows) と Flutter SDK Version (stable / beta / dev) のマトリックスのパターンで実施しています。このおかげで Windows でしか起きないバグを事前に潰すことが出来ました。

.github/workflows/test.yaml
jobs:
  test:
    name: Test
    runs-on: ${{ matrix.os }} # 3つのOSの組み合わせ
    timeout-minutes: 10
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        sdk: [stable, beta, dev]
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 1
      - name: Download dart sdk
        uses: dart-lang/setup-dart@v1
        with:
          sdk: ${{ matrix.sdk }} # 3つのSDKバージョンの組み合わせ
      - name: Install dependencies
        run: dart pub get
      - name: Format code
        run: dart format --set-exit-if-changed .
      - name: Static analyze project
        run: dart analyze --fatal-infos --fatal-warnings .
      - name: Run tests
        run: dart test

GitHub Actions
GitHub Actions の画面

Codecov を使ってカバレッジを測定した

すべての単体テストが完了したら Codecov に結果を送信してカバレッジを測定しています。これによりテストをしていないルートが無いことが視覚的に確認ができ一定の品質が担保されることが期待できます。

.github/workflows/test.yaml
  coverage:
    name: Coverage
    needs:
      - test
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 1
      - name: Download dart sdk
        uses: dart-lang/setup-dart@v1
        with:
          sdk: stable
      - name: Install dependencies
        run: dart pub get
      - name: Run tests
        run: dart test --coverage=coverage # カバレッジありでテストを実行する
      # Codecov に送信するファイルに変換する
      - name: Convert coverage to ICOV
        run: dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib
      # Codecov に結果を送信する
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v2
        with:
          file: coverage/lcov.info
          fail_ci_if_error: true
          flags: unittests
          verbose: true

Codecov のカバレッジ結果を README のバッジに表示もしています。

Codecov

Codecov のバッジを README に表示する方法は、Codecov にログインして、対象のリポジトリを選択後、Settings タブの Badge メニューを選択し、表示されされる URL をコピーして README に貼り付けるだけです。

開発時の単体テストの確認を楽にするスクリプトを作成した

次のスクリプトを作成して、開発時に繰り返し行う単体テストの実行を楽にしました。

bin/test
#!/bin/bash -e

# Format code
dart format --set-exit-if-changed .

# Static analyze project
dart analyze --fatal-infos --fatal-warnings .

# Run tests
dart test --coverage=coverage

# Convert coverage to ICOV
dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.packages --report-on=lib

# Output to html
genhtml coverage/lcov.info -o coverage/html

# Open html
open coverage/html/index.html

スクリプトは次のことを行い、途中でエラーがあれば止まります。テストを実装しているときは何度も実行してカバレッジが 100% になっているかをチェックしました。

  1. コード整形の実施
  2. 静的解析の実施
  3. 単体テストの実施
  4. カバレッジ測定用ファイルの作成
  5. カバレッジ測定結果HTMLファイルの生成
  6. カバレッジ測定結果HTMLファイルを開く

pub.dev への公開の自動化

今までは手動で公開していたのですが、楽をするために自動化しました。

tag を作成すると次のワークフローが発火して、pub.dev への公開と Release の作成をします。事前に pub.dev にログインするアカウントのクレデンシャル情報を GitHub Secrets に登録しておきます。

.github/workflows/publish.yaml
name: Publish

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    name: Publish
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 1
      - name: Download dart sdk
        uses: dart-lang/setup-dart@v1
        with:
          sdk: stable
      - name: Setup credentials
        run: |
          mkdir -p ~/.pub-cache
          cat <<EOF > ~/.pub-cache/credentials.json
          {
            "accessToken":"${{ secrets.PUB_CREDENTIALS_ACCESS_TOKEN }}",
            "refreshToken":"${{ secrets.PUB_CREDENTIALS_REFRESH_TOKEN }}",
            "idToken":"${{ secrets.PUB_CREDENTIALS_ID_TOKEN }}",
            "tokenEndpoint":"https://accounts.google.com/o/oauth2/token",
            "scopes": ["openid","https://www.googleapis.com/auth/userinfo.email"],
            "expiration": 1651458128502
          }
          EOF
      - name: Publish to pub.dev
        run: dart pub publish -f
      - name: Create release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: ${{ github.ref }}

最後に

自分が使いたいから作ったパッケージですが、もし気に入って使って頂けたらうれしいです。フィードバック も歓迎しています!

Flutter 大学という Flutter エンジニアに特化した学習コミュニティに所属しています。オンラインでわいわい議論したり、Flutter の最新情報をゲットしたりできます!ぜひ Flutter 界隈を盛り上げていきましょう!

https://flutteruniv.com?invite_id=9hsdZHg0qtaMIr6RPRulAaRJfA83

あわせて読みたい

https://zenn.dev/susatthi/articles/20220413-153500-flutter-logger

https://itome.team/blog/2019/12/flutter-advent-calendar-day24/

https://qiita.com/tokkun5552/items/2eb6793501c152dabf33

https://qiita.com/shtnkgm/items/cf68a736f81b958c71f9

Discussion