Debug.Log()の全文をファイル出力し、<message truncated>で省略された内容をすぐに確認できるようにする

[PONOS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ponos)の3日目の記事です。

昨日は私@e73ryoの[「SystemInfo.operatingSystemからバージョンを取得するときの注意（iPadOS15.1以降のiPad端末の挙動について）」](
https://qiita.com/e73ryo/items/ebb63cb90c71b4c31db7)でした。

# はじめに

先日、Unityプロジェクトへ外部SDKの導入をしていたところ、久しぶりにConsoleウインドウ上で`<message truncated>`に遭遇しました。

Unityには、**Consoleウインドウに出力しようとしているログの文章が一定の文字数（私の手元で確認した限りでは16,300文字）を超えると`<message truncated>`が末尾に挿入されてログが終了**してしまう仕様があります。
すごーーく長いエラーログが出力された場合に、その全文をConsoleウインドウ上ですぐに確認できないのは不便ですね。

一応、Consoleウインドウの`Open Editor Log`メニューから参照できるログには全文が記録されていますので、
そちらを開いて該当のログを検索することで、ログの全文を取得することが可能ではあります。
`<message truncated>`になってしまうのはあくまでConsoleウインドウで表示される上での仕様なので、ログ情報としては全文が残っています。
しかし、ここで確認できるログはプレーンテキストですべてのログが1ファイルに保存されているため、目的のログだけを抽出するには少し手間がかかります。

ということで、ログの全文取得を改善するディタ拡張を考えてみました。

確認環境は`Unity 2019.4.32f1`です。

# 実装の方針

では、どういった拡張機能を作りましょうか。

最初は**UnityエディタのConsoleウインドウ上でログの全文が表示される機能**にしようと考えていました。
やはり、Consoleウインドウ上ですぐにログの全文が読めるのが一番効率良いですからね。

しかし、

* Consoleウインドウに表示されているメッセージ要素
* `Debug.Log()`で出力されたログ

この２つを関連付ける方法がどうしても見つからず、断念することに…
（`UnityEditor.LogEntry`あたりを色々触ってみましたがうまくいきませんでした…機会があればリベンジしたいです）

今回は、Consoleウインドウと連携させることは諦め、**とにかく作業者がログの全文情報が取得しやすい環境を作ること**を目指しました。

機能要件は以下としました。

* プロジェクトフォルダ以下にログの全文を、1ログ出力1ファイルとして保存する（作業者がすぐに開くことができる場所に置く）
* ファイル名にはログ出力のタイムスタンプを含め、目的のログを探しやすくする
* そのままだとログのファイルが無尽蔵に増え続けてしまうため、最大保存件数を設け、超えた場合は古いログから削除していく

# ログの全文を取得するには

ログの全文は`Debug.Log()`へリクエストされたログの文字列から取得することができます。
`Debug.Log()`によるログの発行は`Application.logMessageReceived`イベントに登録することでハンドリングすることができます。
[Application-logMessageReceived - Unity スクリプトリファレンス](https://docs.unity3d.com/ja/2019.4/ScriptReference/Application-logMessageReceived.html)

以下のように使用します。

```cs
[InitializeOnLoad]
public static class LogReceiver
{
    static LogReceiver()
    {
        // ログの発行イベントを登録。
        Application.logMessageReceived += OnReceived;
    }

    static void OnReceived(string condition, string stackTrace, LogType type)
    {
        // condition … 発行されたログの全文。
        // stackTrace … 発行されたログのスタックトレース。
        // type … ログの種別。
    }
}
```

# 実装したもの

実装したコードはこちら。

```cs
using System;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Debug.Log()で出力されたログを、ログファイルとして書き出すクラス。
/// </summary>
[InitializeOnLoad]
public static class DebugLogWriter
{
    // ログを保存するフォルダ名。
    const string DirectoryName = "DebugLogs";

    // 最大件数。
    const int LogFileLimit = 100;

    static DebugLogWriter()
    {
        Application.logMessageReceived += OnReceived;
    }

    static void OnReceived(string condition, string stackTrace, LogType type)
    {
        if (!Directory.Exists(DirectoryName))
        {
            Directory.CreateDirectory(DirectoryName);
        }

        // ファイル名にタイムスタンプを含めてログ内容を書き出す。
        var now = DateTime.Now;
        var fileName = now.ToString("yyyy-MM-dd-HH-mm-ss-fffffff") + "_" + type.ToString() + ".txt";
        File.WriteAllText(DirectoryName + "/" + fileName, condition + "\n\n" + stackTrace);

        // 最大件数以上を超えるようであれば古いログから削除する。
        // ファイルのメタ情報でソートしたい場合はDirectoryInfo、FileInfoが便利。
        var directoryInfo = new DirectoryInfo(DirectoryName);
        var fileInfos = directoryInfo.GetFiles("*.txt");
        while (LogFileLimit < fileInfos.Length)
        {
            var oldestFileInfo = fileInfos.OrderBy(fileInfo => fileInfo.CreationTime).FirstOrDefault();
            File.Delete(oldestFileInfo.FullName);

            fileInfos = directoryInfo.GetFiles("*.txt");
        }
    }
}
```

実行すると、以下のように`Debug.Log()`の呼び出しごとにファイルが作成されていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/5c9a60b5-9816-9778-78c9-9fa0ea7549ba.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/e7a598f4-dd0f-5dd3-12d4-e37f052a299e.png)

では、試しに`<message truncated>`となるログを出力してみます。
今回は以下のようなコードを用いてテストしてみました。

```cs
static void LogLongMessage()
{
    var stringBuilder = new System.Text.StringBuilder();
    for(int i = 0; i < 10000; ++i)
    {
        stringBuilder.AppendFormat("{0:D5},", i);
    }
    stringBuilder.AppendLine("End"); // ログの最後には「End」が出力される。
    
    Debug.Log(stringBuilder.ToString());
}
```

Consoleウインドウ上だと以下のように途中で`<message truncated>`にされて、ログの最後の「End」が確認できませんが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/c22aab72-c974-aede-2560-63f46340d7b6.png)

書き出されたログのファイル上では全文が参照でき、最後の「End」まで確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7345/50afcdc2-7756-1b95-234a-807a80023f1a.png)

# まとめ

不具合の調査等で`Debug.Log()`の内容は重要なヒントになりますので、なるべく少ない手間で正確な情報が取得できるように環境を整備しておきたいですね。
（Unity公式でなにか機能を用意してほしい…）

明日は@FW14Bさんです！