📙

Flutter で本番環境の Log を収集して GCP で可視化する方法

2024/07/06に公開

こんにちは、Sally社 CTO の @aitaro です。普段はマーダーミステリーアプリ「ウズ」とマダミス制作ツール「ウズスタジオ」、マダミス情報サイト「マダミス.jp」を開発しています。
https://uzu-app.com

今回はその中でも「ウズ」を開発する上で、ウズのアプリケーションログを収集して、GCP の Cloud Logging に保管することで、バグ調査や安定性向上を実現している話をします。

なぜログ収集が重要か

モバイルアプリケーション開発において、本番環境でのログ収集は非常に重要な要素です。開発者がアプリケーションの動作を理解し、問題を迅速に特定・解決するために、ログは不可欠なツールとなります。

ログ収集の重要性は以下の点にあります:

  • バグの発見と修正
    本番環境で発生する予期せぬエラーや異常な動作が報告されたとき、その原因を特定することができます。これにより、開発者は手元で再現が難しいバグでもそれを修正し、アプリケーションの品質を向上させることができます。
  • パフォーマンス分析
    アプリケーションの各機能の実行時間やリソース使用量などのパフォーマンス指標を収集することで、ボトルネックを特定し、最適化の機会を見つけることができます。

Flutterアプリケーションにおいても、これらの利点を活かすためにログ収集は非常に重要です。しかし、開発環境でのデバッグログとは異なり、本番環境でのログ収集にはいくつかの配慮が必要です。プライバシーの保護、パフォーマンスへの影響の最小化、そしてスケーラビリティの確保などが求められます。

ログ収集アーキテクチャ

バックエンドサーバーとは異なり、モバイルアプリケーションではログが発生するのは顧客の端末上です。ログは特に設定しなければ、顧客の端末上に標準出力されるだけで、インターネットの向こう側にいる事業者側が確認することはできません。したがって、GCP, Datadog, Sentry 等どのサービスを使うのかに関わらず、必ず、ユーザー端末からインターネット越しにログを送信して、ログ保管DBに保管するというアーキテクチャになります。

ログ収集サービスの比較

本番環境でのログを効果的に収集・分析するためには、適切なログ保存先サービスを選択することが重要です。ここでは、主要なログ管理サービスを比較し、それぞれの特徴を見ていきます。

サービス名 特徴 Flutterとの相性
GCP Cloud Logging GCPの統合ログ管理サービス。高度な検索・分析機能、BigQueryとの連携、柔軟なアクセス制御を提供。 △ 専用のSDKはないが、googleapis のサポートが強力なので、実装にはそれほど困らない。
AWS CloudWatch AWSの統合ログ管理サービス。リアルタイムモニタリング、アラート機能、Lambda関数との連携が可能。 △ 非公式の aws_cloudwatch パッケージは存在する。
ELK Stack オープンソースの分散型ログ管理ソリューション。高度な検索・分析機能と高いカスタマイズ性が特徴。 × RESTful APIを利用して直接 Elasticsearch にログを送信することになるが、事例は少ない。
Splunk エンタープライズ向けの高度なログ管理プラットフォーム。強力な検索・分析機能と多様なデータソース対応が特徴。 × 調べた限り、SDKは存在せず、ドキュメントも乏しい。
Datadog クラウドスケールの監視サービス。ログ、メトリクス、トレースを統合的に管理。AIによる異常検知機能を提供。 datadog_flutter_plugin 公式プラグインが存在する。
Sentry リアルタイムのエラートラッキングプラットフォーム。詳細なスタックトレースとコンテキスト情報に加えてログもイベントという形で収集可能。 sentry_flutter 公式プラグインが存在する。

弊チームでは、GCPでサーバが構築されていることからログ収集の一貫性、統合の容易さ、追加費用がかからない点より、GCP Cloud Logging を選定しましたが、それぞれ一長一短ですので、システム要件に合わせて選ぶのが良いと思います。

GCP と Flutter の実装

GCP Cloud Logging を使用してFlutterアプリケーションからログを収集する方法を具体的に解説します。

Step 1: GCPプロジェクトのセットアップ

  1. GCPコンソールにログインし、新しいプロジェクトを作成します。
  2. Cloud Logging APIを有効にします。
  3. サービスアカウントを作成し、必要な権限(Logging > Logs Writer)を付与します。
    このキーは Flutter にバンドルさせるので、最悪の場合リバースエンジニアリングされても大丈夫なように、必要以上の権限を付与しないことが重要です。
  4. サービスアカウントのキーファイル(JSON)をダウンロードします。

