
Snowflake Cortex Code の LLM 応答に対するマスキングポリシーを AI Observability で検証してみた
かわばたです。
Snowflake の Cortex Code in Snowsight では、LLM への入出力が AI Observability としてイベントテーブルに記録されます。Cortex Code はテーブルデータにアクセスして SQL を実行できるため、PII(個人識別情報)を含むテーブルを分析させると、LLM の応答にも PII が含まれる可能性があります。
AI Observability のログを使って検証を行った内容を記します。
AI Observability とは
AI Observability は、Snowflake 上で動作する生成 AI アプリケーションの評価・トレーシング・比較を行うための機能です。AI Observability の一般的な評価対象アプリケーションは External Agent オブジェクトとして Snowflake 上に表現されます。一方、Cortex Code in Snowsight ではユーザーが External Agent を明示的に作成しなくても、Cortex Code のやり取りが SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS に span として記録されます。
本来の主要ユースケースは以下の 3 つです。
| ユースケース | 概要 |
|---|---|
| 評価(Evaluation) | LLM-as-a-judge で品質メトリクスを自動算出。Context Relevance(検索結果の関連性)、Groundedness(検索結果に基づいた回答か)、Answer Relevance(回答の関連性)、Correctness(正解との一致度)、Coherence(論理的一貫性)の 5 種類 |
| トレーシング(Tracing) | 入力→検索→LLM 推論→出力の各ステップのレイテンシ・トークン使用量・コストを記録 |
| 比較(Comparison) | 異なるモデル・プロンプト・パラメータの構成を並列評価し、最適な組み合わせを特定 |
今回注目するのはトレーシングの仕組みです。AI Observability はトレーシングの一環として、Cortex Code in Snowsight の会話履歴や各ステップの入力・出力などのトレース情報を SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS テーブルに記録します。適切な権限がある場合、これらの raw content(未リダクトの本文)を SQL で確認できます。この仕組みを利用して、マスキングポリシーが LLM の応答にも反映されるかを Observability ログで検証します。
機能概要(Cortex Code in Snowsight × Observability)
Cortex Code in Snowsight では、AI Observability のイベントが自動的に記録されます。記録される span の構造は以下のとおりです。
span 名(RECORD:name) |
粒度 | 記録内容 |
|---|---|---|
CodingAgentRun |
セッションレベル | 会話ターンごとに 1 span |
CodingAgent.Step-0 |
個別モデル呼び出し | ユーザープロンプト、モデルレスポンス、トークン数、ツール選択、レイテンシ、request_id |
これらのデータは SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS テーブルに保存されます。イベントテーブルの主要カラムは以下のとおりです。
| カラム | 内容 |
|---|---|
TIMESTAMP |
イベント発生時刻 |
RECORD_TYPE |
レコード種別(span の場合は 'SPAN') |
RECORD |
span 名などのメタデータ(RECORD:name::STRING で span 名を取得) |
RECORD_ATTRIBUTES |
モデル名、レイテンシ、ステータス、request_id、会話メッセージ(snow.ai.observability.agent.planning.messages)などの属性。SPAN レコードでは LLM の入出力データはこのカラムに格納される |
RESOURCE_ATTRIBUTES |
ユーザー名、ロール名などのセッション情報 |
VALUE |
LOG / METRIC レコードのペイロード。SPAN レコードでは NULL(OpenTelemetry 仕様) |
TRACE |
トレース ID(TRACE['trace_id'] で会話単位のグルーピングに使用) |
制限事項
- AI Observability 自体には、Cortex Code in Snowsight の応答を自動的に PII 検出・ブロックする専用機能はありません。本記事ではマスキングポリシーと Observability ログの組み合わせで検証しています
- マスキングポリシーの効果は Cortex Code が使用するロールに依存します。Cortex Code in Snowsight はセッション開始時にユーザーのデフォルトロールを使用するため、デフォルトロールに対してマスキングが適用される設計にする必要があります
READ UNREDACTED AI OBSERVABILITY EVENTS TABLE権限がない場合、システムテーブル関数 や関連経路で機密フィールドの raw content(未リダクトの本文)が閲覧できないことがあります- 2026年6月23日時点の情報です
前提条件
- Snowflake: AWS 東京リージョン、Enterprise エディション
- クロスリージョン推論: 本検証環境では有効化済み
事前準備
AI Observability の権限設定
AI Observability のイベントテーブルを閲覧するために、アプリケーションロールと権限を付与します。
USE ROLE ACCOUNTADMIN;
-- AI Observability の読み取りロールを付与
GRANT APPLICATION ROLE SNOWFLAKE.AI_OBSERVABILITY_READER
TO ROLE SYSADMIN;
-- Usage History など SNOWFLAKE.ACCOUNT_USAGE 参照用
GRANT IMPORTED PRIVILEGES ON DATABASE SNOWFLAKE
TO ROLE SYSADMIN;
-- 未リダクトの raw content を読む必要がある場合
GRANT READ UNREDACTED AI OBSERVABILITY EVENTS TABLE ON ACCOUNT
TO ROLE SYSADMIN;
-- イベント保持管理(DELETE / TRUNCATE)が必要な場合のみ
GRANT APPLICATION ROLE SNOWFLAKE.AI_OBSERVABILITY_ADMIN
TO ROLE SYSADMIN;
エラーなく Statement executed successfully が表示されれば問題ありません。

