🪦

【Unity】タイムスタンプ付きログファイル出力実装【供養】

に公開

背景

  • Debug.Log はエディタでしか見られず、ビルド版での不具合調査がつらい
  • タイムスタンプ付きで追えるテキストログがあれば助かる(特に非同期処理とか)

方針

  • エディタでもビルドでも同じ API(Log.Safe, Warn, Error)で呼べる
  • 呼び出し元ファイル名をカテゴリ化
  • ログをテキストファイルとして出力

出力先

  • Editor: ./Assets/Logs/
  • Build: Application.persistentDataPath/

動作イメージ

実装

using System;
using System.IO;
using System.Runtime.CompilerServices;
using UnityEngine;
    
/// <summary>
/// ロギングクラス<para></para>
/// - エディタ上のログに加え、ログファイルを残す<para></para>
///   エディタでのパス:Application.dataPath, "../Logs"<para></para>
///   ビルド版でのパス:Application.persistentDataPath<para></para>
/// - カテゴリ名には原則として呼び出し元のファイル名(拡張子なし)を使用<para></para>
/// (必要であればログ呼び出し時にカテゴリを明示的に指定し対処可能)
/// </summary>
internal static class Log
{
    private static StreamWriter fileWriter;
    private static readonly object locker = new();
    private static bool isInitialized = false;
    
    public enum LogLevel
    {
        Info,
        Warn,
        Error
    }
    
    public static void Initialize(string fileName = "SoundLog.txt")
    {
        if(isInitialized) return;
    
        string logDirectory;
#if UNITY_EDITOR
        logDirectory = Path.Combine(Application.dataPath, "../Logs");
#else
        logDirectory = Application.persistentDataPath;
#endif
        Directory.CreateDirectory(logDirectory);
        string logPath = Path.Combine(logDirectory, fileName);
    
        try
        {
            fileWriter = new(logPath, append: true);
            fileWriter.AutoFlush = true;
            isInitialized = true;
            Safe($"Initialize成功: {logPath}");
        }
        catch (Exception e)
        {
            Error($"Initialize失敗,{e.Message}");
        }
    }
    
    public static void Safe(string message, [CallerFilePath] string category = "")
        => Output(LogLevel.Info, category, message);        
    
    public static void Warn(string message, [CallerFilePath] string category = "")
        => Output(LogLevel.Warn, category, message);

    public static void Error(string message, [CallerFilePath] string category = "")
        => Output(LogLevel.Error, category, message);
        
    private static void Output(LogLevel level, string category, string message)
    {
        if (fileWriter == null) return;
        
        string catePath    = Path.GetFileNameWithoutExtension(category);
        string timestamp   = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
        string fullMessage = $"[{timestamp}] [{level}] [{catePath}] {message}";

#if UNITY_EDITOR
        switch (level)
        {
            case LogLevel.Info:
                Debug.Log(fullMessage);
                break;
        
            case LogLevel.Warn:
                Debug.LogWarning(fullMessage);
                break;
        
            case LogLevel.Error:
                Debug.LogError(fullMessage);
                break;
        }
#endif

            //競合防止(呼び出し側に非同期処理が多いため)
            lock (locker) fileWriter.WriteLine(fullMessage);
    }
    
    public static void Close()
    {
        fileWriter?.Flush();
        fileWriter?.Close();
        fileWriter = null;
    }
}

実装のポイント

  • CallerFilePath で 呼び出し元スクリプト名を自動取得
    • 呼び出し側でタグやカテゴリを意識せずとも、ファイル名がそのままカテゴリになる
    • バグ原因箇所を特定しやすい(1ファイルにクラスが大量に定義されているとそうでもないが)
  • lock で マルチスレッド同時書き込みをガード
    • WriteLineAsync を使うのもアリ
  • AutoFlush = true で確実にログ取得
    • アプリクラッシュ時でも強制フラッシュし、直近のログを残す

使い方

Log.Initialize(); //ゲーム開始時

//ログ出力
Log.Safe($"Preload clip: {clipName}");
Log.Warn($"Volume exceeds range: {vol}");
Log.Error($"Failed to load {path}");

Log.Close(); //ゲーム終了時

実際の開発で導入した例


タイムスタンプ, ログレベル, 呼び出しスクリプトファイル名, ログ内容 が記載されている

  • サウンドライブラリを開発中
  • UniTask を用いた非同期処理が多いので「これはバグの調査が大変そうだな」と思い、
    本クラスを実装した

供養って何

  • 実装後しばらくして、Unity Logging を知った
  • 「あれ、これ…下位互換?(よく言えばミニマム版)」
  • まあ、ロガー開発経験ができたってことで…

参考

Discussion