Windowsアプリケーションでのログ収集大作戦 - New Relic Logs APIで実現した15分以内の問題解決

この記事は[New Relic Advent Calendar 2024](https://qiita.com/advent-calendar/2024/newrelic)の12/22分の記事です。

## はじめに

私たちは、「写真×AI」を活用した保育支援サービス「[こみるこ](https://comiruco.snapsnap.jp/)」を開発しています。

こみるこは、先生が自動撮影カメラを置くだけで、子どもたちの自然な様子を写真撮影、園内での子どもの様子を保護者に届けることができるほか、撮影した写真からその日の子どもの様子を文字に起こし、毎日の連絡帳作成業務を支援するサービスです。

本記事では、このサービスで使用するWindows用アップローダーアプリケーションに、New Relic Logs APIを導入した事例をご紹介します。

## なぜログ収集が必要だったのか

### 現場での不安要素

特に気になっていたのは、先生方にとって普段あまり行わない「カメラとPCの接続作業」でした。

例えば：

- USBケーブルの抜き差し
- カメラの電源操作
- PCでの取り込み作業

これらの作業は、パソコンに慣れた人にとっては当たり前の操作かもしれません。

しかし、保育の現場では違います。毎日の保育業務に加えての作業となるため、戸惑いや予期せぬ操作が発生する可能性が高いと考えていました。

### 収集したかった情報

このような状況で、私たちが特に知りたかったのは

- カメラはきちんとPCに認識されているのか
- 写真の取り込みはスムーズにできているのか
- エラーが発生した場合、どんな操作の時に起きたのか
- 先生方がどのような手順で操作されているのか

といった内容です。

### 保育現場特有の課題

保育の現場には特有の課題もありました。

1. **時間の制約**
    - 子どもたちのお昼寝時間や帰った後など、限られた時間での作業
    - 問題が起きても子供達から離れられない
2. **サポートの難しさ**
    - 問題が起きてからでは状況の把握が困難
    - 電話での聞き取りだけでは正確な状況がわかりにくい
    - 現場訪問にも時間がかかる

## 解決策の検討

### New Relic Logs APIを選んだ理由

複数のログ収集サービスを検討する中で（CloudWatch Logsなども検討）、最終的にNew Relic Logs APIを選択しました。

1. **既存の監視基盤との統合**
    - 他のサービスの監視もNew Relicで実施済み
    - チームがNew Relicの操作に慣れている
    - 一つのダッシュボードで全体把握が可能
2. **運用効率の向上**
    - アラートの設定フローが確立されている
    - チーム間での情報共有が容易
    - 既存の通知設定を流用可能

## 技術的な実装

### New Relic Logs APIの導入手順

WindowsアップローダーはFlutterで開発していたため、DartでNew Relic Logs APIのクライアントを実装しました。

1. **アカウントとAPIキーの準備**
Log APIの[ドキュメント](https://docs.newrelic.com/jp/docs/logs/log-api/introduction-log-api/) を参考にNew Relicの設定を行います。
    - New Relicのアカウントを用意
    - License Key（APIキー）を取得
    - エンドポイントの確認
        - US: https://log-api.newrelic.com/log/v1
        - EU: https://log-api.eu.newrelic.com/log/v1
2. **依存関係の追加**
    
    デバイス情報を取得するための依存関係を追加します。
    
    ```yaml
    # pubspec.yaml
    dependencies:
      http: ^1.1.0
      device_info_plus: ^10.1.0
      package_info_plus: ^8.0.0
    ```
    
3. **Dartでの実装例**
    
    ```dart
    import 'dart:convert';
    
    import 'package:device_info_plus/device_info_plus.dart';
    import 'package:http/http.dart' as http;
    import 'package:package_info_plus/package_info_plus.dart';
    
    class NewRelicLogger {
      final String apiKey;
      final String endpoint;
      final http.Client _client;
    
      NewRelicLogger({
        required this.apiKey,
        required this.endpoint,
      }) : _client = http.Client();
    
      Future<void> sendLog({
        required String message,
        required String logLevel,
        required Map<String, String> attributes,
      }) async {
        final deviceInfo = await _getDeviceInfo();
    
        final logPayload = {
          'timestamp': DateTime.now().toUtc().toIso8601String(),
          'message': message,
          'attributes': {
            'app_name': 'comiruco-uploader',
            'log_level': logLevel,
            'device': {
              'app_version': deviceInfo['version'],
              'computer_name': deviceInfo['computerName'],
              'type': 'PC',
              'id': deviceInfo['deviceId'],
              'os': deviceInfo['os'],
              'os_version': deviceInfo['osVersion'],
            },
            'user': {'id': attributes['user_id'], 'name': attributes['user_name']},
            ...attributes,
          },
        };
    
        try {
          final response = await _client.post(
            Uri.parse(endpoint),
            headers: {
              'Api-Key': apiKey,
              'Content-Type': 'application/json',
            },
            body: jsonEncode(logPayload),
          );
    
          if (response.statusCode != 202) {
            throw Exception(
                'Failed to send log: ${response.statusCode} - ${response.body}');
          }
        } catch (e) {
          print('Error sending log to New Relic: $e');
          rethrow;
        }
      }
    
      // デバイス情報の取得
      Future<Map<String, String>> _getDeviceInfo() async {
        final appInfo = await PackageInfo.fromPlatform();
        final deviceInfo = DeviceInfoPlugin();
        final windowsInfo = await deviceInfo.windowsInfo;
    
        return {
          'version': '${appInfo.version}+${appInfo.buildNumber}',
          'computerName': windowsInfo.computerName,
          'deviceId': windowsInfo.deviceId,
          'os': 'Windows',
          'osVersion': windowsInfo.buildNumber.toString(),
        };
      }
    }
    ```
    

### エラーレベルの設計

エラーレベルは「先生の作業への影響度」を基準に設定しました。

1. **Error**
    - 先生の操作が継続できなくなる重大な問題
    - 例：カメラの認識失敗、写真取り込みの中断、アプリケーションの異常終了
2. **Warning**
    - 操作は継続可能だが、注意が必要な状態
    - 例：一時的な通信の遅延、非推奨の操作の実行
3. **Info**
    - 通常の操作ログ
    - 例：カメラの接続開始、写真の取り込み開始/完了

### アラート体制の構築

初期段階での慎重な運用を行うため、問題の即時検知を重視したアラート設定を行いました。
具体的には、Errorレベルのログが1つでも発生した場合に即座にSlackへ通知が飛ぶように設定しました。
これは、数園からの導入というスモールスタートだからこそ可能な、きめ細かな監視体制でした。サービスの安定性を確認しながら、一つ一つの問題に丁寧に対応することで、将来的な本格展開に向けた知見を積み重ねることが必要と考えました。

## 実際の解決事例

ある日の午後に発生したエラーへの対応事例をご紹介します。

**写真取り込み時のエラー対応**

14:15 - 【園】写真の取り込み中にエラーが発生
14:29 - 【開発】エラーを検知し、Slackで営業チームへ報告
14:32 - 【営業】園に電話で状況を確認
14:38 - 【営業】確認した内容を開発チームへフィードバック

【開発】追加調査により原因を特定

～対応方針を協議～

15:46 - 今後の対応方針と運用方法が決定！

この一連の流れを通じて、エラー発生から約1時間30分で恒久的な解決策の立案まで完了することができました。

## 現場の声

導入後、様々な立場の方から感想をいただきました。

**園の先生**
「困ったときにすぐに連絡がもらえるので、とても安心です。『先生、今こんなエラーが出てますよね？』って電話があったときは、『えっ！？ どうして分かったんですか！？』って驚きました（笑）」

**営業担当者**
「以前は園から『実は先週こんな問題があって...』と後から聞くことが多かったんです。先生方は『すぐに聞いてしまって申し訳ない』と遠慮されがちで、私たちと会ったときや電話したときの『ついで』に相談してくださることが多かったんです。今はすぐに気づけるので、こちらから『お困りでしたよね、申し訳ありません』と先に連絡できるようになりました。先生方の心理的な負担も減ったのではないかと思います」

**開発チーム**
「先生方は本当に忙しいので、問題が起きてもすぐに保育業務に戻らないといけません。その場で状況が分かるようになって、的確なサポートができるようになりました」

## まとめ

New Relic Logs APIの導入により、保育現場特有の課題に対応した監視体制を構築することができました。

特に、先生方の作業を止めないという明確な基準でのエラー検知と、迅速な問題解決のフローを確立できたことは、とても良かったです。

今後は、

- より詳細な利用パターンの分析
- 予防的な改善施策の実施
- サポート体制のさらなる向上

などを進め、更なるサービス向上を目指していきたいと思います。