検証用テーブルの準備(PII を含むダミーデータ)
マスキングポリシーの比較検証のため、PII を含む検証用テーブルを作成します。
USE ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS PII_TEST_DB;
CREATE OR REPLACE TABLE PII_TEST_DB.PUBLIC.SAMPLE_CUSTOMERS AS
SELECT *
FROM VALUES
('山田太郎', 'taro.yamada@example.com', '090-1234-5678', '東京都渋谷区'),
('佐藤花子', 'hanako.sato@example.com', '080-9876-5432', '大阪府大阪市'),
('鈴木一郎', 'ichiro.suzuki@example.com', '070-1111-2222', '愛知県名古屋市')
AS t(name, email, phone, address);

試してみた
マスキングなしで Cortex Code にテーブル分析を依頼
まず、マスキングポリシーを適用していない状態で Cortex Code にテーブル分析を依頼します。
Snowsight で Cortex Code を開き、以下のようなプロンプトを送信しました。
PII_TEST_DB.PUBLIC.SAMPLE_CUSTOMERS テーブルの内容を出力してください
Cortex Code がテーブルの内容(メールアドレスや電話番号を含む行データ)を応答にそのまま含めて返しました。

Observability ログでマスキングなし時の LLM 応答を確認
Observability ログで、LLM の応答に PII が含まれているかを確認します。
USE ROLE SYSADMIN;
-- messages 内の PII を検出(正規表現 + 既知の氏名リスト)
SELECT
TIMESTAMP,
RESOURCE_ATTRIBUTES['snow.user.name']::STRING AS user_name,
RESOURCE_ATTRIBUTES['snow.session.role.primary.name']::STRING AS role_name,
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.request_id']::STRING AS request_id,
-- メールアドレス(正規表現)
REGEXP_SUBSTR(
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING,
'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'
) AS detected_email,
-- 電話番号(正規表現)
REGEXP_SUBSTR(
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING,
'0[7-9]0-[0-9]{4}-[0-9]{4}'
) AS detected_phone,
-- 住所(正規表現:都道府県パターン)
REGEXP_SUBSTR(
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING,
'(東京都|北海道|京都府|大阪府|.{2,3}県)[^ \\n\",.]{1,10}'
) AS detected_address,
-- 氏名(既知のPII値との一致)
CASE
WHEN RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING ILIKE '%山田太郎%' THEN '山田太郎'
WHEN RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING ILIKE '%佐藤花子%' THEN '佐藤花子'
WHEN RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING ILIKE '%鈴木一郎%' THEN '鈴木一郎'
END AS detected_name
FROM SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS
WHERE RECORD_TYPE = 'SPAN'
AND RECORD:name::STRING = 'CodingAgent.Step-0'
AND (
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
RLIKE '.*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}.*'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
RLIKE '.*0[7-9]0-[0-9]{4}-[0-9]{4}.*'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
ILIKE '%山田太郎%'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
ILIKE '%佐藤花子%'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
ILIKE '%鈴木一郎%'
)
ORDER BY TIMESTAMP DESC
LIMIT 10;
メールアドレス(taro.yamada@example.com 等)や電話番号(090-1234-5678 等)がそのまま含まれていることを確認できました。マスキングポリシーがない状態では、テーブル内の PII が LLM の応答を経由して Observability ログにもそのまま記録されます。
氏名に関しては正規表現で扱いづらい項目になるので、Coretex関数で検知する方法もあると考えています。
※今回はバイネームで検知する方法としています。

