Bedrockのログをちょっとだけ見やすくする

BedrockにはCloudWatch Logsにログを記録する機能があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/a5b4d49b-722f-6d2f-9d45-2240b2a59117.png)

ただ、記録したログをそのまま確認しようとしても、目当てのログを見つけるには、ログをひとつひとつ開いて見ていく必要があります。

- 手がかりは日時だけ😭
    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/ccb09ee2-d3ed-9d73-ce18-71b37e48375b.png)

- ひとつひとつ開くと詳細は確認できるが大変😭
    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/e0c490b9-59ec-ad5e-d70e-888332061f4d.png)

**CloudWatch Logs Insights** をつかって、見やすくする方法をご紹介します。

:::note
最後に私が作ったクエリーを記載してますので、どうぞ最後まで御覧ください。

また、改良してより良いクエリーが作成できたら、ぜひコメントで教えてください！
:::

## 手順

CloudWatch Logs Insightsの画面に遷移するには、Bedrockのログイベントを表示した状態で、「アクション」メニューの「Logs Insightsで表示」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/db2206d8-d2aa-9a91-a446-f75711ac1f59.png)

赤枠の部分にクエリーを入力して、ログの検索や出力を加工できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/dfa0e8b6-ce85-cb88-a132-613e61ceb342.png)

初期状態では以下のクエリーが入力されています。

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields @timestamp, @message
```

![us-east-1.console.aws.amazon.com_cloudwatch_home_region=us-east-1 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/0b4c02ac-2433-9f58-d87d-a03fcf78fb92.png)

少しずつ見やすくしていきましょう。

Bedrockのログ出力は以下のようなJSON形式です。

```json
{
    "schemaType": "ModelInvocationLog",
    "schemaVersion": "1.0",
    "timestamp": "2025-01-10T14:08:32Z",
    "accountId": "***",
    "identity": {
    },
    "region": "us-east-1",
    "requestId": "fe5ac981-b9e8-43f1-aa30-21cca7682de4",
    "operation": "Converse",
    "modelId": "amazon.nova-micro-v1:0",
    "input": {
    },
    "output": {
    }
}
```

JSONのキーを、クエリーの`fields`に指定するとその項目が抽出されて、表示されます。

`requestId`、`operation`、`modelId`を表示させてみましょう

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields @timestamp, requestId, operation, modelId, @message
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/c0405870-13cc-ee64-0e7b-7d69ac6de8d7.png)

少し見やすくなりましたね！

### ネストされている項目を表示したい（文字列や数値）

Bedrockのログのoutput項目に、使用したトークンの情報があるので、これを表示させてみましょう。

```json:一部抜粋
"output": {
    "outputContentType": "application/json",
    "outputBodyJson": {
            "output": {
                "message": {
                    "role": "assistant",
                    "content": [
                        {
                            "text": ""
                        }
                    ]
                }
            },
        "stopReason": "end_turn",
        "metrics": {
            "latencyMs": 619
        },
        "usage": {
            "inputTokens": 913,
            "outputTokens": 72,
            "totalTokens": 985
        }
    },
    "outputTokenCount": 72
}
```

ネストされている項目は、ドット区切りで指定すると表示させられます。
また、項目名は`as`を使って指定が可能です。

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields @timestamp, 
    requestId, operation, modelId, 
    output.outputBodyJson.usage.inputTokens as inputTokens,
    output.outputBodyJson.usage.outputTokens,
    output.outputBodyJson.usage.totalTokens,
    @message
```

inputTokensだけ、`as`で別名を付けるとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/08f91095-e000-6014-1cca-974274dc9777.png)

### ネストされている項目を表示したい（複雑な構造）

