シンプルでカラフルで拡張しやすい FirebaseCrashlytics 連携可能な Dart 製ロガーパッケージを公開した話
はじめに
シンプルでカラフルで拡張しやすい FirebaseCrashlytics 連携可能な Dart 製ロガーパッケージを作成して pub.dev に公開しました。パッケージ名は roggle です(「ログル」と呼びます)。roggle は logger のアナグラムです🐷
このパッケージを作成するに至った経緯、パッケージの使い方、パッケージを作成する際に工夫したことをまとめました。
なぜパッケージを作成したのか?
少し前に次の記事を書きました。
記事を書く中でいろいろなロガーパッケージをお試しで使ってみたのですが、自分が欲しい機能をすべて満たすものがなかったので、自分で作ることにしました。はじめは公開するつもりは無かったのですが、自分が複数のプロジェクトで使う可能性もあるし、FirebaseCrashlytics 連携をしたロガーは他になくニーズがあるかもわからないので公開することにしました。
roggle パッケージのコンセプト
- ログメッセージに必要十分な以下の情報が含まれており、かつカスタマイズが可能
- 心を賑やかにしてくれる絵文字💡
- フィルタに便利なロガー名
- 視覚的にわかりやすいログレベルのラベル表示
- ロガーにはほぼ必須のタイムスタンプ
- 呼び出し場所(ファイル名、行、メソッド名)
- 見やすくするためにメッセージがカラフル
- 1メッセージ1行で出力する
- デバッグモード時、深刻なログレベルの場合に処理を停止できる
- リリースモード時、深刻なログレベルの場合に FirebaseCrashlytics にエラーレポートを送信できる
- logger パッケージの高い拡張性をそのまま継承
- logger パッケージから容易に乗り換えが可能
他のロガーパッケージとの違い
logger は自分が欲しいロガーパッケージに一番近かったのですが、ログが複数行に出力されて少しうるさい感じがしました。あと logger パッケージ内で try-catch
されてしまってログ出力内で例外を投げても意図的に処理を停止させることができませんでした。
simple_logger も好きなロガーです。2021 年に作ったアプリではこのロガーを使っていました。しかしカラフルではありませんでした。ログが埋もれてしまうので、どうしてもカラフルにしたかったです。あとログレベルの I/F がちょっと好みでは無かったです。
また、FirebaseCrashlytics 連携を意図したロガーは探してみた限り見つかりませんでした。デバッグビルド時はコンソールに出力して、リリースビルド時に深刻なエラーが発生したら FirebaseCrashlytics にエラーを送信したい、というニーズはあるかなぁと思ったのですが。。
roggle パッケージの設計方針
まったくの自作をするか、機能的に一番近い logger パッケージに依存させるか悩みましたが、大枠の設計が同じで、メッセージのカラフル化の機能を流用したかったので logger パッケージに依存させることにしました。
roggle パッケージの使い方
インストール
pubspec.yaml
を次のように変更して flutter pub get
を実行してインストールしてください。
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 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 の導入方法は以下を参考にしてください。
任意のエラーレベルのときに処理を停止する
デバッグビルド時に、予期せぬ動作をしたときに意図的に処理を止めたいときがあります。
例えば、ログレベルが 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('呼ばれない');
}
サンプル実装
ここまでに紹介した使い方の実装があるサンプルを用意しました。参考にしてください。
パッケージを作成する際に工夫したこと
CI の単体テストを充実させた
GitHub Actions を使って自動テストを実施しています。自動テストは OS (Ubuntu / macOS / Windows) と Flutter SDK Version (stable / beta / dev) のマトリックスのパターンで実施しています。このおかげで Windows でしか起きないバグを事前に潰すことが出来ました。
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 の画面
Codecov を使ってカバレッジを測定した
すべての単体テストが完了したら Codecov に結果を送信してカバレッジを測定しています。これによりテストをしていないルートが無いことが視覚的に確認ができ一定の品質が担保されることが期待できます。
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 のバッジを README に表示する方法は、Codecov にログインして、対象のリポジトリを選択後、Settings タブの Badge メニューを選択し、表示されされる URL をコピーして README に貼り付けるだけです。
開発時の単体テストの確認を楽にするスクリプトを作成した
次のスクリプトを作成して、開発時に繰り返し行う単体テストの実行を楽にしました。
#!/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% になっているかをチェックしました。
- コード整形の実施
- 静的解析の実施
- 単体テストの実施
- カバレッジ測定用ファイルの作成
- カバレッジ測定結果HTMLファイルの生成
- カバレッジ測定結果HTMLファイルを開く
pub.dev への公開の自動化
今までは手動で公開していたのですが、楽をするために自動化しました。
tag を作成すると次のワークフローが発火して、pub.dev への公開と Release の作成をします。事前に pub.dev にログインするアカウントのクレデンシャル情報を GitHub Secrets に登録しておきます。
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 界隈を盛り上げていきましょう!
あわせて読みたい
Discussion