マスキングポリシーの作成と適用
次に、PII カラム(email、phone)にマスキングポリシーを適用します。マスク時の値を固定文字列 ***MASKED*** にすることで、Observability ログで「この固定値以外が含まれていれば PII 漏洩」と判定でき、検出精度が高くなります。
USE ROLE SYSADMIN;
-- メールアドレス用マスキングポリシー(固定値)
CREATE OR REPLACE MASKING POLICY PII_TEST_DB.PUBLIC.EMAIL_MASK
AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('ACCOUNTADMIN') THEN val
ELSE '***MASKED***'
END;
-- 電話番号用マスキングポリシー(固定値)
CREATE OR REPLACE MASKING POLICY PII_TEST_DB.PUBLIC.PHONE_MASK
AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('ACCOUNTADMIN') THEN val
ELSE '***MASKED***'
END;
-- ポリシーをカラムに適用
ALTER TABLE PII_TEST_DB.PUBLIC.SAMPLE_CUSTOMERS
MODIFY COLUMN EMAIL SET MASKING POLICY PII_TEST_DB.PUBLIC.EMAIL_MASK;
ALTER TABLE PII_TEST_DB.PUBLIC.SAMPLE_CUSTOMERS
MODIFY COLUMN PHONE SET MASKING POLICY PII_TEST_DB.PUBLIC.PHONE_MASK;
適用後、SYSADMIN ロールでテーブルを SELECT してマスクされていることを確認します。
SELECT * FROM PII_TEST_DB.PUBLIC.SAMPLE_CUSTOMERS;
email と phone がいずれも ***MASKED*** と表示されれば OK です。

マスキングありで Cortex Code にテーブル分析を依頼
新しい Cortex Code セッションを開き、マスキングポリシーを適用した状態で同じプロンプトを送信します。
PII_TEST_DB.PUBLIC.SAMPLE_CUSTOMERS テーブルの内容を出力してください
Cortex Code の応答には、元の email / phone ではなく固定マスク値(***MASKED***)が含まれることを確認しました。

Observability ログでマスキングあり時の LLM 応答を確認
同じ SQL で Observability ログを確認し、マスキング適用後の LLM 応答を確認します。
USE ROLE SYSADMIN;
SELECT
TIMESTAMP,
RESOURCE_ATTRIBUTES['snow.user.name']::STRING AS user_name,
RESOURCE_ATTRIBUTES['snow.session.role.primary.name']::STRING AS role_name,
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.request_id']::STRING AS request_id,
REGEXP_SUBSTR(
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING,
'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'
) AS detected_email,
REGEXP_SUBSTR(
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING,
'0[7-9]0-[0-9]{4}-[0-9]{4}'
) AS detected_phone,
REGEXP_SUBSTR(
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING,
'(東京都|北海道|京都府|大阪府|.{2,3}県)[^ \\n\",.]{1,10}'
) AS detected_address,
CASE
WHEN RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING ILIKE '%山田太郎%' THEN '山田太郎'
WHEN RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING ILIKE '%佐藤花子%' THEN '佐藤花子'
WHEN RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING ILIKE '%鈴木一郎%' THEN '鈴木一郎'
END AS detected_name
FROM SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS
WHERE RECORD_TYPE = 'SPAN'
AND RECORD:name::STRING = 'CodingAgent.Step-0'
AND (
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
RLIKE '.*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}.*'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
RLIKE '.*0[7-9]0-[0-9]{4}-[0-9]{4}.*'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
ILIKE '%山田太郎%'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
ILIKE '%佐藤花子%'
OR RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']::STRING
ILIKE '%鈴木一郎%'
)
ORDER BY TIMESTAMP DESC
LIMIT 10;
email / phone の元値は含まれず、固定マスク値 MASKED が記録されていることを確認できました。一方で、本検証では name / address にはマスキングポリシーを適用していないため、これらの値は応答や Observability ログに含まれます。