先程の方法では、どうも、JSONの値に該当する部分が「文字列」や「数値」のときしか取れないようでして、オブジェクトや配列の場合はうまく取得できません。

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields @timestamp, 
    requestId, operation, modelId, 
    output.outputContentType as outputContentType,          # 文字列なので出力される
    output.outputBodyJson,                                  # ディクショナリーなので出力されない
    output.outputBodyJson.output.message.role,              # 文字列なので出力される
    output.outputBodyJson.output.message.content,           # 配列なので出力されない
    output.outputBodyJson.output.message.content[0].text,   # 配列が間に挟まってるので出力されない
    @message
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/c5bcdadd-26de-c522-274d-ff9caa0be02e.png)

このような場合は、一度`jsonParse`したうえで、`unnest`する必要があります。

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields @timestamp, 
    requestId, operation, modelId, 
    jsonParse(@message) as json_message,
    output.outputContentType as outputContentType,
    output.outputBodyJson.output.message.role,
    @message
 | unnest json_message.output.outputBodyJson into outputBodyJson
 | unnest json_message.output.outputBodyJson.output.message.content into output_content
 | unnest json_message.output.outputBodyJson.output.message.content[0].text into output_text
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/333c45ae-a95a-1090-145f-3eac78194ab7.png)

## フィルター、リミット

Bedrockの呼び出しには埋め込みモデルもありますが、おそらくログを確認したいことはあまりないのではないでしょうか？そのような場合は、フィルター条件を追加し、除外することができます。

方法としては以下の二通り実現可能です。

1. Operationが`Converse`か`ConverseStream`のものだけを表示する
    ```
    filter @logStream = 'aws/bedrock/modelinvocations'
     | fields @timestamp, 
        requestId, operation, modelId, 
        @message
     | filter operation in ["Converse", "ConverseStream"]
    ```

1. modelIdが`amazon.titan-embed-text-v2:0`のものを除外する

    ```
    filter @logStream = 'aws/bedrock/modelinvocations'
     | fields @timestamp, 
        requestId, operation, modelId, 
        @message
     | filter modelId != "amazon.titan-embed-text-v2:0"
    ```

他にも色々使えそうです。

ログの取得上限は`limit`で指定できます。CloudWatch Logs Insightsの料金はスキャンしたデータ量で決まるので、`limit`を上手に使ってスキャン量を減らすと良いですね。

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields @timestamp, 
    requestId, operation, modelId, 
    @message
 | filter modelId != "amazon.titan-embed-text-v2:0"
 | sort @timestamp desc | limit 1
```



## 最終的にたどり着いたクエリーはこちら

```
filter @logStream = 'aws/bedrock/modelinvocations'
 | fields jsonParse(@message) as json_message, 
    requestId, operation, modelId, 
    output.outputBodyJson.stopReason as stopReason, 
    output.outputBodyJson.metrics.latencyMs as latencyMs, 
    output.outputBodyJson.usage.inputTokens as inputTokens, 
    output.outputBodyJson.usage.outputTokens as outputTokens, 
    output.outputBodyJson.usage.totalTokens as totalTokens

 | unnest json_message.input.inputBodyJson.messages into inputMessages
 | unnest json_message.input.inputBodyJson.system[0].text into system
 | unnest json_message.input.inputBodyJson.inferenceConfig into inferenceConfig
 | unnest json_message.input.inputBodyJson.additionalModelRequestFields into additionalModelRequestFields
 
 | unnest json_message.output.outputBodyJson.output.message.content[0].text into outputMessage
 
 | filter operation = "Converse"

 | sort @timestamp desc | limit 10
 | display 
    @timestamp, requestId, operation, modelId, 
    latencyMs, 
    inputTokens, outputTokens, totalTokens, 
    system, 
    inputMessages, 
    inferenceConfig, 
    additionalModelRequestFields, 
    outputMessage, 
    stopReason
```

inputMessagesを分解して、「最後のメッセージ」を表示したかったのですが、どうしても「最後の」の指定方法がわからずでした。

左半分
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/c92cf676-2bc5-9305-b897-1974562e14d0.png)

右半分
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/689e42f3-4cb6-d8e4-69ff-3504306611b1.png)