设计日志格式并单独收集错误
目标
依次设计访问日志的格式、形态、sink 和过滤器四个方面,并确认它们各自在实际日志文件中如何呈现。
为什么重要
在可观测性配置中,访问日志是唯一无法回溯的部分。事故发生之后才意识到“上游地址也该记下来”,那次事故的日志里也已经没有了。所以选择格式的标准不是喜好,而是一个问题——凌晨三点,仅凭这一行能不能缩小原因范围。此外,文件 sink 会先累积到缓冲区再清空,没有值的位置会打印什么,过滤器应该写在哪一层,这些光读文档很难记住,亲身经历一次才能留下印象。
步骤
- 启动两个上游——
8091为ok,8092为fail。在/root/envd-log/log-text.yaml中放置/ping(direct_responsepong)、/bad(集群bad)、/(集群good)三条路由,以及文件访问日志 sink——路径为/root/envd-log/access.log,格式为%RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%。加上--file-flush-interval-msec 200启动,请求一次/hello后,等到日志出现再确认。 - 创建
/root/envd-log/log-wide.yaml——日志路径为/root/envd-log/wide.log,格式为八个字段:%RESPONSE_CODE%|%REQ(:METHOD)%|%REQ(:PATH)%|%PROTOCOL%|%DURATION%|%UPSTREAM_HOST%|%RESPONSE_FLAGS%|%BYTES_SENT%。用该配置启动后,分别请求一次/hello、/ping、/bad/x,确认留下三行。 - 创建
/root/envd-log/log-miss.yaml——路径为/root/envd-log/miss.log,格式为%RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%|%REQ(X-TENANT)%(最后一个字段是请求头x-tenant)。启动后,不带头各请求一次/ping和/hello,在/root/envd-log/03-missing.txt中写入ping_upstream=(/ping那一行的第三个字段)、ping_tenant=(第四个字段)、hello_tenant=三行。 - 创建
/root/envd-log/log-json.yaml——路径为/root/envd-log/access.json,格式为json_format,键为code、path、upstream、flags、duration_ms五个。启动后,各请求一次/hello和/bad/x,并确认能用jq读取。 - 创建
/root/envd-log/log-err.yaml——只放一个 sink,用filter.status_code_filter只保留 500 及以上(比较运算符GE,值 500),路径为/root/envd-log/errors.json,键为code、path、upstream、flags。启动后,请求两次/hello和一次/bad/x,在/root/envd-log/05-filter.txt中写入lines=(errors.json 的行数)和codes=(这些行的 code 值,用空格分隔)两行。 - 创建
/root/envd-log/log-two.yaml——两个 sink。一个用文本把所有请求记录到/root/envd-log/all.log(格式为%RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%),另一个用 JSON 只把 500 及以上记录到/root/envd-log/only5xx.json。启动后,请求三次/hello和两次/bad/x,在/root/envd-log/06-two.txt中写入all=、err=、ratio_ok=(all 与 err 的差)三行。 - 创建
/root/envd-log/log-reqid.yaml——路径为/root/envd-log/reqid.log,格式为%RESPONSE_CODE%|%REQ(:PATH)%|%REQ(X-REQUEST-ID)%。启动后,请求/given时附加头x-request-id: envd-777,请求/none时不附加任何头。在/root/envd-log/07-reqid.txt中写入given=(第一个请求那一行的第三个字段)、none_len=(第二个请求那一行第三个字段的字符数)、none_is_given=(该值与 envd-777 相同则为 yes,否则为 no)三行。 - 在
/root/envd-log/08-report.md中写入fields=(第 2 步格式的字段数)、missing_marker=(第 3 步中没有值时打印的字符)、all_lines=、error_lines=(第 6 步的两个值)四行,并在下面至少写四行学到的内容。
参考
- 启动 Envoy 时请同时加上
--file-flush-interval-msec 200。默认值是 10 秒,所以请求之后马上看,日志似乎是空的。 - 即便如此,也不要使用固定的
sleep,而要使用循环一直等到줄 수가 N 이상이 될 때까지(韩文,意为“行数达到 N 以上”)。 - 重新启动前,请用
pkill -x envoy清理,并用循环等待启动,直到/ready返回 LIVE。 - 统计或提取字段时使用
awk -F'|',确认 JSON 时使用jq -c .。 - Envoy 会向日志文件追加写入。更改格式并重新启动之前,请先删除该文件——否则旧格式的行和新格式的行会混在同一个文件里。
- 常见错误——把 sink 的
filter写在typed_config里面。那里没有这个字段,整个配置会被拒绝。应该写在与name相同的层。
把日志保存为文件,并了解它为什么出现得晚
启动两个上游——8091 为 ok,8092 为 fail。在 /root/envd-log/log-text.yaml 中放置 /ping(direct_response pong)、/bad(集群 bad)、/(集群 good)三条路由,以及文件访问日志 sink——路径为 /root/envd-log/access.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%。加上 --file-flush-interval-msec 200 启动,请求一次 /hello 后,等到日志出现再确认。
文件 sink 不会在每个请求时都写入磁盘——它先在缓冲区中累积,再定期清空。这个周期的默认值是 10 秒,所以请求之后立刻 cat,看起来什么都没有。“日志没有留下来”的报告,相当一部分就是这个原因。在实验中,用 --file-flush-interval-msec 200 缩短周期,消除等待时间。即便如此,也请使用循环一直等到出现行,而不是固定的 sleep。
事先确定事故之后需要的值
创建 /root/envd-log/log-wide.yaml——日志路径为 /root/envd-log/wide.log,格式为八个字段:%RESPONSE_CODE%|%REQ(:METHOD)%|%REQ(:PATH)%|%PROTOCOL%|%DURATION%|%UPSTREAM_HOST%|%RESPONSE_FLAGS%|%BYTES_SENT%。用该配置启动后,分别请求一次 /hello、/ping、/bad/x,确认留下三行。
格式字符串中的 %...% 是命令操作符。请求头用 %REQ(이름)%(占位符为头名称),响应头用 %RESP(이름)%(占位符为头名称),像 :METHOD 和 :PATH 这样以冒号开头的是 HTTP/2 的伪头。选择放入哪些值的标准只有一个——凌晨三点仅凭这一行,能不能缩小原因范围。只有状态码,无法缩小范围;必须有哪个上游接收的、花了多少毫秒、附加了什么标记,才能缩小。
了解没有值时会打印什么
创建 /root/envd-log/log-miss.yaml——路径为 /root/envd-log/miss.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%|%REQ(X-TENANT)%(最后一个字段是请求头 x-tenant)。启动后,不带头各请求一次 /ping 和 /hello,在 /root/envd-log/03-missing.txt 中写入 ping_upstream=(/ping 那一行的第三个字段)、ping_tenant=(第四个字段)、hello_tenant= 三行。
/ping 是 direct_response,不会去往上游——那么 %UPSTREAM_HOST% 就没有可填的值。没有发送的头也一样。Envoy 不会把这样的位置留空,而是用只有一个字符的标记填充。如果不先了解这个标记是什么,用机器读取日志时,就会把它当成真实的值直接存储下来。数字段数时,awk -F'|' 很方便。
改成机器可读的格式
创建 /root/envd-log/log-json.yaml——路径为 /root/envd-log/access.json,格式为 json_format,键为 code、path、upstream、flags、duration_ms 五个。启动后,各请求一次 /hello 和 /bad/x,并确认能用 jq 读取。
用分隔符切分的文本格式便于人阅读,但只要值里出现分隔符(路径中带有问号和值的情况),字段就会错位。如果打算把日志发送到采集器并进行查询,一开始就用 json_format 更好。键的名称可以随意定,值的位置使用同样的命令操作符。确认用 jq -c . 파일(占位符为文件名)——哪怕只有一行损坏,jq 也会当场失败。
把错误单独收集起来
创建 /root/envd-log/log-err.yaml——只放一个 sink,用 filter.status_code_filter 只保留 500 及以上(比较运算符 GE,值 500),路径为 /root/envd-log/errors.json,键为 code、path、upstream、flags。启动后,请求两次 /hello 和一次 /bad/x,在 /root/envd-log/05-filter.txt 中写入 lines=(errors.json 的行数)和 codes=(这些行的 code 值,用空格分隔)两行。
每个 sink 都可以设置 filter。filter 写在与 name 相同的层,而不是 typed_config 里面——写在里面会提示“没有这样的字段”,配置会被拒绝。过滤器除了状态码,还有耗时(duration_filter)、响应标记、头条件。把错误单独收集起来,即使保留期设得很长,容量也能承受。
同时使用两个 sink
创建 /root/envd-log/log-two.yaml——两个 sink。一个用文本把所有请求记录到 /root/envd-log/all.log(格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%UPSTREAM_HOST%),另一个用 JSON 只把 500 及以上记录到 /root/envd-log/only5xx.json。启动后,请求三次 /hello 和两次 /bad/x,在 /root/envd-log/06-two.txt 中写入 all=、err=、ratio_ok=(all 与 err 的差)三行。
sink 是一个列表,可以放置多个,并且每个都有各自的格式和过滤器。实际工作中常见的配置就是这样——全量日志用简短的格式短期保留,错误日志用详细的格式长期保留。把两者混在一个文件里,就无法分别设定保留期限。
请求 ID 由谁生成
创建 /root/envd-log/log-reqid.yaml——路径为 /root/envd-log/reqid.log,格式为 %RESPONSE_CODE%|%REQ(:PATH)%|%REQ(X-REQUEST-ID)%。启动后,请求 /given 时附加头 x-request-id: envd-777,请求 /none 时不附加任何头。在 /root/envd-log/07-reqid.txt 中写入 given=(第一个请求那一行的第三个字段)、none_len=(第二个请求那一行第三个字段的字符数)、none_is_given=(该值与 envd-777 相同则为 yes,否则为 no)三行。
请求 ID 是把经过多个服务的同一个请求串起来的线,所以最前面的代理在没有值时会生成,有值时则原样传递。默认行为是直接使用客户端给出的值,因此也要记住,外部可以随意填入任何值发送进来(对于来自信任边界之外的值,有专门的配置可以将其删除)。字符数用 awk '{print length($0)}' 或 wc -c 统计。
留下日志设计备忘
在 /root/envd-log/08-report.md 中写入 fields=(第 2 步格式的字段数)、missing_marker=(第 3 步中没有值时打印的字符)、all_lines=、error_lines=(第 6 步的两个值)四行,并在下面至少写四行学到的内容。
这份备忘是下次确定某个服务的日志格式时你自己要读的文字。与其写“放了什么”,不如写“为什么决定放它”。值取自前面步骤创建的文件。