Step 2: Flutterアプリでの設定と実装

  1. googleapis をプロジェクトに追加します。
  2. GCP に通信するためにの log.dart を構成し、Log クラスを実装します。
  3. ログを出力したい任意のアプリケーション上の実装箇所でLog.info("メッセージを送信")を追加します。

構成された log.dart の抜粋は以下のようになります。

log.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:googleapis/logging/v2.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:intl/intl.dart';
import 'package:http/http.dart';

final createClient = clientViaServiceAccount(
  ServiceAccountCredentials.fromJson('''
    {
      "private_key_id": "your-private-key-id",
      "private_key": "your-private-key",
      "client_email": "your-client-email",
      "client_id": "your-client-id",
      "type": "service_account"
    }
    '''),
  [LoggingApi.loggingWriteScope],
);


class Log {
  Log._(this.level, this.msg);

  final String level;
  final dynamic msg;

  static void info(dynamic msg) {
    Log._('INFO', msg).send();
  }

  static void warn(dynamic msg) {
    Log._('WARNING', msg).send();
  }

  static void error(dynamic msg) {
    Log._('ERROR', msg).send();
  }

  Future<void> send() async {
    var log = {
      'timestamp': DateFormat('yyyy-MM-ddTHH:mm:ss').format(DateTime.now().toUtc()),
      'level': level,
      'message': msg.toString(),
    };

    final account = await createClient();
    final loggingApi = LoggingApi(account);
    final resource = MonitoredResource()
      ..type = 'global'
      ..labels = {'project_id': 'your-project-id'};

    final logEntry = LogEntry()
      ..logName = 'projects/your-project-id/logs/flutter-app'
      ..jsonPayload = log
      ..resource = resource
      ..severity = level;

    final request = WriteLogEntriesRequest()..entries = [logEntry];
    await loggingApi.entries.write(request);
  }
}

このファイルで行なっている処理は以下の流れです。

  1. 初めに googleapis の Client の初期化処理。
  2. Cloud Logging の仕様に沿った json での構造化ロギングの定義。例えば、timestamp,message,jsonPayloadあたりは自由に決めているのではなく、Cloud Logging の予約語です。
  3. googleapis を用いで GCP Cloud Logging へのログの書き込み。

注意する点としては、上記のコードは疑似コードになるので、例えば client の初期化をログ送信の度にしないようにしたり、ログの量が多い場合は一定clientサイドで貯めてからバッチで送信する必要があります。また、現状のコードでは Log.info の関数は string 型しか受けつけていませんが、構造化ロギングの強みを最大限生かすのであれば、Map型でLog送信できるようにするのがおすすめです。例えば userId をjsonで付与することで、そのユーザーに発生した出来事を追うことができます。

ログの可視化で得られた効果

GCP Cloud Loggingを使用してFlutterアプリケーションのログを収集することで、様々なバグ調査やパフォーマンス改善に活かすことができますが、実際に運用して以下のような効果がありました。

a) ユーザーごとのエラー調査の効率化

ユーザーからお問い合わせのあった事象について、Flutterログを蓄積することによって、そのユーザーが経験している問題を詳細に調査することができます。例えば以下のログを設定することで、Cloud Logging で userId を絞り込むことで、具体的に何が発生していたのかが明確になります。

Log.error('エラー発生: ユーザー ${user.name} がプロフィール更新に失敗', userId: user.id, err: e);

b) プレイ中のバッテリー残量の推移の可視化

マーダーミステリーアプリではマーダーミステリーのプレイ中、ユーザーのプレイ中のバッテリー消費を監視することが重要です。以下のようなログを定期的に記録することで、バッテリー残量の推移を追跡できます:


Log.info('バッテリー状態: レベル ${battery.level}%, プレイ時間 ${playTime.inMinutes}分', level: battery.level, minutes: playTime.inMinutes);

このデータを活用することで、プレイ中のバッテリー消費スピードを可視化することができました。ウズでは一時期プレイ中のバッテリー消費量が大きくなっている時期があったのですが、パフォーマンスの問題はそれが解決したかどうかを評価するのが難しいです。バッテリー消費速度の形で定量的に評価することで、パフォーマンスの問題に取り組むことができました。

まとめ

本記事では、Flutterアプリケーションで本番環境のログを収集し、GCP Cloud Loggingを使用して分析する方法について解説しました。Flutterにおけるログ収集の仕組みを確立することで、開発チームはデータに基づいた意思決定を行い、アプリケーションの品質とユーザー満足度を継続的に向上させることができました。

UZU テックブログ

Discussion