亚马逊AWS官方博客
基于 Amazon Bedrock ApplyGuardrail API 的内容安全过滤:可观测性看板与运营实践
摘要:当使用 Amazon Bedrock Guardrails 的 ApplyGuardrail API 作为独立内容安全护栏时,Model Invocation Logging 并不适用,需要自建监控体系来追踪拦截详情。本文介绍两种方案——基于 Amazon CloudWatch 的实时监控看板和基于 Amazon S3 + Amazon Athena + Amazon QuickSight 的离线分析看板,并提供一个开箱即用的 Java 封装库,让开发者一行代码完成内容过滤与日志记录。
目录
一、背景介绍
越来越多的企业在生产环境中使用 Amazon Bedrock Guardrails 来保护 AI 应用的输入和输出安全。Guardrails 提供了两种使用方式:
- 与模型推理集成:通过 Converse / InvokeModel API 的
guardrailConfig参数,在调用模型时自动应用护栏 - 独立调用:通过
ApplyGuardrail API单独对文本进行安全评估,不经过模型推理
第二种方式适用于以下场景:
- 在调用模型之前预过滤用户输入
- 对 Bedrock 托管模型和自建模型、第三方 API 的输出做安全检查
- 在应用网关层做统一的内容安全拦截
然而,ApplyGuardrail API 有一个重要的限制:Model Invocation Logging 默认支持 API 元数据保存 CloudTrail 中,但不支持该 API 的完整用户请求 Prompt 和 API 响应的日志记录。这意味着虽然 CloudWatch Metrics 会自动采集拦截率、延迟等聚合指标,但无法在原生日志中看到”具体哪条内容因为什么原因被拦截”。
本文将介绍如何通过自建日志管道解决这个问题,并提供两种完整的监控看板方案。
二、方案概览
2.1 核心思路
ApplyGuardrail API 的响应默认包含完整的 assessments 信息(无需额外设置 trace),我们只需要在应用侧捕获这些信息并写入日志存储即可。
[图 1-1:方案 A 的整体监控架构] |
[图 1-2:方案 B 的整体监控架构] |
2.2 两种方案对比
| 维度 | 方案 A:CloudWatch Dashboard | 方案 B:S3 + Athena + QuickSight |
| 数据延迟 | 秒级(实时) | 分钟级(SPICE 定时刷新) |
| 查询方式 | Logs Insights(类 SQL) | 标准 SQL(Athena) |
| 可视化 | CloudWatch Dashboard | QuickSight Dashboard |
| 适用场景 | 日常运维监控、告警响应 | 深度分析、跨月趋势、合规审计 |
| 存储成本 | 较高(CloudWatch Logs 定价) | 较低(S3 存储) |
| 长期保留 | 需配置保留策略 | 天然支持,成本低 |
建议:两种方案按需启用。方案 A 用于实时运维监控和告警,方案 B 用于长期存储和深度分析。
2.3 核心 AWS 服务
| 组件 | AWS 服务 | 用途 |
| 内容过滤 | Amazon Bedrock Guardrails | ApplyGuardrail API 进行文本安全评估 |
| 实时指标 | Amazon CloudWatch Metrics | 自动采集拦截率、延迟、按策略分布 |
| 实时日志 | Amazon CloudWatch Logs | 存储每次调用的完整评估详情 |
| 实时看板 | Amazon CloudWatch Dashboard | Metrics 图表 + Logs Insights 查询 |
| 长期存储 | Amazon S3 | Hive 分区格式存储日志 JSON |
| SQL 查询 | Amazon Athena | 对 S3 日志进行 SQL 分析 |
| 可视化 | Amazon QuickSight | 构建交互式分析看板 |
三、ApplyGuardrail API 响应结构
在深入方案之前,先了解 API 返回的数据结构。与 InvokeModel 不同,ApplyGuardrail API 默认返回完整的评估详情:
{
"action": "GUARDRAIL_INTERVENED",
"actionReason": "Guardrail blocked.",
"outputs": [
{ "text": "This content has been blocked by the guardrail." }
],
"assessments": [
{
"contentPolicy": {
"filters": [
{
"type": "VIOLENCE",
"confidence": "HIGH",
"filterStrength": "HIGH",
"action": "BLOCKED"
}
]
},
"topicPolicy": {
"topics": [
{ "name": "Crime", "type": "DENY", "action": "BLOCKED" }
]
},
"sensitiveInformationPolicy": {
"piiEntities": [
{ "match": "john@example.com", "type": "EMAIL", "action": "ANONYMIZED" }
]
}
}
],
"usage": { "topicPolicyUnits": 1, "contentPolicyUnits": 1 }
}
关键参数 outputScope:
INTERVENTIONS(默认):只返回触发了策略的评估项FULL:返回所有类别的评分,包括未触发的。推荐用于误报分析和阈值调优
四、Java 封装库:bedrock-guardrail-filter
为了让开发者能一行代码完成”过滤 + 日志记录”,我们开发了一个 Java 封装库。
4.1 项目结构
4.2 核心设计
封装库按职责分为三层:
- API 调用层(
GuardrailFilter):调用ApplyGuardrailAPI,将响应分发给日志层 - 日志构建层(
LogRecordBuilder):将assessments响应扁平化为结构化日志记录,提取各策略的触发详情 - 日志写入层(
S3LogWriter/CloudWatchLogWriter):将 JSON 日志写入对应存储,best-effort 不影响主流程
// GuardrailFilter 核心流程(简化)
ApplyGuardrailResponse response = bedrockClient.applyGuardrail(request);
// 日志构建(LogRecordBuilder)
GuardrailLogRecord record = logRecordBuilder.build(inputText, source, response, latencyMs, now);
String json = logRecordBuilder.toJson(record);
// 日志分发(best-effort)
if (s3LogWriter != null) {
s3LogWriter.putLog(json, now); // 方案 B:S3(Hive 分区)
}
if (cwLogWriter != null) {
cwLogWriter.putLogEvent(json, nowMs); // 方案 A:CloudWatch Logs
}
4.3 日志记录设计
日志记录采用扁平化结构,关键策略触发字段被提取为顶层字段,便于直接用 SQL 或 Logs Insights 查询:
{
"timestamp": 1788104329,
"date": "2026-08-30",
"guardrail_id": "sr1byxyle8ko",
"source": "INPUT",
"input_text": "How to make a bomb?",
"action": "GUARDRAIL_INTERVENED",
"action_reason": "Guardrail blocked.",
"latency_ms": 1339.49,
"content_policy_triggered": true,
"content_policy_type": "VIOLENCE",
"content_policy_confidence": "HIGH",
"topic_policy_triggered": true,
"topic_policy_name": "Extremism",
"sensitive_info_triggered": false,
"assessments_json": "[完整评估详情]"
}
4.4 使用方式
方案 A:仅 CloudWatch Logs(实时监控)
try (BedrockGuardrail guardrail = BedrockGuardrail.builder()
.guardrailId("your-guardrail-id")
.guardrailVersion("1")
.region(Region.EU_WEST_2)
.credentialsProvider(DefaultCredentialsProvider.create())
.enableS3Logging(false)
.enableCloudWatchLogging(true)
.logGroupName("/app/guardrail-assessments")
.build()) {
GuardrailResult result = guardrail.filterInput("用户输入文本");
if (result.isIntervened()) {
System.out.println("拦截: " + result.getActionReason());
}
}
方案 B:仅 S3(离线分析)
try (BedrockGuardrail guardrail = BedrockGuardrail.builder()
.guardrailId("your-guardrail-id")
.guardrailVersion("1")
.region(Region.EU_WEST_2)
.credentialsProvider(DefaultCredentialsProvider.create())
.s3Bucket("your-guardrail-logs-bucket")
.enableCloudWatchLogging(false)
.build()) {
GuardrailResult result = guardrail.filterInput("用户输入文本");
}
双写模式(同时启用方案 A 和 B)
try (BedrockGuardrail guardrail = BedrockGuardrail.builder()
.guardrailId("your-guardrail-id")
.guardrailVersion("1")
.region(Region.EU_WEST_2)
.credentialsProvider(DefaultCredentialsProvider.create())
.s3Bucket("your-guardrail-logs-bucket")
.enableS3Logging(true)
.enableCloudWatchLogging(true)
.logGroupName("/app/guardrail-assessments")
.build()) {
GuardrailResult result = guardrail.filterInput("用户输入文本");
}
4.5 IAM 权限要求
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:ApplyGuardrail",
"Resource": "arn:aws:bedrock:*:*:guardrail/*"
},
{
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::your-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
五、方案 A:CloudWatch Dashboard(实时监控)
5.1 数据来源
CloudWatch Dashboard 结合两个数据源:
- CloudWatch Metrics(
AWS/Bedrock/Guardrails命名空间):Bedrock 自动采集,包含调用量、拦截量、延迟等聚合指标 - CloudWatch Logs Insights:查询我们写入的结构化日志,获取具体的拦截内容和策略详情
5.2 第一步:确认 Metrics 数据
ApplyGuardrail API 调用后,Bedrock 会自动向 CloudWatch 推送以下指标(无需额外配置):
| 指标 | 维度 | 说明 |
Invocations |
Operation=ApplyGuardrail |
总调用次数 |
InvocationsIntervened |
Operation=ApplyGuardrail |
被拦截次数 |
InvocationLatency |
Operation=ApplyGuardrail |
评估延迟 (ms) |
InvocationsIntervened |
GuardrailPolicyType=ContentPolicy |
按策略类型拆分 |
Invocations |
GuardrailContentSource=Input |
按 Input/Output 拆分 |
ℹ️ 注意:
Metrics 查询必须指定维度(如 Operation=ApplyGuardrail),不带维度的查询将返回空数据。
5.3 第二步:配置 CloudWatch Logs 写入
使用 Java 封装库启用 CloudWatch Logs 写入后,每次 ApplyGuardrail 调用会自动写入一条 JSON 日志到指定的 Log Group。Log Group 和 Log Stream 会在首次写入时自动创建。
5.4 第三步:创建 Dashboard
Dashboard 使用 JSON 定义,通过 AWS CLI 部署:
aws cloudwatch put-dashboard \
--dashboard-name "Bedrock-Guardrails-Monitor" \
--dashboard-body file://dashboard.json \
--region eu-west-2
Dashboard 包含以下面板:
5.4.1 Metrics 面板(上半部分)
| 面板 | 类型 | 数据来源 |
| 调用量 & 拦截量 | 时序图 | Invocations + InvocationsIntervened |
| 拦截率 % | 时序图 | Metric Math: (Intervened/Invocations)*100 |
| 延迟 Avg/P95/P99 | 时序图 | InvocationLatency 多统计量 |
| 按策略类型分布 | 堆叠面积图 | InvocationsIntervened 按 GuardrailPolicyType 拆分 |
| Input vs Output | 柱状图 | Invocations 按 GuardrailContentSource 拆分 |
[图 2:CloudWatch Dashboard – Metrics 面板] |
5.4.2 Logs Insights 面板(下半部分)
| 面板 | 查询说明 |
| 每小时拦截率统计 | stats count(*) as total, sum(action='GUARDRAIL_INTERVENED') as blocked by bin(1h) |
| 拦截策略类型分布 | 按 content_policy_type / topic_policy_name 分组的饼图 |
| 最近被拦截内容详情 | 展示原始输入文本、触发策略、confidence 等完整信息 |
| 延迟趋势 | stats avg(latency_ms), pct(latency_ms, 95), pct(latency_ms, 99) by bin(1h) |
[图 3:CloudWatch Dashboard – Logs Insights 面板] |
5.5 Dashboard 配置样例
dashboard.json
{
"widgets": [
{
"height": 2,
"width": 24,
"y": 0,
"x": 0,
"type": "text",
"properties": {
"markdown": "# Bedrock Guardrails 监控看板\nCloudWatch Metrics(自动采集) + 应用日志(CloudWatch Logs Insights)"
}
},
{
"height": 6,
"width": 8,
"y": 2,
"x": 0,
"type": "metric",
"properties": {
"metrics": [
[ "AWS/Bedrock/Guardrails", "Invocations", "Operation", "ApplyGuardrail", { "stat": "Sum", "label": "Total Invocations" } ],
[ ".", "InvocationsIntervened", "Operation", "ApplyGuardrail", { "stat": "Sum", "label": "Intervened" } ]
],
"view": "timeSeries",
"stacked": false,
"region": "eu-west-2",
"title": "调用量 & 拦截量",
"period": 300
}
},
{
"height": 6,
"width": 8,
"y": 2,
"x": 8,
"type": "metric",
"properties": {
"metrics": [
[ { "expression": "(m2/m1)*100", "label": "Block Rate %", "id": "e1" } ],
[ "AWS/Bedrock/Guardrails", "Invocations", "Operation", "ApplyGuardrail", { "stat": "Sum", "id": "m1", "visible": false } ],
[ ".", "InvocationsIntervened", "Operation", "ApplyGuardrail", { "stat": "Sum", "id": "m2", "visible": false } ]
],
"view": "timeSeries",
"stacked": false,
"region": "eu-west-2",
"title": "拦截率 %",
"period": 300,
"yAxis": { "left": { "min": 0, "max": 100 } }
}
},
{
"height": 6,
"width": 8,
"y": 2,
"x": 16,
"type": "metric",
"properties": {
"metrics": [
[ "AWS/Bedrock/Guardrails", "InvocationLatency", "Operation", "ApplyGuardrail", { "stat": "Average", "label": "Avg" } ],
[ "...", { "stat": "p95", "label": "P95" } ],
[ "...", { "stat": "p99", "label": "P99" } ]
],
"view": "timeSeries",
"stacked": false,
"region": "eu-west-2",
"title": "延迟 (ms)",
"period": 300
}
},
{
"height": 6,
"width": 8,
"y": 8,
"x": 0,
"type": "metric",
"properties": {
"metrics": [
[ "AWS/Bedrock/Guardrails", "InvocationsIntervened", "GuardrailPolicyType", "ContentPolicy", "Operation", "ApplyGuardrail", { "stat": "Sum", "label": "Content" } ],
[ "...", "TopicPolicy", ".", ".", { "stat": "Sum", "label": "Topic" } ],
[ "...", "WordPolicy", ".", ".", { "stat": "Sum", "label": "Word" } ],
[ "...", "SensitiveInformationPolicy", ".", ".", { "stat": "Sum", "label": "SensitiveInfo" } ]
],
"view": "timeSeries",
"stacked": true,
"region": "eu-west-2",
"title": "按策略类型拦截分布",
"period": 300
}
},
{
"height": 6,
"width": 8,
"y": 8,
"x": 8,
"type": "metric",
"properties": {
"metrics": [
[ "AWS/Bedrock/Guardrails", "Invocations", "GuardrailContentSource", "Input", "Operation", "ApplyGuardrail", { "stat": "Sum", "label": "Input" } ],
[ "...", "Output", ".", ".", { "stat": "Sum", "label": "Output" } ]
],
"view": "bar",
"stacked": false,
"region": "eu-west-2",
"title": "Input vs Output 调用量",
"period": 300
}
},
{
"height": 2,
"width": 24,
"y": 14,
"x": 0,
"type": "text",
"properties": {
"markdown": "## 应用日志分析(Logs Insights)\n 来源:Java SDK 写入 CloudWatch Logs `/app/guardrail-assessments`"
}
},
{
"height": 6,
"width": 12,
"y": 16,
"x": 0,
"type": "log",
"properties": {
"query": "SOURCE '/app/guardrail-assessments'\n| stats count(*) as total, sum(action='GUARDRAIL_INTERVENED') as blocked by bin(1h) as time_bucket\n| sort time_bucket desc",
"region": "eu-west-2",
"stacked": false,
"view": "table",
"title": "每小时拦截率统计"
}
},
{
"height": 6,
"width": 12,
"y": 16,
"x": 12,
"type": "log",
"properties": {
"query": "SOURCE '/app/guardrail-assessments'\n| filter action = 'GUARDRAIL_INTERVENED'\n| stats count(*) as count by coalesce(content_policy_type, topic_policy_name, sensitive_info_type, 'OTHER') as policy_type\n| sort count desc",
"region": "eu-west-2",
"stacked": false,
"view": "pie",
"title": "拦截策略类型分布"
}
},
{
"height": 8,
"width": 24,
"y": 22,
"x": 0,
"type": "log",
"properties": {
"query": "SOURCE '/app/guardrail-assessments'\n| filter action = 'GUARDRAIL_INTERVENED'\n| fields @timestamp, input_text, action_reason, content_policy_type, content_policy_confidence, topic_policy_name, sensitive_info_type, word_policy_match, latency_ms\n| sort @timestamp desc\n| limit 30",
"region": "eu-west-2",
"stacked": false,
"view": "table",
"title": "最近被拦截的内容详情"
}
},
{
"height": 6,
"width": 24,
"y": 30,
"x": 0,
"type": "log",
"properties": {
"query": "SOURCE '/app/guardrail-assessments'\n| stats avg(latency_ms) as avg_latency, pct(latency_ms, 95) as p95_latency, pct(latency_ms, 99) as p99_latency by bin(1h) as time_bucket\n| sort time_bucket desc",
"region": "eu-west-2",
"stacked": false,
"view": "timeSeries",
"title": "应用侧延迟趋势 (Avg / P95 / P99)"
}
}
]
}
5.6 常用 Logs Insights 查询
5.6.1 查看被拦截请求的详情
SOURCE '/app/guardrail-assessments'
| filter action = 'GUARDRAIL_INTERVENED'
| fields @timestamp, input_text, content_policy_type, topic_policy_name,
sensitive_info_type, content_policy_confidence, latency_ms
| sort @timestamp desc
| limit 50
5.6.2 按策略类型统计拦截分布
SOURCE '/app/guardrail-assessments'
| filter action = 'GUARDRAIL_INTERVENED'
| stats count(*) as count
by coalesce(content_policy_type, topic_policy_name, sensitive_info_type, 'OTHER') as policy_type
| sort count desc
5.7 告警配置
基于 CloudWatch Metrics 设置告警:
| 告警 | 条件 | 用途 |
| 拦截率突增 | InvocationsIntervened / Invocations > 10% 持续 5 分钟 |
配置变更或攻击检测 |
| 绝对拦截数突增 | InvocationsIntervened > 100 每 5 分钟 |
量级异常 |
| 延迟劣化 | InvocationLatency P95 > 500ms |
Guardrail 性能问题 |
六、方案 B:S3 + Athena + QuickSight(离线分析)
6.1 S3 日志存储
日志以 Hive 分区格式写入 S3,Athena 可自动识别分区:
6.2 第一步:Athena 建表
使用 Partition Projection 自动管理分区,无需手动执行 MSCK REPAIR TABLE:
CREATE EXTERNAL TABLE guardrail_assessments (
timestamp bigint,
date string,
hour string,
guardrail_id string,
guardrail_version string,
source string,
input_text string,
action string,
action_reason string,
output_text string,
latency_ms double,
assessments_json string,
content_policy_triggered boolean,
content_policy_type string,
content_policy_confidence string,
content_policy_strength string,
topic_policy_triggered boolean,
topic_policy_name string,
word_policy_triggered boolean,
word_policy_match string,
sensitive_info_triggered boolean,
sensitive_info_type string,
contextual_grounding_triggered boolean,
contextual_grounding_score double,
usage map<string, int>
)
PARTITIONED BY (year string, month string, day string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://your-bucket/guardrail-logs/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.year.type' = 'enum',
'projection.year.values' = '2024,2025,2026,2027,2028,2029,2030',
'projection.month.type' = 'integer',
'projection.month.range' = '1,12',
'projection.month.digits' = '2',
'projection.day.type' = 'integer',
'projection.day.range' = '1,31',
'projection.day.digits' = '2',
'storage.location.template' =
's3://your-bucket/guardrail-logs/year=${year}/month=${month}/day=${day}/'
);
6.3 第二步:常用查询
6.3.1 每日拦截率趋势
SELECT date,
COUNT(*) AS total,
SUM(CASE WHEN action = 'GUARDRAIL_INTERVENED' THEN 1 ELSE 0 END) AS blocked,
ROUND(SUM(CASE WHEN action = 'GUARDRAIL_INTERVENED' THEN 1 ELSE 0 END)
* 100.0 / COUNT(*), 2) AS block_rate
FROM guardrail_assessments
WHERE year = '2026' AND month = '08'
GROUP BY date ORDER BY date;
6.3.2 误报候选(LOW confidence 的拦截)
SELECT date, input_text, content_policy_type, content_policy_confidence
FROM guardrail_assessments
WHERE action = 'GUARDRAIL_INTERVENED'
AND content_policy_confidence = 'LOW'
ORDER BY timestamp DESC LIMIT 100;
[图 4:Athena 查询结果示例] |
6.4 第三步:QuickSight Dashboard
- 在 QuickSight 中添加 Athena 数据源
- 创建 SPICE 数据集,使用 Custom SQL 并 CAST(date AS DATE) 得到日期类型列
- 配置增量刷新计划(每 15 分钟)
推荐的 Dashboard 面板:
| 面板 | 图表类型 | 数据 |
| Action 分布 | 饼图 | NONE vs GUARDRAIL_INTERVENED |
| 每日拦截率趋势 | 折线图 | date × block_rate |
| 按策略类型分布 | 柱状图 | content_policy_type × count |
| Topic Policy 触发排名 | 水平柱状图 | topic_policy_name × count |
| PII 类型分布 | 饼图 | sensitive_info_type × count |
| 平均延迟 | KPI | avg(latency_ms) |
| 拦截详情表 | 表格 | 完整字段 |
[图 5:QuickSight Dashboard – Overview] |
[图 6:QuickSight Dashboard – Block Details] |
七、误报分析实践
无论使用哪种方案,以下最佳实践有助于降低误报率:
- 初期使用 Detect Mode — 将 Guardrail action 配置为 NONE,只记录不拦截,积累 48 小时基线数据
- 使用
outputScope=FULL— 捕获所有类别的评分,包括”接近阈值但未触发”的内容 - 定期审查 LOW confidence 拦截 — 通过 Logs Insights 或 Athena 查询
content_policy_confidence = 'LOW‘ 的拦截记录 - 按类别独立调优 — 各内容类别(Hate/Insults/Sexual/Violence/Misconduct)可独立设置过滤强度
- 按业务场景拆分 Guardrail — 不同业务场景使用不同配置,避免一刀切
八、效果与收益
| 指标 | 无监控 | 方案 A(CloudWatch) | 方案 B(S3 + Athena) |
| 拦截详情可见性 | ❌ 仅聚合指标 | ✅ 实时可见 | ✅ SQL 可查 |
| 数据延迟 | N/A | 秒级 | 15 分钟(SPICE 刷新) |
| 误报分析 | ❌ 无法做 | ✅ Logs Insights | ✅ Athena SQL |
| 长期存储成本 | N/A | 较高 | 极低(S3) |
| 告警能力 | ✅ 仅 Metrics | ✅ Metrics + 日志 | ✅ QuickSight Threshold |
| 跨月趋势分析 | ❌ | 受限(Logs 保留期) | ✅ 无限 |
九、附录:完整源代码
以下是 bedrock-guardrail-filter 封装库的完整源代码。项目基于 Maven 构建,依赖 AWS SDK for Java v2(2.34.0)。你可以直接复制这些文件到对应目录,执行 mvn clean package 编译使用。
9.1 BedrockGuardrail.java(入口类)
package com.aws.guardrail.filter;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockruntime.model.GuardrailContentSource;
/**
* Simplified entry point for Bedrock Guardrail filtering with automatic logging.
*
* <p>Supports two logging destinations (can be enabled simultaneously):</p>
* <ul>
* <li><b>S3</b> (default on) - Long-term storage, Athena/QuickSight analysis</li>
* <li><b>CloudWatch Logs</b> (default off) - Real-time monitoring, Dashboard, Logs Insights</li>
* </ul>
*
* <p>Example with both S3 and CloudWatch Logs:</p>
* <pre>
* try (BedrockGuardrail guardrail = BedrockGuardrail.builder()
* .guardrailId("abc123")
* .guardrailVersion("1")
* .region(Region.EU_WEST_2)
* .credentialsProvider(ProfileCredentialsProvider.create("guardrails"))
* .s3Bucket("my-guardrail-logs")
* .enableCloudWatchLogging(true)
* .logGroupName("/app/guardrail-assessments")
* .build()) {
*
* GuardrailResult result = guardrail.filterInput("user text");
* }
* </pre>
*/
public class BedrockGuardrail implements AutoCloseable {
private final GuardrailFilter filter;
private BedrockGuardrail(GuardrailFilter filter) {
this.filter = filter;
}
public GuardrailResult filterInput(String text) {
return filter.filterInput(text);
}
public GuardrailResult filterOutput(String text) {
return filter.filterOutput(text);
}
public GuardrailResult filter(String text, GuardrailContentSource source) {
return filter.filter(text, source);
}
@Override
public void close() {
filter.close();
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String guardrailId;
private String guardrailVersion;
private Region region;
private AwsCredentialsProvider credentialsProvider;
// S3
private String s3Bucket;
private String s3Prefix = "guardrail-logs";
private boolean enableS3Logging = true;
// CloudWatch Logs
private String logGroupName = "/app/guardrail-assessments";
private String logStreamName = "guardrail-assessments";
private boolean enableCloudWatchLogging = false;
private GuardrailConfig.OutputScope outputScope = GuardrailConfig.OutputScope.FULL;
/** Required. Guardrail identifier (ID or ARN). */
public Builder guardrailId(String guardrailId) {
this.guardrailId = guardrailId;
return this;
}
/** Required. Guardrail version number. */
public Builder guardrailVersion(String guardrailVersion) {
this.guardrailVersion = guardrailVersion;
return this;
}
/** Required. AWS Region. */
public Builder region(Region region) {
this.region = region;
return this;
}
/** Required. AWS credentials provider. */
public Builder credentialsProvider(AwsCredentialsProvider credentialsProvider) {
this.credentialsProvider = credentialsProvider;
return this;
}
// ---- S3 logging ----
/** S3 bucket for assessment logs. Required when S3 logging is enabled. */
public Builder s3Bucket(String s3Bucket) {
this.s3Bucket = s3Bucket;
return this;
}
/** S3 key prefix. Default: "guardrail-logs". */
public Builder s3Prefix(String s3Prefix) {
this.s3Prefix = s3Prefix;
return this;
}
/** Enable/disable S3 logging. Default: true. */
public Builder enableS3Logging(boolean enableS3Logging) {
this.enableS3Logging = enableS3Logging;
return this;
}
/** @deprecated Use {@link #enableS3Logging(boolean)} instead. */
@Deprecated
public Builder enableLogging(boolean enableLogging) {
this.enableS3Logging = enableLogging;
return this;
}
// ---- CloudWatch Logs ----
/**
* Enable CloudWatch Logs writing. Default: false.
* When enabled, logs are written to CloudWatch for real-time Dashboard and Logs Insights.
*/
public Builder enableCloudWatchLogging(boolean enableCloudWatchLogging) {
this.enableCloudWatchLogging = enableCloudWatchLogging;
return this;
}
/**
* CloudWatch Logs group name. Default: "/app/guardrail-assessments".
* Created automatically if it doesn't exist.
*/
public Builder logGroupName(String logGroupName) {
this.logGroupName = logGroupName;
return this;
}
/**
* CloudWatch Logs stream name. Default: "guardrail-assessments".
* Created automatically if it doesn't exist.
*/
public Builder logStreamName(String logStreamName) {
this.logStreamName = logStreamName;
return this;
}
// ---- Other ----
/** Assessment output scope. Default: FULL. */
public Builder outputScope(GuardrailConfig.OutputScope outputScope) {
this.outputScope = outputScope;
return this;
}
public BedrockGuardrail build() {
GuardrailConfig config = GuardrailConfig.builder()
.guardrailId(guardrailId)
.guardrailVersion(guardrailVersion)
.region(region)
.credentialsProvider(credentialsProvider)
.s3Bucket(s3Bucket)
.s3Prefix(s3Prefix)
.enableS3Logging(enableS3Logging)
.logGroupName(logGroupName)
.logStreamName(logStreamName)
.enableCloudWatchLogging(enableCloudWatchLogging)
.outputScope(outputScope)
.build();
GuardrailFilter filter = GuardrailFilter.create(config);
return new BedrockGuardrail(filter);
}
}
}
9.2 GuardrailConfig.java(配置类)
package com.aws.guardrail.filter;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
/**
* Configuration for Bedrock Guardrail Filter.
*
* <p>Supports two logging destinations (can be enabled simultaneously):</p>
* <ul>
* <li><b>S3</b> - For long-term storage and Athena/QuickSight analysis</li>
* <li><b>CloudWatch Logs</b> - For real-time monitoring and Logs Insights queries</li>
* </ul>
*/
public class GuardrailConfig {
private final String guardrailId;
private final String guardrailVersion;
private final Region region;
private final AwsCredentialsProvider credentialsProvider;
// S3 logging
private final String s3Bucket;
private final String s3Prefix;
private final boolean enableS3Logging;
// CloudWatch Logs
private final String logGroupName;
private final String logStreamName;
private final boolean enableCloudWatchLogging;
private final OutputScope outputScope;
/**
* Controls the detail level of guardrail assessments returned.
*/
public enum OutputScope {
INTERVENTIONS,
FULL
}
private GuardrailConfig(Builder builder) {
this.guardrailId = builder.guardrailId;
this.guardrailVersion = builder.guardrailVersion;
this.region = builder.region;
this.credentialsProvider = builder.credentialsProvider;
this.s3Bucket = builder.s3Bucket;
this.s3Prefix = builder.s3Prefix;
this.enableS3Logging = builder.enableS3Logging;
this.logGroupName = builder.logGroupName;
this.logStreamName = builder.logStreamName;
this.enableCloudWatchLogging = builder.enableCloudWatchLogging;
this.outputScope = builder.outputScope;
}
public String getGuardrailId() { return guardrailId; }
public String getGuardrailVersion() { return guardrailVersion; }
public Region getRegion() { return region; }
public AwsCredentialsProvider getCredentialsProvider() { return credentialsProvider; }
public String getS3Bucket() { return s3Bucket; }
public String getS3Prefix() { return s3Prefix; }
public boolean isEnableS3Logging() { return enableS3Logging; }
public String getLogGroupName() { return logGroupName; }
public String getLogStreamName() { return logStreamName; }
public boolean isEnableCloudWatchLogging() { return enableCloudWatchLogging; }
public OutputScope getOutputScope() { return outputScope; }
/** @deprecated Use {@link #isEnableS3Logging()} instead. */
@Deprecated
public boolean isEnableLogging() { return enableS3Logging; }
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String guardrailId;
private String guardrailVersion;
private Region region;
private AwsCredentialsProvider credentialsProvider;
// S3
private String s3Bucket;
private String s3Prefix = "guardrail-logs";
private boolean enableS3Logging = true;
// CloudWatch Logs
private String logGroupName = "/app/guardrail-assessments";
private String logStreamName = "guardrail-assessments";
private boolean enableCloudWatchLogging = false;
private OutputScope outputScope = OutputScope.FULL;
/** Required. Guardrail identifier (ID or ARN). */
public Builder guardrailId(String guardrailId) {
this.guardrailId = guardrailId;
return this;
}
/** Required. Guardrail version number. */
public Builder guardrailVersion(String guardrailVersion) {
this.guardrailVersion = guardrailVersion;
return this;
}
/** Required. AWS Region. */
public Builder region(Region region) {
this.region = region;
return this;
}
/** Required. AWS credentials provider. */
public Builder credentialsProvider(AwsCredentialsProvider credentialsProvider) {
this.credentialsProvider = credentialsProvider;
return this;
}
/** S3 bucket for assessment logs. Required when S3 logging is enabled. */
public Builder s3Bucket(String s3Bucket) {
this.s3Bucket = s3Bucket;
return this;
}
/** S3 key prefix. Default: "guardrail-logs". */
public Builder s3Prefix(String s3Prefix) {
this.s3Prefix = s3Prefix;
return this;
}
/** Enable/disable S3 logging. Default: true. */
public Builder enableS3Logging(boolean enableS3Logging) {
this.enableS3Logging = enableS3Logging;
return this;
}
/** @deprecated Use {@link #enableS3Logging(boolean)} instead. */
@Deprecated
public Builder enableLogging(boolean enableLogging) {
this.enableS3Logging = enableLogging;
return this;
}
/**
* CloudWatch Logs group name. Default: "/app/guardrail-assessments".
* The log group will be created automatically if it doesn't exist.
*/
public Builder logGroupName(String logGroupName) {
this.logGroupName = logGroupName;
return this;
}
/**
* CloudWatch Logs stream name. Default: "guardrail-assessments".
* The log stream will be created automatically if it doesn't exist.
*/
public Builder logStreamName(String logStreamName) {
this.logStreamName = logStreamName;
return this;
}
/**
* Enable CloudWatch Logs writing. Default: false.
* When enabled, assessment logs are written to CloudWatch Logs for
* real-time monitoring and Logs Insights queries.
*/
public Builder enableCloudWatchLogging(boolean enableCloudWatchLogging) {
this.enableCloudWatchLogging = enableCloudWatchLogging;
return this;
}
/** Assessment output scope. Default: FULL. */
public Builder outputScope(OutputScope outputScope) {
this.outputScope = outputScope;
return this;
}
public GuardrailConfig build() {
if (guardrailId == null || guardrailId.isEmpty()) {
throw new IllegalArgumentException("guardrailId is required");
}
if (guardrailVersion == null || guardrailVersion.isEmpty()) {
throw new IllegalArgumentException("guardrailVersion is required");
}
if (region == null) {
throw new IllegalArgumentException("region is required");
}
if (credentialsProvider == null) {
throw new IllegalArgumentException("credentialsProvider is required");
}
if (enableS3Logging && (s3Bucket == null || s3Bucket.isEmpty())) {
throw new IllegalArgumentException("s3Bucket is required when S3 logging is enabled");
}
if (enableCloudWatchLogging && (logGroupName == null || logGroupName.isEmpty())) {
throw new IllegalArgumentException("logGroupName is required when CloudWatch logging is enabled");
}
return new GuardrailConfig(this);
}
}
}
9.3 GuardrailFilter.java(API 调用 + 日志分发)
package com.aws.guardrail.filter;
import com.aws.guardrail.filter.logging.CloudWatchLogWriter;
import com.aws.guardrail.filter.logging.LogRecordBuilder;
import com.aws.guardrail.filter.logging.S3LogWriter;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import software.amazon.awssdk.services.bedrockruntime.model.*;
import java.time.Instant;
import java.util.Objects;
/**
* Core service that wraps Amazon Bedrock ApplyGuardrail API with automatic logging
* to S3 and/or CloudWatch Logs.
*
* <p>Responsibilities:</p>
* <ul>
* <li>Call ApplyGuardrail API</li>
* <li>Dispatch structured log to configured destinations (S3 / CloudWatch Logs)</li>
* </ul>
*/
public class GuardrailFilter implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(GuardrailFilter.class);
private final GuardrailConfig config;
private final BedrockRuntimeClient bedrockClient;
private final LogRecordBuilder logRecordBuilder;
private final S3LogWriter s3LogWriter;
private final CloudWatchLogWriter cwLogWriter;
private final boolean ownsBedrockClient;
/**
* Creates a GuardrailFilter with auto-created AWS clients.
*/
public static GuardrailFilter create(GuardrailConfig config) {
Region region = config.getRegion();
AwsCredentialsProvider creds = config.getCredentialsProvider();
BedrockRuntimeClient bedrockClient = BedrockRuntimeClient.builder()
.region(region).credentialsProvider(creds).build();
S3LogWriter s3LogWriter = config.isEnableS3Logging()
? new S3LogWriter(config.getS3Bucket(), config.getS3Prefix(), region, creds)
: null;
CloudWatchLogWriter cwLogWriter = config.isEnableCloudWatchLogging()
? new CloudWatchLogWriter(config.getLogGroupName(), config.getLogStreamName(), region, creds)
: null;
return new GuardrailFilter(config, bedrockClient, s3LogWriter, cwLogWriter, true);
}
private GuardrailFilter(GuardrailConfig config,
BedrockRuntimeClient bedrockClient,
S3LogWriter s3LogWriter,
CloudWatchLogWriter cwLogWriter,
boolean ownsBedrockClient) {
this.config = config;
this.bedrockClient = bedrockClient;
this.logRecordBuilder = new LogRecordBuilder(config);
this.s3LogWriter = s3LogWriter;
this.cwLogWriter = cwLogWriter;
this.ownsBedrockClient = ownsBedrockClient;
}
public GuardrailResult filterInput(String text) {
return applyGuardrail(text, GuardrailContentSource.INPUT);
}
public GuardrailResult filterOutput(String text) {
return applyGuardrail(text, GuardrailContentSource.OUTPUT);
}
public GuardrailResult filter(String text, GuardrailContentSource source) {
return applyGuardrail(text, source);
}
private GuardrailResult applyGuardrail(String text, GuardrailContentSource source) {
Objects.requireNonNull(text, "text must not be null");
Objects.requireNonNull(source, "source must not be null");
// Build request
ApplyGuardrailRequest.Builder requestBuilder = ApplyGuardrailRequest.builder()
.guardrailIdentifier(config.getGuardrailId())
.guardrailVersion(config.getGuardrailVersion())
.source(source)
.content(GuardrailContentBlock.builder()
.text(GuardrailTextBlock.builder().text(text).build())
.build());
if (config.getOutputScope() == GuardrailConfig.OutputScope.FULL) {
requestBuilder.outputScope(GuardrailOutputScope.FULL);
}
// Call API
long startTime = System.nanoTime();
ApplyGuardrailResponse response = bedrockClient.applyGuardrail(requestBuilder.build());
double latencyMs = (System.nanoTime() - startTime) / 1_000_000.0;
GuardrailResult result = new GuardrailResult(response, latencyMs);
log.debug("ApplyGuardrail: action={}, latency={}ms", result.getAction(), latencyMs);
// Build log record and dispatch
dispatchLog(text, source, response, latencyMs);
return result;
}
private void dispatchLog(String text, GuardrailContentSource source,
ApplyGuardrailResponse response, double latencyMs) {
Instant now = Instant.now();
GuardrailLogRecord record = logRecordBuilder.build(text, source, response, latencyMs, now);
String json;
try {
json = logRecordBuilder.toJson(record);
} catch (JsonProcessingException e) {
log.error("Failed to serialize log record", e);
return;
}
if (s3LogWriter != null) {
try {
s3LogWriter.putLog(json, now);
} catch (Exception e) {
log.warn("Failed to write log to S3: {}", e.getMessage(), e);
}
}
if (cwLogWriter != null) {
try {
cwLogWriter.putLogEvent(json, now.toEpochMilli());
} catch (Exception e) {
log.warn("Failed to write log to CloudWatch: {}", e.getMessage(), e);
}
}
}
@Override
public void close() {
if (ownsBedrockClient) {
try { bedrockClient.close(); } catch (Exception e) { log.debug("Error closing bedrock client", e); }
}
if (s3LogWriter != null) {
try { s3LogWriter.close(); } catch (Exception e) { log.debug("Error closing S3 log writer", e); }
}
if (cwLogWriter != null) {
try { cwLogWriter.close(); } catch (Exception e) { log.debug("Error closing CW log writer", e); }
}
}
}
9.4 GuardrailResult.java(过滤结果)
package com.aws.guardrail.filter;
import software.amazon.awssdk.services.bedrockruntime.model.ApplyGuardrailResponse;
/**
* Result of a Guardrail filter operation.
* Provides easy access to the action taken and output text.
*/
public class GuardrailResult {
private final String action;
private final String actionReason;
private final String outputText;
private final boolean intervened;
private final ApplyGuardrailResponse rawResponse;
private final double latencyMs;
public GuardrailResult(ApplyGuardrailResponse response, double latencyMs) {
this.rawResponse = response;
this.action = response.actionAsString();
this.actionReason = response.actionReason();
this.latencyMs = latencyMs;
this.intervened = "GUARDRAIL_INTERVENED".equals(this.action);
// Extract output text
if (response.outputs() != null && !response.outputs().isEmpty()) {
this.outputText = response.outputs().get(0).text();
} else {
this.outputText = null;
}
}
/**
* The action taken by the guardrail: "NONE" or "GUARDRAIL_INTERVENED".
*/
public String getAction() {
return action;
}
/**
* Reason for intervention (null if no intervention).
*/
public String getActionReason() {
return actionReason;
}
/**
* The output text (modified/blocked message if intervened, or original if passed).
*/
public String getOutputText() {
return outputText;
}
/**
* Whether the guardrail intervened (blocked or modified the content).
*/
public boolean isIntervened() {
return intervened;
}
/**
* The raw AWS SDK response for advanced inspection.
*/
public ApplyGuardrailResponse getRawResponse() {
return rawResponse;
}
/**
* Latency of the ApplyGuardrail API call in milliseconds.
*/
public double getLatencyMs() {
return latencyMs;
}
@Override
public String toString() {
return "GuardrailResult{" +
"action='" + action + '\'' +
", intervened=" + intervened +
", outputText='" + (outputText != null ? outputText.substring(0, Math.min(100, outputText.length())) : "null") + '\'' +
", latencyMs=" + latencyMs +
'}';
}
}
9.5 GuardrailLogRecord.java(日志结构)
package com.aws.guardrail.filter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
/**
* Structured log record for a Guardrail assessment.
* Written as JSON to S3 for downstream analysis via Athena/QuickSight.
*
* Fields are flattened for easy Athena SQL queries.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class GuardrailLogRecord {
@JsonProperty("timestamp")
private long timestamp;
@JsonProperty("date")
private String date;
@JsonProperty("hour")
private String hour;
@JsonProperty("guardrail_id")
private String guardrailId;
@JsonProperty("guardrail_version")
private String guardrailVersion;
@JsonProperty("source")
private String source;
@JsonProperty("input_text")
private String inputText;
@JsonProperty("action")
private String action;
@JsonProperty("action_reason")
private String actionReason;
@JsonProperty("output_text")
private String outputText;
@JsonProperty("latency_ms")
private double latencyMs;
@JsonProperty("assessments_json")
private String assessmentsJson;
@JsonProperty("content_policy_triggered")
private boolean contentPolicyTriggered;
@JsonProperty("content_policy_type")
private String contentPolicyType;
@JsonProperty("content_policy_confidence")
private String contentPolicyConfidence;
@JsonProperty("content_policy_strength")
private String contentPolicyStrength;
@JsonProperty("topic_policy_triggered")
private boolean topicPolicyTriggered;
@JsonProperty("topic_policy_name")
private String topicPolicyName;
@JsonProperty("word_policy_triggered")
private boolean wordPolicyTriggered;
@JsonProperty("word_policy_match")
private String wordPolicyMatch;
@JsonProperty("sensitive_info_triggered")
private boolean sensitiveInfoTriggered;
@JsonProperty("sensitive_info_type")
private String sensitiveInfoType;
@JsonProperty("contextual_grounding_triggered")
private boolean contextualGroundingTriggered;
@JsonProperty("contextual_grounding_score")
private Double contextualGroundingScore;
@JsonProperty("usage")
private Map<String, Integer> usage;
// Getters and Setters
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getHour() {
return hour;
}
public void setHour(String hour) {
this.hour = hour;
}
public String getGuardrailId() {
return guardrailId;
}
public void setGuardrailId(String guardrailId) {
this.guardrailId = guardrailId;
}
public String getGuardrailVersion() {
return guardrailVersion;
}
public void setGuardrailVersion(String guardrailVersion) {
this.guardrailVersion = guardrailVersion;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getInputText() {
return inputText;
}
public void setInputText(String inputText) {
this.inputText = inputText;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getActionReason() {
return actionReason;
}
public void setActionReason(String actionReason) {
this.actionReason = actionReason;
}
public String getOutputText() {
return outputText;
}
public void setOutputText(String outputText) {
this.outputText = outputText;
}
public double getLatencyMs() {
return latencyMs;
}
public void setLatencyMs(double latencyMs) {
this.latencyMs = latencyMs;
}
public String getAssessmentsJson() {
return assessmentsJson;
}
public void setAssessmentsJson(String assessmentsJson) {
this.assessmentsJson = assessmentsJson;
}
public boolean isContentPolicyTriggered() {
return contentPolicyTriggered;
}
public void setContentPolicyTriggered(boolean contentPolicyTriggered) {
this.contentPolicyTriggered = contentPolicyTriggered;
}
public String getContentPolicyType() {
return contentPolicyType;
}
public void setContentPolicyType(String contentPolicyType) {
this.contentPolicyType = contentPolicyType;
}
public String getContentPolicyConfidence() {
return contentPolicyConfidence;
}
public void setContentPolicyConfidence(String contentPolicyConfidence) {
this.contentPolicyConfidence = contentPolicyConfidence;
}
public String getContentPolicyStrength() {
return contentPolicyStrength;
}
public void setContentPolicyStrength(String contentPolicyStrength) {
this.contentPolicyStrength = contentPolicyStrength;
}
public boolean isTopicPolicyTriggered() {
return topicPolicyTriggered;
}
public void setTopicPolicyTriggered(boolean topicPolicyTriggered) {
this.topicPolicyTriggered = topicPolicyTriggered;
}
public String getTopicPolicyName() {
return topicPolicyName;
}
public void setTopicPolicyName(String topicPolicyName) {
this.topicPolicyName = topicPolicyName;
}
public boolean isWordPolicyTriggered() {
return wordPolicyTriggered;
}
public void setWordPolicyTriggered(boolean wordPolicyTriggered) {
this.wordPolicyTriggered = wordPolicyTriggered;
}
public String getWordPolicyMatch() {
return wordPolicyMatch;
}
public void setWordPolicyMatch(String wordPolicyMatch) {
this.wordPolicyMatch = wordPolicyMatch;
}
public boolean isSensitiveInfoTriggered() {
return sensitiveInfoTriggered;
}
public void setSensitiveInfoTriggered(boolean sensitiveInfoTriggered) {
this.sensitiveInfoTriggered = sensitiveInfoTriggered;
}
public String getSensitiveInfoType() {
return sensitiveInfoType;
}
public void setSensitiveInfoType(String sensitiveInfoType) {
this.sensitiveInfoType = sensitiveInfoType;
}
public boolean isContextualGroundingTriggered() {
return contextualGroundingTriggered;
}
public void setContextualGroundingTriggered(boolean contextualGroundingTriggered) {
this.contextualGroundingTriggered = contextualGroundingTriggered;
}
public Double getContextualGroundingScore() {
return contextualGroundingScore;
}
public void setContextualGroundingScore(Double contextualGroundingScore) {
this.contextualGroundingScore = contextualGroundingScore;
}
public Map<String, Integer> getUsage() {
return usage;
}
public void setUsage(Map<String, Integer> usage) {
this.usage = usage;
}
}
9.6 LogRecordBuilder.java(日志构建 + 策略提取)
package com.aws.guardrail.filter.logging;
import com.aws.guardrail.filter.GuardrailConfig;
import com.aws.guardrail.filter.GuardrailLogRecord;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import software.amazon.awssdk.services.bedrockruntime.model.*;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Builds a structured {@link GuardrailLogRecord} from an ApplyGuardrail API response.
* Extracts and flattens policy trigger details for easy downstream querying.
*/
public class LogRecordBuilder {
private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter HOUR_FMT = DateTimeFormatter.ofPattern("HH");
private final GuardrailConfig config;
public LogRecordBuilder(GuardrailConfig config) {
this.config = config;
}
/**
* Build a log record from an API response.
*/
public GuardrailLogRecord build(String inputText, GuardrailContentSource source,
ApplyGuardrailResponse response, double latencyMs,
Instant now) {
GuardrailLogRecord record = new GuardrailLogRecord();
// Basic fields
record.setTimestamp(now.getEpochSecond());
record.setDate(DATE_FMT.format(now.atZone(ZoneOffset.UTC)));
record.setHour(HOUR_FMT.format(now.atZone(ZoneOffset.UTC)));
record.setGuardrailId(config.getGuardrailId());
record.setGuardrailVersion(config.getGuardrailVersion());
record.setSource(source.toString());
record.setInputText(inputText);
record.setAction(response.actionAsString());
record.setActionReason(response.actionReason());
record.setLatencyMs(latencyMs);
// Output text
if (response.outputs() != null && !response.outputs().isEmpty()) {
record.setOutputText(response.outputs().get(0).text());
}
// Full assessments JSON
try {
record.setAssessmentsJson(MAPPER.writeValueAsString(response.assessments()));
} catch (JsonProcessingException e) {
record.setAssessmentsJson("[]");
}
// Usage
if (response.usage() != null) {
Map<String, Integer> usageMap = new HashMap<>();
putIfNotNull(usageMap, "topicPolicyUnits", response.usage().topicPolicyUnits());
putIfNotNull(usageMap, "contentPolicyUnits", response.usage().contentPolicyUnits());
putIfNotNull(usageMap, "wordPolicyUnits", response.usage().wordPolicyUnits());
putIfNotNull(usageMap, "sensitiveInformationPolicyUnits", response.usage().sensitiveInformationPolicyUnits());
putIfNotNull(usageMap, "sensitiveInformationPolicyFreeUnits", response.usage().sensitiveInformationPolicyFreeUnits());
putIfNotNull(usageMap, "contextualGroundingPolicyUnits", response.usage().contextualGroundingPolicyUnits());
record.setUsage(usageMap);
}
// Extract flattened policy triggers
if (response.assessments() != null) {
for (GuardrailAssessment assessment : response.assessments()) {
extractContentPolicy(assessment, record);
extractTopicPolicy(assessment, record);
extractWordPolicy(assessment, record);
extractSensitiveInfoPolicy(assessment, record);
extractContextualGrounding(assessment, record);
}
}
return record;
}
/**
* Serialize a log record to JSON.
*/
public String toJson(GuardrailLogRecord record) throws JsonProcessingException {
return MAPPER.writeValueAsString(record);
}
// ---- Policy extraction ----
private void extractContentPolicy(GuardrailAssessment assessment, GuardrailLogRecord record) {
if (assessment.contentPolicy() == null) return;
List<GuardrailContentFilter> filters = assessment.contentPolicy().filters();
if (filters == null) return;
for (GuardrailContentFilter filter : filters) {
if ("BLOCKED".equals(filter.actionAsString())) {
record.setContentPolicyTriggered(true);
record.setContentPolicyType(filter.typeAsString());
record.setContentPolicyConfidence(filter.confidenceAsString());
record.setContentPolicyStrength(filter.filterStrengthAsString());
break;
}
}
}
private void extractTopicPolicy(GuardrailAssessment assessment, GuardrailLogRecord record) {
if (assessment.topicPolicy() == null) return;
List<GuardrailTopic> topics = assessment.topicPolicy().topics();
if (topics == null) return;
for (GuardrailTopic topic : topics) {
if ("BLOCKED".equals(topic.actionAsString())) {
record.setTopicPolicyTriggered(true);
record.setTopicPolicyName(topic.name());
break;
}
}
}
private void extractWordPolicy(GuardrailAssessment assessment, GuardrailLogRecord record) {
if (assessment.wordPolicy() == null) return;
List<GuardrailCustomWord> words = assessment.wordPolicy().customWords();
if (words == null) return;
for (GuardrailCustomWord word : words) {
if ("BLOCKED".equals(word.actionAsString())) {
record.setWordPolicyTriggered(true);
record.setWordPolicyMatch(word.match());
break;
}
}
}
private void extractSensitiveInfoPolicy(GuardrailAssessment assessment, GuardrailLogRecord record) {
if (assessment.sensitiveInformationPolicy() == null) return;
List<GuardrailPiiEntityFilter> entities = assessment.sensitiveInformationPolicy().piiEntities();
if (entities == null) return;
for (GuardrailPiiEntityFilter entity : entities) {
if ("BLOCKED".equals(entity.actionAsString()) || "ANONYMIZED".equals(entity.actionAsString())) {
record.setSensitiveInfoTriggered(true);
record.setSensitiveInfoType(entity.typeAsString());
break;
}
}
}
private void extractContextualGrounding(GuardrailAssessment assessment, GuardrailLogRecord record) {
if (assessment.contextualGroundingPolicy() == null) return;
List<GuardrailContextualGroundingFilter> filters = assessment.contextualGroundingPolicy().filters();
if (filters == null) return;
for (GuardrailContextualGroundingFilter filter : filters) {
if ("BLOCKED".equals(filter.actionAsString())) {
record.setContextualGroundingTriggered(true);
record.setContextualGroundingScore(filter.score());
break;
}
}
}
private void putIfNotNull(Map<String, Integer> map, String key, Integer value) {
if (value != null) {
map.put(key, value);
}
}
}
9.7 S3LogWriter.java(S3 写入)
package com.aws.guardrail.filter.logging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* Writes guardrail assessment logs to S3 in Hive-partitioned JSON format.
*
* <p>S3 key structure:</p>
* <pre>
* {prefix}/year=YYYY/month=MM/day=DD/hour=HH/{timestamp}.json
* </pre>
*/
public class S3LogWriter implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(S3LogWriter.class);
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter HOUR_FMT = DateTimeFormatter.ofPattern("HH");
private final S3Client s3Client;
private final String bucket;
private final String prefix;
private final boolean ownsClient;
public S3LogWriter(String bucket, String prefix, Region region, AwsCredentialsProvider credentialsProvider) {
this.bucket = bucket;
this.prefix = prefix;
this.s3Client = S3Client.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
this.ownsClient = true;
}
public S3LogWriter(String bucket, String prefix, S3Client s3Client) {
this.bucket = bucket;
this.prefix = prefix;
this.s3Client = s3Client;
this.ownsClient = false;
}
/**
* Write a JSON log to S3 with Hive-style partitioning.
*/
public void putLog(String json, Instant now) {
String dateStr = DATE_FMT.format(now.atZone(ZoneOffset.UTC));
String[] dateParts = dateStr.split("-");
String hourStr = HOUR_FMT.format(now.atZone(ZoneOffset.UTC));
String key = String.format("%s/year=%s/month=%s/day=%s/hour=%s/%s_%d.json",
prefix, dateParts[0], dateParts[1], dateParts[2],
hourStr, dateStr.replace("-", ""), now.toEpochMilli());
s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType("application/json")
.build(),
RequestBody.fromString(json));
log.debug("Log written to s3://{}/{}", bucket, key);
}
@Override
public void close() {
if (ownsClient) {
try {
s3Client.close();
} catch (Exception e) {
log.debug("Error closing S3 client", e);
}
}
}
}
9.8 CloudWatchLogWriter.java(CloudWatch 写入)
package com.aws.guardrail.filter.logging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient;
import software.amazon.awssdk.services.cloudwatchlogs.model.*;
import java.util.Collections;
/**
* Writes structured guardrail assessment logs to CloudWatch Logs.
*
* <p>Automatically creates the log group and log stream if they don't exist.
* Logs are written as JSON for easy querying with CloudWatch Logs Insights.</p>
*/
public class CloudWatchLogWriter implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(CloudWatchLogWriter.class);
private final CloudWatchLogsClient logsClient;
private final String logGroupName;
private final String logStreamName;
private final boolean ownsClient;
private boolean initialized = false;
public CloudWatchLogWriter(String logGroupName, String logStreamName,
Region region, AwsCredentialsProvider credentialsProvider) {
this.logGroupName = logGroupName;
this.logStreamName = logStreamName;
this.logsClient = CloudWatchLogsClient.builder()
.region(region)
.credentialsProvider(credentialsProvider)
.build();
this.ownsClient = true;
}
public CloudWatchLogWriter(String logGroupName, String logStreamName,
CloudWatchLogsClient logsClient) {
this.logGroupName = logGroupName;
this.logStreamName = logStreamName;
this.logsClient = logsClient;
this.ownsClient = false;
}
/**
* Ensure the log group and log stream exist (lazy init on first write).
*/
private synchronized void ensureInitialized() {
if (initialized) return;
// Create log group
try {
logsClient.createLogGroup(CreateLogGroupRequest.builder()
.logGroupName(logGroupName)
.build());
log.info("Created CloudWatch log group: {}", logGroupName);
} catch (ResourceAlreadyExistsException e) {
log.debug("Log group already exists: {}", logGroupName);
}
// Create log stream
try {
logsClient.createLogStream(CreateLogStreamRequest.builder()
.logGroupName(logGroupName)
.logStreamName(logStreamName)
.build());
log.info("Created CloudWatch log stream: {}/{}", logGroupName, logStreamName);
} catch (ResourceAlreadyExistsException e) {
log.debug("Log stream already exists: {}/{}", logGroupName, logStreamName);
}
initialized = true;
}
/**
* Write a JSON log event to CloudWatch Logs.
*
* @param json JSON string to write
* @param timestamp Unix epoch milliseconds
*/
public void putLogEvent(String json, long timestamp) {
ensureInitialized();
InputLogEvent logEvent = InputLogEvent.builder()
.timestamp(timestamp)
.message(json)
.build();
PutLogEventsRequest request = PutLogEventsRequest.builder()
.logGroupName(logGroupName)
.logStreamName(logStreamName)
.logEvents(Collections.singletonList(logEvent))
.build();
logsClient.putLogEvents(request);
log.debug("Log event written to {}/{}", logGroupName, logStreamName);
}
String getLogGroupName() {
return logGroupName;
}
String getLogStreamName() {
return logStreamName;
}
@Override
public void close() {
if (ownsClient) {
try {
logsClient.close();
} catch (Exception e) {
log.debug("Error closing CloudWatch Logs client", e);
}
}
}
}
9.9 pom.xml (Maven 配置)
十、总结
- ApplyGuardrail API 的日志监控需要自建:Model Invocation Logging 不支持该 API,但 API 响应自带完整评估详情,捕获即可
- CloudWatch 方案适合实时运维:结合原生 Metrics 和应用日志,秒级延迟,支持告警
- S3 + Athena + QuickSight 适合深度分析:低成本长期存储,标准 SQL 查询,交互式可视化
- 两种方案按需启用:Java 封装库支持两种方案,可以按需开启,一行代码搞定
outputScope=FULL是关键配置:它让你能看到”差一点就被拦截”的内容,是误报分析的基础
➡️ 下一步行动:
相关产品:
- Amazon CloudWatch — 可观测性工具
- Amazon S3 — 适用于 AI、分析和存档的几乎无限的安全对象存储
- Amazon Bedrock — 用于构建生成式人工智能应用程序和代理的端到端平台
- Amazon Athena — 使用 SQL 在 S3 中查询数据
- Amazon QuickSight — 高速业务分析服务
相关文章:
- LiteLLM + Amazon QuickSight 数据可视化配置手册
- 使用 Amazon Athena 分析 Kiro 团队用量报表:动态模型列的数据建模实践
- 把 OpenClaw 从个人助手变成客服:一次信任模型的翻转
- AI Agent 的迁移与现代化 — 使用 Amazon Bedrock AgentCore 将 OpenClaw 从单机改造为多租户 Serverless 架构 第六篇
- AI Agent 的迁移与现代化 — 使用 Amazon Bedrock AgentCore 将 OpenClaw 从单机改造为多租户 Serverless 架构 第一篇
十一、参考链接
- Monitor Amazon Bedrock Guardrails using CloudWatch metrics
- Use the ApplyGuardrail API
- aws-samples/sample-bedrock-guardrails-monitoring
- Amazon Athena Partition Projection
- Amazon QuickSight SPICE Incremental Refresh
*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。
本篇作者
AWS 架构师中心:云端创新的引领者探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用 |
![]() |








