Flutterアプリ内の行動ログを保存して、デバッグしやすくする
はじめに
Firebase CrashlyticsやSentryが使えますが、ログを送ってもらった方が手順がわかって、解決につながることが多いです。また、開発時のQAテストでも活躍しています。
目次
- ログを出力する
- ログの内容をファイルに保存
- ログの内容をメールで送る
ログを出力する
必要なパッケージのインストール
ログを保存するために必要なパッケージをインストールします。
パッケージ名 | 役割 |
---|---|
logger | ログ出力 |
pretty_http_logger | httpパッケージ用のログ出力 |
rotation_log | ログ保存・圧縮 |
flutter_email_sender | ログをメール添付して送る |
flutter pub add logger pretty_http_logger rotation_log flutter_email_sender
ログの初期化
全体で適用できるように、logger.dartを作成し、ログの処理をまとめて定義します。
final logger = Logger(filter: ProductionFilter());
main.dartで、Flutter内のエラーを補足して、ログを通して出力するようにします。
Future<void> main() async {
FlutterError.onError = (details) {
logger.e(details.summary.toString(), details.exception, details.stack);
};
PlatformDispatcher.instance.onError = (error, stack) {
logger.e("", error, stack);
return true;
};
...
}
通信処理
センキャクでは、OpenAPI Generatorで書き出したパッケージを使って通信を行っています。内部の処理はhttpパッケージで作られていて、Clientを差し替えて処理を拡張できます。
通信内容をpretty_http_loggerを使ってログを出力できるようにします。
final apiClient = ApiClient();
final httpClient = HttpClientWithMiddleware.build(
middlewares: [
HttpLogger(logLevel: LogLevel.BODY),
],
);
apiClient.client = httpClient;
final memberApi = MemberApi(apiClient);
riverpod内のエラー
センキャクでは状態管理でriverpodを使っており、stateの更新時にエラーが起こった時にも、ログに残せるように処理を追加します。AsyncValue.guardを使ってエラーハンドリングをしますが、同じようにして、try〜catchで監視し、エラーが起こった場合にloggerに流すようにします。
import 'package:riverpod/riverpod.dart';
Future<AsyncValue<T>> guardWithLog<T>(Future<T> Function() future) async {
try {
return AsyncValue.data(await future());
} catch (err, stack) {
logger.e('', err, stack);
return AsyncValue.error(err, stack);
}
}
利用例
Future<void> fetch() async {
state = const AsyncLoading();
final repository = ref.read(dataRepositoryProvider);
state = await guardWithLog(() => repositoryfetch());
}
画面遷移
センキャクでは、routing処理にgo_routerを使っています。画面遷移の流れも、ログに入れておくと、エラーが起こった時の流れが追いやすくなります。GoRouterのobserversに、ログ保存用のNavigatorObserverを登録します。
class LoggerNavigatorObserver extends NavigatorObserver {
void _sendLog(Route<dynamic> route) {
logger.i("Screen: ${route.settings.name}");
}
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
if (route is PageRoute) {
_sendLog(route);
}
}
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (newRoute != null && newRoute is PageRoute) {
_sendLog(newRoute);
}
}
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
if (previousRoute != null && previousRoute is PageRoute) {
_sendLog(previousRoute);
}
}
}
ログの内容をファイルに保存
loggerは、Debug Consoleに出力してくれますが、ファイルの保存まではしてくれないので、追加でrotation_logのパッケージを利用します。このパッケージは、ログの内容をファイルに保存したり、まとめて圧縮する機能を持っています。
logger.dartにRotationLogPrinterクラスを定義し、loggerのprinterに設定して出力先を変更します。
import 'package:logger/logger.dart';
import 'package:rotation_log/rotation_log.dart';
final rotationLogger = RotationLogger(RotationLogTerm.day(3));
Logger logger = Logger(
filter: ProductionFilter(),
printer: RotationLogPrinter(),
);
class RotationLogPrinter extends LogPrinter {
final prettyPrinter = PrettyPrinter(printEmojis: false, colors: false);
List<String> log(LogEvent event) {
final formatLog = prettyPrinter.log(event);
rotationLogger.log(event.level, '\n${formatLog.join('\n')}');
return formatLog;
}
}
また、起動時にrotationLoggerの初期化処理を追加します。
Future<void> main() async {
await rotationLogger.init();
...
}
ログの内容をメールで送る
rotation_logで保存した内容をflutter_email_senderを使って、メールで添付して送信できるようにします。
import 'package:flutter_email_sender/flutter_email_sender.dart';
Future<void> _sendDebugReport() async {
final logPath = await rotationLogger.archiveLog();
final email = Email(
recipients: [ 'xxx@xxxxx' ],
body: 'メール本文',
subject: 'メールタイトル',
isHTML: false,
attachmentPaths: [logPath],
);
await FlutterEmailSender.send(email);
await rotationLogger.init();
}
おわりに
- 今回はFlutterアプリ内の行動ログの保存についてまとめてみました。みなさんもぜひ試してみてください。
- センキャクでは一緒にプロダクト開発をしてくれる仲間を絶賛募集しています。少しでもご興味ある方はこちらから。
Discussion