日志如何工作
Caddy 具有强大而灵活的日志记录功能,但它们可能与您习惯的不同,特别是如果您来自更陈旧的共享主机或其他遗留 Web 服务器。
概述
日志记录主要有两个方面:发射和消费。
发射 意味着生成消息。它包含三个步骤
- 收集相关信息(上下文)
- 构建有用的表示形式(编码)
- 将该表示形式发送到输出(写入)
此功能内置于 Caddy 的核心中,使 Caddy 代码库或模块(插件)的任何部分都能够发射日志。
消费 是消息的摄取和处理。为了有用,发射的日志必须被消费。仅仅写入但从未读取的日志无法提供任何价值。消费日志可以像管理员读取控制台输出一样简单,也可以像连接日志聚合工具或云服务来过滤、计数和索引日志消息一样高级。
Caddy 的角色
Caddy 是一个日志发射器。它不消费日志,除了编码和写入日志所需的最小处理。这很重要,因为它保持 Caddy 的核心更简单,从而减少错误和边缘情况,同时减轻维护负担。最终,日志处理超出了 Caddy 核心的范围。
但是,总是存在 Caddy 应用程序模块消费日志的可能性。(据我们所知,它尚不存在。)
结构化日志
与大多数现代应用程序一样,Caddy 的日志是结构化的。这意味着消息中的信息不仅仅是不透明的字符串或字节切片。相反,数据保持强类型,并按各个字段名称键控,直到需要对消息进行编码并将其写出。
比较传统的非结构化日志——例如传统的 HTTP 服务器常用的陈旧的通用日志格式 (CLF)
127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.1" 200 2326
这种格式“具有结构”但不是“结构化的”:它只能用于记录 HTTP 请求。没有(有效)方法以不同的方式对其进行编码,因为它是不透明的字节字符串。它也缺少很多信息。它甚至不包括请求的 Host 标头!这种日志格式仅在托管单个站点时有用,并且仅用于获取有关请求的最基本信息。
现在比较来自 Caddy 的等效结构化日志消息,编码为 JSON 并格式化良好以进行显示
{
"level": "info",
"ts": 1646861401.5241024,
"logger": "http.log.access",
"msg": "handled request",
"request": {
"remote_ip": "127.0.0.1",
"remote_port": "41342",
"client_ip": "127.0.0.1",
"proto": "HTTP/2.0",
"method": "GET",
"host": "localhost",
"uri": "/",
"headers": {
"User-Agent": ["curl/7.82.0"],
"Accept": ["*/*"],
"Accept-Encoding": ["gzip, deflate, br"],
},
"tls": {
"resumed": false,
"version": 772,
"cipher_suite": 4865,
"proto": "h2",
"server_name": "example.com"
}
},
"bytes_read": 0,
"user_id": "",
"duration": 0.000929675,
"size": 10900,
"status": 200,
"resp_headers": {
"Server": ["Caddy"],
"Content-Encoding": ["gzip"],
"Content-Type": ["text/html; charset=utf-8"],
"Vary": ["Accept-Encoding"]
}
}
您可以看到结构化日志更有用,并且包含更多信息。此日志消息中丰富的信息不仅有用,而且几乎没有性能开销:Caddy 的日志是零分配的。结构化日志对数据类型或上下文没有限制:它们可以在任何代码路径中使用,并包含任何类型的信息。
由于日志是结构化的和强类型的,因此可以将它们编码为任何格式。因此,如果您不想使用 JSON,则可以将日志编码为任何其他表示形式。Caddy 通过 日志编码器模块 支持其他格式,甚至可以添加更多格式。
最重要的是在结构化日志和遗留格式之间的区别中,在性能损失的情况下,结构化日志 可以转换为遗留的通用日志格式 ,但反之则不然。从 CLF 转换为结构化格式是非平凡的(或至少效率低下),并且考虑到信息的缺乏,这是不可能的。
本质上,高效、结构化的日志记录通常提倡以下理念
- 日志太多比太少好
- 过滤比丢弃好
- 延迟编码以获得更大的灵活性和互操作性
发射
在代码中,日志发射类似于以下内容
logger.Debug("proxy roundtrip",
zap.String("upstream", di.Upstream.String()),
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}),
zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)),
zap.Duration("duration", duration),
zap.Int("status", res.StatusCode),
)
您可以看到,这个函数调用包含日志级别、消息和几个数据字段。所有这些都是强类型的,Caddy 使用零分配日志记录库,因此日志发射快速高效,几乎没有开销。
logger
变量是一个 zap.Logger
,它可能具有与其关联的任何数量的上下文,其中包括名称和数据字段。这使得记录器可以很好地“继承”自父上下文,从而实现高级跟踪和指标。
从那里,消息通过高效的处理管道发送,在那里进行编码和写入。
日志记录管道
如您在上面看到的,消息由记录器发射。然后将消息发送到 日志 进行处理。
Caddy 允许您 配置多个日志,这些日志可以处理消息。日志由编码器、写入器、最小级别、采样率以及要包含或排除的记录器列表组成。在 Caddy 中,始终存在名为 default
的默认日志。您可以通过在配置中的 此对象 中指定键为 "default"
的日志来自定义它。
- 编码器: 日志的格式。将内存中的数据表示形式转换为字节切片。编码器可以访问日志消息的所有字段。
- 写入器: 日志输出。可以是任何日志写入器模块,例如写入文件或网络套接字。它只是写入字节。
- 级别: 日志具有不同的级别,从 DEBUG 到 FATAL。级别低于指定级别的消息将被日志忽略。
- 采样: 极热路径可能会发射比有效处理更多的日志;启用采样是一种减少负载的方法,同时仍然产生具有代表性的消息样本。
- 包含/排除: 每条消息都由一个记录器发射,该记录器具有一个名称(通常从模块 ID 派生)。日志可以包含或排除来自某些记录器的消息。
当从 Caddy 发射日志消息时
- 将检查始发记录器的名称与每个日志的包含/排除列表;如果包含(或未排除),则将其添加到该日志中。
- 如果启用了采样,则快速计算确定是否保留日志消息。
- 使用日志的配置编码器对消息进行编码。
- 然后将编码的字节写入日志的配置写入器。
默认情况下,所有消息都将发送到所有配置的日志。这符合上面描述的结构化日志记录的值。您可以通过设置日志的包含/排除列表来限制哪些消息发送到哪些日志,但这主要用于过滤来自不同模块的消息;它并非旨在像日志聚合服务一样使用。为了保持 Caddy 的日志记录管道精简高效,日志消息的高级处理被推迟到消费。
消费
将消息发送到输出后,消费者将读取它们,解析它们,并相应地处理它们。
这是一个与发射日志非常不同的问题领域,Caddy 的核心不处理消费(尽管 Caddy 应用程序模块当然可以)。您可以使用许多工具来处理 JSON 消息(或其他格式)流,并查看、过滤、索引和查询日志。您甚至可以编写或实现自己的工具。
例如,如果您运行需要 CLF 的旧版软件,该 CLF 基于特定字段(例如主机名)分隔到不同的文件中,则可以使用或编写一个简单的工具,该工具读取 JSON,调用 sprintf()
以创建 CLF 字符串,然后将其写入基于 request.host
字段值的文件。
Caddy 的日志记录工具也可用于实现指标和跟踪:指标基本上计算具有某些特征的消息,而跟踪根据多个消息之间的共性将它们链接在一起。
通过消费 Caddy 的日志,您可以做无数的事情!