Unityのログ出力無効化方法いろいろ

[グレンジ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/grenge) 21日目の記事を担当している高橋と申します。クライアントサイドのエンジニアをやっています。

Unityを使って作成したスマホアプリの製品版では、デバッグログの出力を抑制することで、パフォーマンスを向上させたり、不要な情報をユーザーに見せないようにすることができます。

特に、以下のような理由からログ出力を抑制することが重要です。

- **パフォーマンスの最適化**: 大量のログ出力は処理負荷を増大させ、フレームレートの低下を招く可能性があります
- **セキュリティとプライバシー**: 機密情報や内部処理に関する情報が誤って出力されると、製品の安全性に悪影響を及ぼす可能性があります
- **ユーザー体験の向上**: 不必要なログメッセージが製品版で表示されると、ユーザーに不快な印象を与えることがあります

本記事では、以下の4つの方法を具体的なサンプルコードと共に解説し、それぞれのメリット・デメリットについて考察します。

- ログ無効化フラグを使用する
- グローバルネームスペースでDebug.Logを再定義する
- Conditional属性を使用する
- ログハンドラを上書きする

（ちなみに私は最後の方法で実装しています）

## 方法1: ログ無効化フラグを使用する

Unityには、ログ出力を一括で有効/無効にするフラグとして `Debug.unityLogger.logEnabled` があります。このフラグをRuntimeInitializeOnLoadMethod属性を付与したメソッド内でfalseに設定することでログを無効化します。

### サンプルコード
```csharp
#if !DEBUG
public static class test
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Init()
    {
        Debug.unityLogger.logEnabled = false;
    }
}
#endif
```
```csharp
void Start() {
    Debug.Log("このログは製品版では出力されません。");
}
```

### メリット
- 簡単に実装可能で、すぐに効果を確認できる
- 製品版ではサードパーティ一製パッケージが出力するものも含めて一括でデバッグログを抑制できる

### デメリット
- 強力だが、その反面必要なログと不要なログを選別することができない
- 設定が一箇所で集中管理されておらず、他のスクリプトでログを有効に戻す可能性がある

## 方法2: グローバルスコープでDebug.Logを再定義する

Debug.Logをグローバルスコープで再定義することで、Debug.Log系メソッドの呼び出しを無効化します。

### サンプルコード
```csharp
#if RELEASE
public static class Debug {
	public static void Log(object message) { }
	public static void Log(object message, object context) { }

    // ・・・LogWarning, LogErrorなども同様に定義・・・
}
#endif
```
```csharp
void Start() {
    Debug.Log("このログは製品版では出力されません。");
}
```

### メリット
- 必要に応じてログの挙動を詳細にカスタマイズできる
- デバッグビルドと製品版で明確にログの出力制御を分けられる

### デメリット
- サードパーティ一製パッケージが出力するログやUnityEngine.Debug.Logを直接呼び出している箇所は抑制できない
- メンテナンスが煩雑になりやすい

## 方法3: Conditional属性を使用する

`System.Diagnostics.Conditional`属性を付与した自前のログ出力メソッドを定義してDebug.Logの代わりにそちらを呼び出すように実装することで、コンパイル時にログの出力コード自体を含めないようにできます。

### サンプルコード
```csharp
public static class LogManager {
    [System.Diagnostics.Conditional("DEBUG")]
    public static void Log(object message) {
        UnityEngine.Debug.Log(message);
    }

    // ・・・LogWarning, LogErrorなども同様に定義・・・
}

void Start() {
    LogManager.Log("このログはデバッグビルドでのみ出力されます。");
}
```

### メリット
- 不要なログ処理を完全に除去でき、実行時のオーバーヘッドが発生しない
- 簡潔なコードで実装可能

### デメリット
- Conditional属性を使用しない既存のログ処理には影響がない
- Conditional属性の適用範囲がコンパイル時に固定される
- サードパーティ一製パッケージが出力するログやDebug.Log系メソッドで出力しているログは抑制できない


## 方法4: ログハンドラを上書きする

カスタムのログハンドラを設定することで、ログ出力の挙動を細かく制御できます。下記サンプルでは、例外ログは全てのビルドで出力しますが、それ以外のログは製品版では無効化されます。またConditional属性も合わせて使用することで実行時のオーバーヘッドも抑制しています。

### サンプルコード
```csharp
public static class LogManager {
#if RELEASE
	private class ProductLogHandler : ILogHandler {
		private readonly ILogHandler _defaultLogHandler;

		public ProductLogHandler(ILogHandler defaultLogHandler) {
			_defaultLogHandler = defaultLogHandler;
		}

		public void LogFormat(LogType logType, Object context, string format, params object[] args) {
			if (logType != LogType.Exception) {
				// このログハンドラは例外ログ以外は出力しない
				return;
			}

			_defaultLogHandler.LogFormat(logType, context, format, args);
		}

		void ILogHandler.LogException(Exception exception, Object context) {
			_defaultLogHandler.LogException(exception, context);
		}
	}

	[RuntimeInitializeOnLoadMethod]
	public static void OnRuntimeMethodLoad() {
		// リリース版では、ログハンドラを上書きして例外ログ以外は出力しないようにする
		var logHandler = UnityEngine.Debug.unityLogger.logHandler;
		UnityEngine.Debug.unityLogger.logHandler = new ProductLogHandler(logHandler);
	}
#endif // RELEASE

	[Conditional("DEBUG")]
	public static void Log(object message) {
		_Log(LogType.Log, message, null);
	}

	[Conditional("DEBUG")]
	public static void Log(object message, Object context) {
		_Log(LogType.Log, message, context);
	}
	
	// ・・・LogWarning, LogErrorなども同様に定義・・・

	// 例外ログは無効化しない
	public static void LogException(Exception exception) {
		_Log(LogType.Exception, exception.Message, null);
	}
	public static void LogException(Exception exception, Object context) {
		_Log(LogType.Exception, exception.Message, context);
	}

	private static void _Log(LogType type, object message, Object context) {
		UnityEngine.Debug.unityLogger.Log(type, message, context);
	}
} // class LogManager
```
```csharp
void Start() {
    LogManager.Log("このログはデバッグビルドでのみ出力される");

	try {
		// 例外が発生する可能性のある処理
	}
	catch (Exception e) {
        // このログは全てのビルドで出力される
		LogManager.LogException(e);
	}
    
}
```

### メリット
- ログ出力の制御を柔軟に行える
- Unity標準のログ機能を活用しながら、製品版向けのカスタマイズが可能
- サードパーティ一製パッケージが出力するログやDebug.Log系メソッドで出力しているログも含めて制御することができる

### デメリット
- 実装がやや複雑である
- カスタムログハンドラが複数設定されると競合する可能性がある

## まとめ

| 方法 | メリット | デメリット |
|------|----------|-------------|
| ログ無効化フラグ | 実装が簡単 | 必要なログも抑制される |
| グローバルスコープ | 挙動を細かく制御可能 | 既存コードの対応が必要 |
| Conditional属性 | 実行時オーバーヘッドがゼロ | 対象が限定的 |
| ログハンドラ上書き | 柔軟性が高い | 実装が複雑 |

製品版でのログ出力を抑制する方法は用途やプロジェクトの規模に応じて選択するのが重要です。それぞれの方法を試して、自分のプロジェクトに最適なアプローチを見つけてみてください！