every Tech Blog

株式会社エブリーのTech Blogです。

Flutterアプリのログの出し方を整理した話

この記事はevery Tech Blog Advent Calendar 2024の21日目の記事です。

はじめに

こんにちは、リテールハブ開発部のネットスーパーチームでFlutterエンジニアをしている野口です。 今回は弊社で運用しているFlutterアプリのログの出し方を整理した際の話をします。

なぜログを整理するか

弊社で運用しているアプリの中で事業譲渡で引き継いだアプリがあります。 そのアプリは以下のように3つのパターンでログを出しており、統一されていませんでした。

統一されていないため以下のような問題があり、整理することにしました。

  • ログがどこで出てるのか把握しづらい
  • 開発者によって書き方が異なってしまう
  • エラーなどの問い合わせの際に、状況が把握できない

ログ整理の方針

ログを整理するにあたり、以下の理由でloggerに統一することにしました。

  • デフォルトでログレベルのサポートがある

loggerはデフォルトで以下のログレベルを提供しており、これを利用することでログの重要度に応じたログの出しわけができます。

ログレベルの種類

  • trace
  • debug
  • info
  • warning
  • error
  • wtf(What a Terrible Failure)

また、デフォルトでログレベルに合わせて色が変わるため視認性が良くなります。

実際にログを整理する

1. 不要なログ出力コードを整理

まず初めに、既存のログ出力をしている箇所で不要なログを削除しました。 printdeveloper.logの使用箇所はログ出力する意味がないものだったので削除しました。 loggerは例外処理のログ出力で使用していたので、後述のログ出力方法に置き換えるようにします。

2. ログの出力処理を一元化

次に、AppLogクラスを作成し、ログの出力処理を一元化しました。 これにより、統一された方法でログを出力できるようになり、誤った使い方を防ぐことができるようになります。

class AppLog {
  static final Logger _logger = Logger();

  static void debug({String message, StackTrace stackTrace}) {
    _log(message: message, level: Level.debug, stackTrace: stackTrace);
  }

  static void info({String message, StackTrace stackTrace}) {
    _log(message: message, level: Level.info, stackTrace: stackTrace);
  }

  static void error({String message, Exception exception, StackTrace stackTrace}) {
    _log(message: message, level: Level.error, exception: exception, stackTrace: stackTrace);
  }


  static void _log({String message, Level level, Exception exception, StackTrace stackTrace}) {
    if(!kReleaseMode){
      _logger.log(level, message, exception, stackTrace);
    }

    if(level.index >= Level.info.index && message != null) {
      FirebaseCrashlytics.instance.log(message);  
    }

    if (level.index >= Level.error.index && exception != null) {
      FirebaseCrashlytics.instance.recordFlutterError(FlutterErrorDetails(exception: exception, stack: stackTrace));
    }
  }
}

ログレベルはdebug、info、errorの3種類に分けており、_log関数の中でログレベルに応じて処理を分けています。

また、!kReleaseModeではリリースビルドでログを表示しないようにしており、本番環境では表示されないため、安心して使用できます。

debugログ

debugログは、主に開発時に変数の値やアプリの状態を確認したい場合に使用します。

if(!kReleaseMode){
  _logger.log(level, message, exception, stackTrace);
}

debugログの使用例:

void fetchData() {
  final data = {"key": "value"};
  AppLog.debug(message: "fetchData() called with data: $data");
}

infoログ

infoログは、アプリの動作状況やユーザーの操作イベントを記録したい場合に使用します。

また、Firebase Crashlyticsに情報を送信することで、本番環境での挙動を把握するのにも役立ちます。

if(!kReleaseMode){
  _logger.log(level, message, exception, stackTrace);
}

if(level.index >= Level.info.index && message != null) {
  FirebaseCrashlytics.instance.log(message);  
}

infoログの使用例:

void userLogin(String username) {
  AppLog.info(message: "User logged in with username: $username");
}

errorログ

errorログは、例外が発生した場合や補足したいエラーを記録する際に使用します。

また、Firebase Crashlyticsに例外情報を送信することで、問題を迅速にトラッキングできます。

if(!kReleaseMode){
  _logger.log(level, message, exception, stackTrace);
}

if (level.index >= Level.error.index && exception != null) {
  FirebaseCrashlytics.instance.recordFlutterError(FlutterErrorDetails(exception: exception, stack: stackTrace));
}

errorログの使用例:

void processPayment() {
  try {
    // エラーが発生する可能性のある処理
    throw Exception("Payment processing failed");
  } catch (e, stackTrace) {
    AppLog.error(
      message: "Payment error occurred",
      exception: e,
      stackTrace: stackTrace,
    );
  }
}

終わりに

今回はログの出し方を整理する話をしました。 ログの出し方を一元化したことで、ログの使い方が開発者に伝わりやすくなり、誤ったログの使い方を防ぐことができるようになりました。 さらに、以前は本番環境でユーザーがabd logcatなど使用すればログが見える状況でしたが、ログを本番環境で出ないようにし、誤ってユーザー情報がログ出力されるなどのリスクを防ぐことでセキュリティ面でも改善ができました。

ご覧いただきありがとうございました。