Floci 的 CloudWatch Logs 指标过滤器契约验证:一份来自真实 AWS 观测的行为边界档案
【免费下载链接】flociLight, fluffy, and always free - The AWS Local Emulator alternative项目地址: https://gitcode.com/gh_mirrors/fl/floci
本文围绕 docs/services/cloudwatch-metric-filters-verification.md 展开。该文档是 Floci(AWS 本地模拟器)中 CloudWatch Logs 指标过滤器(Metric Filter)模块的一份"契约验证记录":它把在真实 AWS 上实测到的行为与模拟器测试期望明确分离开来,并给出可供 Floci 及其 SDK 兼容性测试直接消费的规范化证据文件。读完本文,你将理解 Floci 如何用"三档证据标签"约束模拟行为、148 例模式语料与有状态回放脚本如何工作,以及默认值累加、维度全有全无、系统维度、正则配额、CloudFormation 替换与回滚等关键行为在源码中的落地方式。
一、为什么需要一份"契约验证记录"
CloudWatch Logs 的指标过滤器(Metric Filter)位于日志与指标的交界处:PutMetricFilter定义过滤规则与指标变换,PutLogEvents写入日志时按规则匹配并产出指标数据点。这个行为链路复杂、边界众多(默认值、维度、时间戳、正则配额、CloudFormation 生命周期……),任何模拟器在复刻时都容易"想当然"。
Floci 的做法不是依赖文档猜测,而是用真实验证结果约束模拟实现。该文档开篇就划清了界限:
- 它是"把 AWS 观测结果与模拟器测试、文档推断需求分开"的记录;
- 不是"每个 CloudWatch Logs 行为都已测试"的声明。
所有观测数据以 JSON fixture 形式固化在仓库中,作为测试的输入数据而非模拟器输出,从而保证"模拟器测试期望"永远来自 AWS 真值。
二、三档证据标签:区分"实测"与"推断"
文档定义了证据可信度分级,这也是阅读本文所有结论时必须先理解的规则:
| 标签 | 含义 |
|---|---|
| Verified on Live AWS | 该场景已对官方 AWS 端点实际执行过;不暗示该场景之外的变体也成立 |
| SDK-tested against Floci | AWS SDK 客户端实际驱动过 Floci,证明的是客户端互操作性,而非独立的 AWS 行为真值 |
| Documentation-backed | 有 AWS API 参考、指南或公开 schema 支撑该需求;不代表真实执行过 |
这套标签体系在 fixture 中也有一致体现:例如 metric-filter-publishing-aws.json 的顶层字段就是"verification": "Verified on Live AWS",并声明"值是独立的 AWS 结果,而非模拟器生成的期望"。
三、观测环境与方法
文档记录了 2026-09-16 在eu-west-1的真实探测条件,全部使用隔离的合成日志组、指标过滤器和单个 CloudFormation 栈:
- AWS CLI 版本
2.36.45; - STS 身份私有校验,服务调用使用显式官方端点;
- 指标查询统一用
GetMetricStatistics,周期 60 秒,统计量覆盖Sum、SampleCount、Minimum、Maximum; - 主观测结束于 08:40:45 UTC,混合事件跟进收敛于 08:56:48 UTC,最终系统维度探测与清理完成于 09:06:51 UTC;
- 收敛判定:早期出现的空结果或部分结果不被当作最终契约,必须反复读取直至结果稳定。
这一点在回放脚本中体现为await_stable机制(见 metric_filter_replay.py):在超时上限内持续轮询,直到"完整结果"连续稳定达到--stable-seconds(AWS 模式至少 30 秒)才算收敛,否则报ReplayError("observations did not converge within the bounded window")。
四、模式语料库:148 例合成用例的捕获脚本
文档指出模式语料由 tools/aws-capture/cloudwatchlogs/capture_filter_patterns.py 单独捕获,共148 个合成用例,覆盖三类套件:
| 套件 | 内容示例 |
|---|---|
base(cases()) | 术语匹配(ERROR、-timeout否定、?ERROR可选、短语引号、通配ERR*)、正则(%ERROR|WARN%、锚点、量词、\d+、字符类)、JSON 数值/布尔/IS NULL/NOT EXISTS/通配*/指数/精度、选择器(数组索引、$.a[*]、$['a.b']、负索引)、空格分隔字段([a, b, c]、省略号、约束、跨字段 OR) |
edges(edge_cases()) | 数值边界(1e309溢出、1e-999下溢、9007199254740993精度)、结构化消息的=/!=/通配、关键字大小写、正则计数(一个模式内 2 个与 3 个正则的差异) |
boundaries(boundary_cases()) | 数组内IS TRUE/FALSE/NULL、嵌套通配、a/b、a@b、unicodecafé、" "空白短语、超大指数 |
该脚本的关键安全设计(值得复刻):
- 强制官方端点:
--endpoint-url必须等于https://logs.{region}.amazonaws.com,否则直接parser.error拒绝执行(capture_filter_patterns.py); - 只读合成请求:仅调用
TestMetricFilter,不写任何资源、不使用真实用户日志; - 限流重试:遇到
Throttling按 2 的指数退避重试最多 4 次(L206-L214); - 异常白名单:只有
InvalidParameterException才会被记为error字段,其他任何异常都会中止捕获(raise RuntimeError("AWS capture blocked: ...")); - provenance 元数据:每个 fixture 都记录
source、capturedAt、region、endpoint、eventNumberBase等溯源信息,并声明"仅合成数据"。
运行方式是显式 opt-in:--profile、--region、--endpoint-url、--output均必填。正常测试只消费它产出的filter-pattern-aws*.json记录文件,无需凭证、无网络访问。
五、有状态回放:metric_filter_replay.py 的双模式设计
模式语料(TestMetricFilter)只验证"匹配判定",而指标发布(默认值、维度、系统维度)需要有状态回放。文档所述的有状态观测记录在metric-filter-publishing-aws.json中,而执行回放的脚本是 compatibility-tests/sdk-test-python/tests/metric_filter_replay.py:
双模式互斥(parse_args):
--aws:写真实商业分区 AWS,必须同时提供--profile、--region、--ack-live-writes I_ACCEPT_AWS_WRITES三重确认,region 必须属于botocore已知的商业分区,且--stable-seconds >= 30、--timeout >= 120;--endpoint:指向localhost/127.0.0.1/::1的回环地址,仅允许 http/https、无凭据,自动使用 dummy 凭证test/test与默认区域eu-west-1。
防误删所有权机制(owned_group,L80-L95):创建日志组时打上floci-metric-replay: <token>标签;删除前再次验证所有权标签,若标签不匹配或已被占用,拒绝清理;创建失败时绝不删除既有资源。
离线测试守护(test_metric_filter_replay.py):所有 pytest 用例通过 monkeypatch 禁掉socket.connect,确保"导入与正常 pytest 执行绝不联网";并验证参数校验拒绝远程/无防护写入、客户端固定官方端点(即使环境变量被污染)、错误输出做脱敏(不泄露 profile、账号等私有值)。
观测矩阵(scenarios())对应 fixture 中的四组数据:
metricDefaults:混合批次、先命中后未命中、先未命中后命中、纯未命中(quiet)四类场景;extraction:缺失值/显式 null/非数值字符串/数值控制组;ordinaryDimensions:完整、缺 A、缺 B、全缺四种事件;systemDimensions:@aws.account、@aws.region、两者组合。
每个场景占据独立的事件时间分钟,且每个场景的期望值直接从 fixture 读取(不可变),这保证了可复现性。
六、观测行为全集(Verified on Live AWS)
下表是文档的核心——每一行都在其声明范围内于真实 AWS 上验证过。这是 Floci 实现指标过滤器时必须满足的行为契约:
| 领域 | 场景 | 观测结果 |
|---|---|---|
| 组级正则配额 | 五个各含两个正则表达式的指标过滤器 | 接受;第六个含正则的过滤器被拒,返回LimitExceededException。配额按"含正则的过滤器"计数 |
| 正则替换 | 把含正则的模式换成纯文本,之后在配额满时再从纯文本改回正则 | 第一次修改释放了槽位;被拒的更新保留了原纯文本定义;删除另一个含正则的过滤器后更新成功 |
| 模式内正则配额 | 一个模式里放三个表达式 | 拒绝,InvalidParameterException;两个表达式可接受 |
| 默认值贡献 | 一次请求三个未命中事件,默认值 7,随后无更多摄入 | Sum 21、SampleCount 3、Minimum 7、Maximum 7;后续空闲分钟不再出现额外数据点 |
| 混合批次 | 模式ERROR、指标值 3、默认 7;一个批次含ERROR、INFO、ERROR | Sum 13、SampleCount 3、Minimum 3、Maximum 7。匹配事件不会抑制未命中事件的默认值 |
| 分离批次 | 同一过滤器在同一事件时间分钟内先命中后未命中、或先未命中后命中 | 两种顺序都收敛到 Sum 10、SampleCount 2 |
| 迟到追加 | 先观测到默认值 11,之后同一历史分钟内追加带时间戳的匹配值 3 | Sum 14、SampleCount 2。先前的默认值不被撤回 |
| 时间戳落点 | 新摄入的倒签、当代、未来时间戳事件 | 指标出现在事件时间戳所在分钟;创建过滤器之前时间戳的事件,只要摄入发生在创建之后也可工作 |
| 提取值 | 用独立判别字段匹配;指标值引用$.value;默认 7 | 缺失或显式 null 值产生 7;数值 3 产生 3;非数值字符串在完整观测窗口内不产生数据点 |
| 变换引用校验 | JSON 模式中未提及的标量指标值与维度选择器;然后通配指标值$.values[*] | 未提及的标量选择器被接受;通配指标值被拒,InvalidParameterException。这四个规范化校验观测见metric-filter-reference-validation-aws.json |
| 无默认值 | 同样的提取场景,不设默认值 | 只有合法数值产生数据点 |
| 普通维度 | 从$.a配维度 A、$.b配维度 B;发送完整、缺 A、缺 B、全缺事件 | 完整集产生带维度的序列;三个不完整集产生无维度样本,而非部分维度序列或丢弃事件 |
| 默认值 JSON 类型 | 原始签名请求中数值defaultValue: 7,然后字符串defaultValue: "7" | 数字:HTTP 200 空 body;字符串:HTTP 400SerializationException,STRING_VALUE cannot be converted to Double |
| 默认值与维度 | 只有系统维度时的默认值;然后带普通变换维度的默认值 | 仅系统维度被接受;普通维度 + 默认值被拒绝 |
| 系统维度发布 | 常规非中心化日志组;模式ERROR、值 3、默认 7;一个ERROR一个INFO;依次请求 account、region、两者 | 匹配值获得请求的维度;模式未命中的默认值无维度。每种情况恰好两个样本、总计 10 |
| 维度数量 | 两个普通维度 + 一个系统维度,然后 + 两个系统维度 | 总计三个接受;总计四个拒绝,InvalidParameterException |
| 选择操作符 | fieldSelectionCriteria中的词形AND和OR | 接受 |
| 变换日志标志 | 在GetTransformer返回空配置的新建组上设置true和false | 均接受并可往返(round-trip);随后的账号策略读取返回零个 transformer 策略。不测试 transformer 执行 |
| CloudFormation 身份 | 显式命名的指标过滤器;栈输出Ref和DescribeStackResource | 两者都返回过滤器名称本身,不是LogGroupName|FilterName |
| CloudFormation 替换 | 重命名显式命名的过滤器 | 旧过滤器的DELETE_IN_PROGRESS/DELETE_COMPLETE先于新过滤器的CREATE_IN_PROGRESS/CREATE_COMPLETE;栈完成更新 |
| CloudFormation 回滚 | 在合法的WARN/值 2 定义之后,用非法模式{重命名 | 旧过滤器被删除、替换创建失败、回滚重建先前的名称与定义;日志读取、变化的 creationTime、Ref 与还原后的模板共同确认;被尝试的过滤器没有存活 |
| CloudFormation 替换策略 | 安装UpdateReplacePolicy: Retain后重命名 | 该类型仍然先删除旧过滤器再创建替换;无DELETE_SKIPPED事件,仅替换件存活。不适用于其他资源类型或栈 DeletionPolicy |
| TestMetricFilter 编号 | 记录的模式语料请求(含匹配与不匹配消息) | 匹配事件编号从 1 开始 |
| TestMetricFilter JSON 提取 | 记录的 JSON 模式匹配 | 公开的extractedValues是空对象;内部字段提取仍需要以发布指标值 |
七、关键行为解读:从观测到实现
7.1 默认值按事件累加,不是"每分钟一次"
文档特别强调:默认值观测推翻了"每分钟默认值封顶一次"的实现猜想。CloudWatch 按分钟聚合事件贡献;一个抑制同分钟内不同未命中事件的"分钟累加器"不是等价实现。
Floci 源码严格按"每个事件一个贡献"实现:在 CloudWatchLogsMetricFilterService.java 的evaluate方法中,对每个日志事件独立执行pattern.match,未命中时value = t.getDefaultValue(),每个匹配/未命中事件都生成一个独立的Publication。对应的 Floci 测试在 CloudWatchLogsMetricFilterPublishingTest.java 中验证:三个INFO未命中立即产生Sum 21、SampleCount 3,而下一个空闲分钟为 0。
7.2 迟到追加不撤回
"先默认值 11,后匹配值 3" 收敛到 Sum 14,说明已发布的数据点不会被重算。Floci 测试 lateMatchAppendsWithoutRetractingAlreadyPublishedDefault 精确复刻:先摄入INFO得到 11,再摄入同一分钟内的ERROR追加到 14。
7.3 时间戳按事件时间落点
事件时间戳(而非摄入时间)决定指标落在哪个分钟,且支持未来时间戳与"创建前时间戳 + 创建后摄入"。源码中Publication记录的是logEvent.getTimestamp() / 1000(毫秒转秒),测试 timestampsComeFromEventsIncludingBeforeCreationAndFutureNotIngestionTime 直接验证未来分钟与创建前分钟两种情况。文档同时澄清:这不意味着"过滤器创建前已摄入的日志会被追溯处理"。
7.4 提取值的三态语义
$.value缺失或显式 null → 使用默认值 7;数值 3 → 3;非数值字符串 → 不产生数据点。源码注释精确描述了这一规则:"Preserve raw text: only missing/null falls back. A present nonnumeric value skips."(只对缺失/null 回退到默认值,存在的非数值跳过)。
7.5 普通维度 all-or-none
维度要么全有、要么全无:不完整的维度集产生无维度样本,绝不产生部分维度序列。fixture 中ordinaryDimensions的absentSeries明确列出{"A": "alpha"}与{"B": "beta"}两个"不存在的序列"。源码在evaluate中遇到任何维度提取为 null 就dimensions.clear()并中断。
7.6 系统维度只随匹配事件发布
@aws.account/@aws.region系统维度作用于匹配事件,而模式未命中的默认值无维度。系统字段校验与维度上限在 MetricFilterRules.java 中:emitSystemFieldDimensions只允许这两个字段,且与普通维度合计不得超过MAX_DIMENSIONS = 3。
7.7 默认值的 wire 级类型校验
原始签名请求级别的观测(metric-filter-default-value-wire-aws.json)证明:defaultValue传 JSON 字符串"7"会在协议反序列化层被拒(HTTP 400、SerializationException、STRING_VALUE cannot be converted to Double),而数值7返回 200。这说明该约束发生在 AWS 服务端序列化层,模拟器在更外层校验同样合法——关键是必须拒绝。Floci 在 LogsMetricFilterCfnProvisioner.java 与CloudWatchLogsMetricFilterService.requireTransformation中均强制defaultValue为有限数值。
7.8 通配指标值被拒绝
metricValue: "$.values[*]"这类通配引用在 AWS 端返回InvalidParameterException: Invalid metric transformation: metric value $.values[*] must be a valid number。该观测在 metric-filter-reference-validation-aws.json 的四个规范化校验用例中记录(含"未提及的标量选择器被接受"作为正面对照)。源码的requireTransformation同样要求:metricValue要么是合法数字,要么是模式能提供的单个值字段(pattern.declaresSingleValueField(value)),通配引用不满足此条件。
7.9 正则配额:共享的组级 5 个 + 模式内 2 个
- 组级:每个日志组最多 5 个含正则的过滤器,且该配额由指标过滤器与订阅过滤器共享(
validateFilterRegexQuota同时扫描 metricFilterStore 与 subscriptionFilterStore,见 CloudWatchLogsService.java,常量MAX_REGEX_FILTERS_PER_LOG_GROUP = 5); - 模式内:单个模式最多 2 个正则表达式(
FilterPattern.MAX_REGEXES = 2,见 FilterPattern.java)。
文档明确标注:该共享配额是Documentation-backed(Put 操作参考 + 正则语法指南),因为有状态探测未创建订阅目标,不构成对实时混合写入配额行为的实证。
八、CloudFormation 生命周期:替换顺序与回滚
8.1 身份:Ref与物理 ID 都只是过滤器名称
与直觉(LogGroupName|FilterName)不同,AWS::Logs::MetricFilter的Ref和DescribeStackResource返回的都是过滤器名称本身。源码中setIdentity将 physicalId 设为identity.name(),日志组作为私有元数据(__FlociMetricFilterLogGroupName)存储(LogsMetricFilterCfnProvisioner.java)。
8.2 替换 = 先删后建
当前AWS::Logs::MetricFilterregistry schema 指定delete_then_create。metric-filter-cfn-aws.json记录了完整事件序列:UPDATE_IN_PROGRESS → DELETE_IN_PROGRESS → DELETE_COMPLETE → CREATE_IN_PROGRESS → CREATE_COMPLETE → UPDATE_COMPLETE,删除理由是"AWS 官方要求的替换:先删除现有资源,再创建新资源"。源码provision()中replacement分支完全按此顺序执行并上报事件。
8.3 回滚恢复完整旧定义
非法模式{的重命名探测产生了完整的回滚事件链:尝试创建失败 → 回滚删除尝试件 → 重建先前的名称与定义。fixture 记录priorInstanceWasRecreated: true、attemptedFilterSurvived: false、storedTemplateRestored: true。源码通过快照机制(SNAPSHOT属性保存完整旧定义)在rollbackUpdate中恢复;文档也确认了creationTime变化证明是"重建"而非"复用"。
8.4UpdateReplacePolicy: Retain对 MetricFilter 无效
一个反直觉的实测结论:即使安装UpdateReplacePolicy: Retain,重命名时该类型仍然先删后建,没有DELETE_SKIPPED事件,最终只存活替换件。源码注释明确:"AWS replaces this type by deleting first, including with UpdateReplacePolicy Retain."(AWS 通过先删除来替换该类型,包括带 Retain 策略时)。文档同时谨慎限定:这不描述其他资源类型或栈级 DeletionPolicy。
九、Floci 源码中的契约落地与工程化
9.1 服务实现与发布队列
CloudWatchLogsMetricFilterService.java 是核心实现,其常量直接对应文档观测到的 AWS 行为:
| 常量 | 值 | 对应行为 |
|---|---|---|
MAX_FILTERS_PER_LOG_GROUP | 100 | 组级过滤器上限 |
MAX_DIMENSIONS(MetricFilterRules) | 3 | 普通 + 系统维度合计上限 |
MAX_REGEX_FILTERS_PER_LOG_GROUP | 5 | 组级正则过滤器共享配额 |
MAX_REGEXES(FilterPattern) | 2 | 单模式正则数上限 |
MAX_QUEUED_SAMPLES | 100_000 | 发布队列容量 |
MAX_PENDING_SAMPLES | 10_000 | 失败重试保留样本数 |
发布链路采用异步队列:PutLogEvents触发onLogEventsIngested事件,每个过滤器的贡献被评估成不可变Batch入队,由单线程发布者(logs-metric-publisher)写出到 CloudWatch Metrics 服务。失败样本进入pending重试表,每秒重试一次,且带 60 tick 的聚合告警;队列放不下时在请求线程内联写入而非丢弃;重置/停止时通过resetGeneration代际计数丢弃属于旧状态的样本。这套设计保证"模拟器测试期望来自 AWS 真值"的 fixture 可以被确定性地复现。
9.2 测试如何消费 fixture
- Java 端:CloudWatchLogsMetricFilterPublishingTest.java 直接读取打包的
metric-filter-publishing-aws.json,把每一节(metricDefaults、extraction、ordinaryDimensions、systemDimensions)作为输入数据逐条断言 Floci 的发布结果。类注释强调:"AWS observations are input data, never regenerated from emulator output."(AWS 观测是输入数据,绝不由模拟器输出重新生成); - Python 端:fixture 打包在 compatibility-tests/sdk-test-python/tests/fixtures/metric-filter-publishing-aws.json,与回放模块同目录,
test_fixture_is_packaged_beside_the_module_not_in_a_repository_ancestor甚至校验它不落在仓库祖先目录; - Java SDK 兼容测试:MetricFilterFixture.java 从
classpath:/cloudwatchlogs/metric-filter-publishing-aws.json加载同一份 oracle。
9.3 未测量边界:诚实声明
文档明确列出不在观测范围内的边界,Floci 因此不宣称支持:
- null 或重复的系统维度条目、所有非法维度值变体;
- 完整 transformer 执行、中心化源元数据、崩溃恢复、发布失败注入;
- 组级 100 过滤器上限的替换、与其他栈资源的冲突;
- 维度通配引用、带字面星号的属性名(
metric-filter-reference-validation-aws.json的unmeasured字段)。
十、证据文件索引
| 文件 | 内容 |
|---|---|
| docs/services/cloudwatch-metric-filters-verification.md | 契约验证记录本体:三档标签、观测环境、行为全集、未测量边界 |
| tools/aws-capture/cloudwatchlogs/capture_filter_patterns.py | 148 例合成模式语料的 opt-in 捕获脚本(base/edges/boundaries 三套件) |
| src/test/resources/cloudwatchlogs/metric-filter-publishing-aws.json | 有状态发布观测的规范化记录(默认值、提取、普通维度、系统维度) |
| src/test/resources/cloudwatchlogs/metric-filter-default-value-wire-aws.json | 原始签名请求级别:字符串"7"→ 400SerializationException;数值 7 → 200 |
| src/test/resources/cloudwatchlogs/metric-filter-reference-validation-aws.json | 四项变换引用校验观测(未提及标量接受、通配拒绝) |
| src/test/resources/cloudwatchlogs/metric-filter-cfn-aws.json | CloudFormation 身份、替换顺序、回滚、Retain 策略观测 |
| compatibility-tests/sdk-test-python/tests/metric_filter_replay.py | 有状态回放脚本(--aws/--endpoint双模式、所有权保护、收敛轮询) |
| compatibility-tests/sdk-test-python/tests/test_metric_filter_replay.py | 离线校验(禁网、参数守卫、输出脱敏、场景矩阵断言) |
| src/test/java/io/github/hectorvent/floci/services/cloudwatch/logs/CloudWatchLogsMetricFilterPublishingTest.java | Floci 端消费 fixture 的发布行为测试 |
| src/main/java/io/github/hectorvent/floci/services/cloudwatch/logs/CloudWatchLogsMetricFilterService.java | 指标过滤器服务:Put/Describe/Delete/Test 与异步发布队列 |
| src/main/java/io/github/hectorvent/floci/services/cloudwatch/logs/MetricFilterRules.java | 系统字段、选择标准、维度上限校验 |
| src/main/java/io/github/hectorvent/floci/services/cloudformation/provisioners/LogsMetricFilterCfnProvisioner.java | CFN 身份、替换、回滚与所有权快照 |
十一、参考来源
文档所列依据(均为 AWS 官方资料,此处按名称列出,便于对照):
- PutMetricFilter API 参考;
- PutSubscriptionFilter API 参考(用于共享正则配额);
- TestMetricFilter API 参考;
- Filter and pattern syntax(过滤与模式语法指南);
- Metric-filter values and dimensions(指标过滤器值与维度指南);
- Metric-filter concepts(指标过滤器概念);
- CloudFormation MetricFilter 参考;
- CloudFormation registry schemas(
delete_then_create替换策略的来源)。
结语
这份契约验证文档的价值在于边界清晰:哪些行为有 AWS 实测背书、哪些只是文档支撑、哪些完全未测量,都被显式记录并固化为可消费的 JSON fixture。Floci 的实现层(服务常量、配额校验、发布队列、CFN provisioner)与测试层(Java/Python 双端 fixture 消费)都围绕这份契约展开,形成"真实 AWS 观测 → 规范化证据 → 模拟实现 → 兼容性验证"的闭环。对于希望在模拟器上获得高保真 CloudWatch Logs 指标行为的开发者,这份文档与其配套脚本、fixture 是理解行为边界与复现验证流程的最佳起点。
【免费下载链接】flociLight, fluffy, and always free - The AWS Local Emulator alternative项目地址: https://gitcode.com/gh_mirrors/fl/floci
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考