※赤枠以外の部分については、私が続けて問い合わせをしてしまった中で出てきたアウトプットなのでこの検証とは別ととらえていただければと思います。
マスキングポリシーなしの場合と比較すると、以下のとおりです。
| 条件 | Observability ログの LLM 応答 |
|---|---|
| マスキングポリシーなし | taro.yamada@example.com、090-1234-5678 などの PII がそのまま記録 |
| マスキングポリシーあり | ***MASKED*** が記録され、元の PII は含まれない |
Cortex Code の実行ロールにマスキングポリシーが適用されることで、対象カラムの PII が LLM 応答および Observability ログにもマスク済みの値で記録されることを確認できました。
Cortex Code 利用履歴との結合
利用者名は AI_OBSERVABILITY_EVENTS の RESOURCE_ATTRIBUTES['snow.user.name'] から直接取得できます。さらに CORTEX_CODE_SNOWSIGHT_USAGE_HISTORY と REQUEST_ID で結合することで、トークン数やクレジット消費などの利用履歴と紐づけられます。
-- Cortex Code 利用履歴と結合
WITH events AS (
SELECT
TIMESTAMP,
RECORD:name::STRING AS span_name,
RESOURCE_ATTRIBUTES['snow.user.name']::STRING AS user_name,
RESOURCE_ATTRIBUTES['snow.session.role.primary.name']::STRING AS role_name,
RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.request_id']::STRING AS request_id,
LEFT(TO_VARCHAR(RECORD_ATTRIBUTES['snow.ai.observability.agent.planning.messages']), 500) AS messages_preview
FROM SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS
WHERE RECORD_TYPE = 'SPAN'
AND RECORD:name::STRING = 'CodingAgent.Step-0'
)
SELECT
e.user_name,
e.role_name,
u.USAGE_TIME,
e.TIMESTAMP AS event_timestamp,
e.request_id,
u.TOKEN_CREDITS,
u.TOKENS,
e.messages_preview
FROM events e
LEFT JOIN SNOWFLAKE.ACCOUNT_USAGE.CORTEX_CODE_SNOWSIGHT_USAGE_HISTORY u
ON e.request_id = u.REQUEST_ID
ORDER BY e.TIMESTAMP DESC;
user_name とともにイベントが表示され、TOKEN_CREDITS でコスト情報が紐づいていれば OK です。

最後に
AI Observability は本来「LLM アプリケーションの評価・トレーシング・比較」のための機能ですが、トレーシングで LLM の入出力がイベントテーブルに記録される仕組みを活用して、マスキングポリシーの効果を検証しました。
結果として、マスキングポリシーが適用されたカラムについては、Cortex Code in Snowsight が SQL を実行して取得したデータもマスクされ、LLM の応答および Observability ログにもマスク済みの値が記録されることを確認できました。ただし、マスキングポリシー未適用のカラム、ユーザーがプロンプトに直接入力した PII、過去に記録済みの Observability ログなどは別途対策が必要です。
運用への展開としては、以下のようなアプローチが考えられます。
- PII を含むテーブルにはマスキングポリシーを適用し、LLM 経由の PII 漏洩を予防する
- Observability ログを定期的に監査して、マスキングが正しく適用されているかを確認する
RESOURCE_ATTRIBUTES['snow.user.name']やCORTEX_CODE_SNOWSIGHT_USAGE_HISTORYとの結合で、どのユーザーの利用でどのようなデータが LLM に渡されたかを追跡する
注意点として、マスキングポリシーはテーブルデータの保護には有効ですが、以下のケースは防げません。
- ユーザーがプロンプトに直接 PII を入力するケース
- マスキングポリシー未適用のカラム(今回の検証では name や address は未適用)
- マスキングポリシー適用前に記録済みの Observability ログ(後からポリシーを適用しても過去のログは自動的にマスクされません。必要に応じて
AI_OBSERVABILITY_ADMINロールによるログ削除や保持期間の管理を検討してください) - ACCOUNTADMIN など、ポリシー上アンマスクが許可されたロールで Cortex Code を使用するケース
AI Observability の本来のユースケースである評価メトリクス(Context Relevance、Groundedness 等)を使った LLM アプリケーションの品質評価も、別途試してみたいと思います。
この記事が何かの参考になれば幸いです!







