📊
Bedrock の Model invocation log を DuckDB で解析する
Introduction
Bedrockのモデル実行時ログはS3やCloudWatch Logsに出力できますが、ログの解析には手間がかかります。今回はDuckDBを使ってログを加工・解析する方法を試してみます。
Dive into model invocatoin log
今回は CloudWatch Logs に出力されたログをS3にエクスポートし、そのデータを参照します。
DuckDBではAWS CLIのプロファイルをSECRETとして利用しています。
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
関数が便利です。
-- テーブル作成
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