📊

Bedrock の Model invocation log を DuckDB で解析する

に公開

Introduction

Bedrockのモデル実行時ログはS3やCloudWatch Logsに出力できますが、ログの解析には手間がかかります。今回はDuckDBを使ってログを加工・解析する方法を試してみます。
https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html

Dive into model invocatoin log

今回は CloudWatch Logs に出力されたログをS3にエクスポートし、そのデータを参照します。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/S3Export.html

DuckDBではAWS CLIのプロファイルをSECRETとして利用しています。
https://duckdb.org/docs/stable/extensions/httpfs/s3api.html#loading-a-secret-based-on-a-profile

CREATE OR REPLACE SECRET secret (
    TYPE s3,
    PROVIDER credential_chain,
    CHAIN config,
    REGION 'us-east-1',
    PROFILE '<profile-name>'
);

-- select * from duckdb_secrets();

次に、S3 へエクスポートした CloudWatch Logs を raw 列として raw_invocation_log_X テーブルにインポートします。
CloudWatch Logs のログは <timestamp> <log> のような形式であるため、整形が必要ですがまずはざっくりとテーブルを作ります。

なおクエリは DuckDB UI で実行しています。

$ duckdb -ui bedrock_model_invocation_log.duckdb
create table raw_invocaton_log
as select * 
from read_csv('s3://<path-to-log>/*.gz',
  columns = {'raw': 'varchar',}
);

次に、raw列を展開し、整形したテーブルを作成します。

create table if not exists raw_invocation_log_filtered
  as
with t1 as (
  /* split raw */
  select
    left(raw, 24)             as timestamp,
    right(raw, len(raw) - 25) as log
  from raw_invocaton_log
), final as (
  /* parse timestamp and filter only valid json */
  select
    -- https://duckdb.org/docs/stable/sql/functions/dateformat.html#format-specifiers
    strptime("timestamp", '%Y-%m-%dT%H:%M:%S.%gZ') as timestamp,
    json(log) as log
  from t1
  where
    json_valid(log) is true
)
select
  timestamp,
  log
from final;

Model invocation log のフォーマットにばらつきがあるため、パターンを分析します。DuckDBのjson_structure 関数が便利です。

https://duckdb.org/docs/stable/data/json/json_functions.html#json-scalar-functions

-- テーブル作成
create table log_schema
as
select distinct json_structure(log) as log_schema
from raw_invocation_log_filtered
;

-- ファイル出力
copy (
  select distinct json_structure(log) as log_schema
  from raw_invocation_log_filtered
) to 'log_schema.json'
;

log_schema テーブルや log_schema.json を確認し、共通部分を抽出して最終的なテーブルを作成します。

create table invocation_log
as
with t1 as (
  select
    "timestamp" as log_timestamp,
    json_extract_string(log, '$.schemaType')     as schemaType,
    json_extract_string(log, '$.schemaVersion')  as schemaVersion,
    json_extract_string(log, '$.timestamp')      as "timestamp",
    json_extract_string(log, '$.accountId')      as accountId,
    json_extract_string(log, '$.identity.arn')   as identity_arn,
    json_extract_string(log, '$.region')         as region,
    json_extract_string(log, '$.requestId')      as requestId,
    json_extract_string(log, '$.operation')      as operation,
    json_extract_string(log, '$.modelId')        as modelId,
    json_extract(log, '$.input')                 as input,
    json_extract(log, '$.output')                as output
  from raw_invocation_log_filtered
), final as (
  select 
    -- common
    log_timestamp,
    schemaType,
    schemaVersion,
    strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ') as timestamp,
    accountId,
    identity_arn,
    region,
    requestId,
    operation,
    modelId,
    json_extract_string(input, '$.inputContentType')                             as inputContentType,
    json_extract_string(input, '$.inputTokenCount')::ubigint                     as inputTokenCount,
    json_extract(input, '$.inputBodyJson')                                       as inputBodyJson,
    json_extract_string(output, '$.outputContentType')                           as outputContentType,
    json_extract_string(output, '$.outputTokenCount')::ubigint                   as outputTokenCount,
    json_extract(output, '$.outputBodyJson')                                     as outputBodyJson,
    -- Amazon Titan embed text v2
    json_extract_string(input, '$.inputBodyJson.inputText')                      as inputText,
    json_extract_string(input, '$.inputBodyJson.dimensions')::ubigint            as dimensions,
    json_extract(output, '$.outputBodyJson.embedding')                           as embedding,
    json_extract_string(output, '$.outputBodyJson.inputTextTokenCount')::ubigint as inputTextTokenCount,
    -- Anthropic Claude Converse
    json_extract(input, '$.inputBodyJson.messages')                              as input_messages,
    json_extract(input, '$.inputBodyJson.inferenceConfig')                       as inferenceConfig,
    json_extract(output, '$.outputBodyJson.output.message')                      as output_message,
    json_extract_string(output, '$.outputBodyJson.stopReason')                   as stopReason,
    json_extract(output, '$.outputBodyJson.metrics')                             as metrics,
    json_extract(output, '$.outputBodyJson.usage')                               as usage
  from t1
)

select * from final;

以下のような出力になります。 input/output については解析の用途に応じて JSON Function を駆使する必要がありそうです。(面倒ですね・・・)

D select * from invocation_log where modelId like '%anthropic.claude-3-5-sonnet%' limit 1;
┌─────────────────────┬────────────────────┬───────────────┬─────────────────────┬──────────────┬──────────────────────┬───────────┬──────────────────────┬───┬───────────┬─────────────────────┬──────────────────────┬─────────────────┬──────────────────────┬────────────┬───────────────────┬──────────────────────┐
│    log_timestamp    │     schemaType     │ schemaVersion │      timestamp      │  accountId   │     identity_arn     │  region   │      requestId       │ … │ embedding │ inputTextTokenCount │    input_messages    │ inferenceConfig │    output_message    │ stopReason │      metrics      │        usage         │
│      timestamp      │      varchar       │    varchar    │      timestamp      │   varchar    │       varchar        │  varchar  │       varchar        │   │   json    │       uint64        │         json         │      json       │         json         │  varchar   │       json        │         json         │
├─────────────────────┼────────────────────┼───────────────┼─────────────────────┼──────────────┼──────────────────────┼───────────┼──────────────────────┼───┼───────────┼─────────────────────┼──────────────────────┼─────────────────┼──────────────────────┼────────────┼───────────────────┼──────────────────────┤
│ 2024-08-19 09:09:08 │ ModelInvocationLog │ 1.0           │ 2024-08-19 09:09:08 │ 123456789012 │ arn:aws:sts::12345…  │ us-east-1 │ ebb0c3da-bf59-4dbb…  │ … │ NULL      │        NULL         │ [{"role":"user","c…  │ {"maxTokens":1} │ {"role":"assistant…  │ max_tokens │ {"latencyMs":316} │ {"inputTokens":8,"…  │
├─────────────────────┴────────────────────┴───────────────┴─────────────────────┴──────────────┴──────────────────────┴───────────┴──────────────────────┴───┴───────────┴─────────────────────┴──────────────────────┴─────────────────┴──────────────────────┴────────────┴───────────────────┴──────────────────────┤
│ 1 rows                                                                                                                                                                                                                                                                                          26 columns (16 shown) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Conlusion

S3 に出力したログの解析には Athena が一般的ですが、試行錯誤やアドホックな調査であればDuckDB でも十分対応できます。S3 から直接読み込めて出力も柔軟なため、今後も DuckDB を活用していきたいと思います。

Discussion