CloudWatch LogsがUnicodeエスケープ地獄になっていたのでLambda Test Toolとかで解決を模索してみた

## はじめに
発端は、lambda関数で動いているERRORを確認するため、CloudWatch Logsでログを確認していた時のこと。
謎の文字列を発見。

```json
{
  "timestamp": "2023-06-04T05:58:22.951Z",
  "level": "Warning",
  "message": "\u30E1\u30C3\u30BB\u30FC\u30B8\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
}
```
なんだこれと思って調べたら、**JSONでシリアライズされているはずの日本語が、
Unicodeエスケープされていました。**

これはアプリケーション側の実装ミスと仮定して（その時はほぼ断定でしたが）、解決するために
色々模索していたので、犯人捜しと顛末に至る道程を記事にしてみました。

# 犯人捜しの始まり
## 疑惑1: JsonSerializerOptions の設定ミス？
最初に疑ったのは、冒頭でも記載しましたがアプリケーション側の設定ミスにあると考えました。
色々探していて、最初はここに目星をつけました。

```c#
jsonOptions = new()
{
    WriteIndented = false,
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)  
};
```
`UnicodeRanges.All`が何か悪さをしている、もしくはAWSでは効かないのかと考えました。

## 疑惑2: Parameter Storeの値の問題？
今回は`Parameter Store`で一部の値を設定していたので、
そちらも確認してみました。

確認してみると、
```json
{
  "Messages": [
    {"Id": 1, "Text": "処理が完了しました。"}  
  ]
}
```
うーん、正常な日本語ですね～。。。
しかもここでは特にUnicodeをエスケープする箇所でもないので、ここは違いそうだなって早々に感じました。


## 疑惑3: UnicodeRanges.All の罠？
となると、`UnicodeRanges.All`が非常に怪しいことになります。
検証するべく、僕が大好きな[.NET Fiddle](https://dotnetfiddle.net/)でテストしてみることにしました。
```C#
// 検証してみたテストコード
var testData = new { message = "パラメーター不正" };

var options1 = new JsonSerializerOptions 
{ 
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) 
};
var options2 = new JsonSerializerOptions 
{ 
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 
};

Console.WriteLine($"UnicodeRanges.All: {JsonSerializer.Serialize(testData, options1)}");
Console.WriteLine($"UnsafeRelaxedJsonEscaping: {JsonSerializer.Serialize(testData, options2)}");
```
JavaScriptEncoderライブラリの`UnsafeRelaxedJsonEscaping`も比較対象として、比べてみました。
これでおかしかったら、疑惑は確信に変わると思って実行しました。

**結果:**
```C#
UnicodeRanges.All: {"message":"パラメーター不正"}
UnsafeRelaxedJsonEscaping: {"message":"パラメーター不正"}
```
**「あれ？どっちも正常じゃん...」。**

# Local Test Tool で検証
今度はローカル環境（Lambda Test Tool）で実行してみました。
ログでみるために、テストメソッドを作成してみました。
``` C#
private SomeResponse CreateResponse(ILogger logger, object responseData, int statusCode = 200)
{
    // === Unicode エンコード検証用テスト ===
    var testText = "データ処理エラー";
    var testObj = new { directText = testText, source = "test" };
    
    logger.LogInformation($"現在のエンコーダー: {JsonSerializer.Serialize(testObj, _jsonOptions)}");
    
    var unsafeOptions = new JsonSerializerOptions 
    { 
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 
    };
    logger.LogInformation($"Unsafeエンコーダー: {JsonSerializer.Serialize(testObj, unsafeOptions)}");
    
    // 実際のレスポンス作成
    return new SomeResponse
    {
        Status = statusCode,
        Data = JsonSerializer.Serialize(responseData), // ← ここがポイント
        ContentType = "application/json"
    };
}
```

**結果:**
ログ出力: {"directText":"データ処理エラー","source":"test"} ✅
レスポンスデータ: "errorMessage":"\\u30C7\\u30FC\\u30BF\\u51E6\\u7406\\u30A8\\u30E9\\u30FC" ❌

ログは正常なのにAPIレスポンスが、エスケープされていることがわかりました！
この原因は、よく確認すると2箇所で異なるシリアライズをしていました（上記コメントに注目）。

ですので、シリアライズを行う個所を統一するように修正しました。
```C#
Body = JsonSerializer.Serialize(body, _jsonOptions)  // _jsonOptions を追加
```
これで治りましたね！やったね！

# しかし、さらなる謎

だがしかし、最初に見つけたJSONファイル内のCloudWatch Logsの問題は解決してない...。
改めて、CloudWatch Logsを確認してみました。

```json
{
  "@message": "{\u30E1\u30C3\u30BB\u30FC\u30B8...}",  // ← Unicodeエスケープ
  "message": "メッセージの読み込みに失敗しました"  // ← 正常な日本語
}
```
ここで重要なことに気づきます。
**「あれ？同じログなのに、@messageフィールド と JSON内のmessage で表示が違う？」**


# 結論: AWS の仕様らしい
調査した結果、AWS公式ドキュメントで以下のことが判明しました。
- **AWS Lambda .NET ランタイムは内部で System.Text.Json のデフォルト設定を使用**。<br><br>



- **CloudWatch Logsは自動的にJSONフィールドを解析し、@message（生ログ）と message（解析済み）の両方を提供**。<br><br>



- **GitHub Issue #1809でも同様の問題が報告済み**。<br>

なんと、AWSのもともとの仕組みのようだったようです:sweat_smile:。
しかもこれにより、@messageフィールドの生ログで調べるのではなく、解析済みフィールド（message）を使用すれば日本語で検索可能であることがわかりました。


# 終わりに
うーん、、、。（笑）
まさか、AWS上でそうなっているとは思いませんでした（笑）。
絶対アプリケーション側に問題があると思ったのですが、時にはそうでない時もあるのですね:worried:。

一応、調べるにあたって拝見したリンクもまとめておきます。
これを機に、少しでもAWSと仲良くしていきたいなと感じるばかりです！

# 参考・調査文献
[AWS Lambda .NET GitHub Issue #1809](https://github.com/aws/aws-lambda-dotnet/issues/1809)
[JSON Structured Logging for .NET Lambda Functions](https://aws.amazon.com/blogs/developer/structured-logging-for-net-lambda/)
[CloudWatch Logs Insights Query Syntax](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html)