caddy日志工作原理
Caddy 具有强大且灵活的日志功能,但这些功能可能与你之前使用的功能有所不同,尤其是如果你是从传统的共享主机或其他遗留 Web 服务器迁移过来的。
概述
日志有两个主要方面:产生
和消费
。
产生
指的是生成消息,它包含三个步骤:
-
收集相关信息(上下文)。
-
构建有用的消息表示(编码)。
-
将该表示发送到输出(写入)。
这些功能已经嵌入到 Caddy 的核心中,使得 Caddy 代码库的任何部分或模块(插件)都可以生成日志。
消费
则是接收和处理消息。为了发挥作用,生成的日志必须被消费。仅仅写入但从未读取的日志是没有价值的。消费日志可以简单到管理员查看控制台输出,也可以复杂到连接日志聚合工具或云服务来过滤、统计和索引日志消息。
Caddy 的角色
Caddy 是一个日志生成器
。它不会消费日志,除非是为了编码和写入日志所需的最低限度处理。这一点很重要,因为它保持了 Caddy 核心的简洁性,减少了漏洞和边缘情况,同时减轻了维护负担。最终,日志处理不在 Caddy 核心的范围内。
然而,开发一个消费日志的 Caddy 应用模块是有可能的(据我们所知,目前还没有)。
结构化日志
与大多数现代应用程序一样,Caddy 的日志是结构化的
。这意味着消息中的信息不仅仅是不透明的字符串或字节序列,而是以强类型的形式存在,并通过单独的字段名
进行键入,直到需要编码消息并将其写入时为止。
与传统的非结构化日志(例如传统的 HTTP 服务器中常用的旧式通用日志格式 CLF)进行比较:
1 | 127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.1" 200 2326 |
{
“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”]
}
}
1 |
|
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 的遗留软件,并且需要根据特定字段(例如主机名)将日志分成不同的文件,你可以使用或编写一个简单的工具来读取 JSON
,调用 sprintf()
创建 CLF 字符串,然后根据 request.host
字段中的值将其写入文件。
Caddy 的日志功能还可以用于实现指标和跟踪:指标基本上是统计具有某些特征的消息,而跟踪则是根据它们之间的共同点将多条消息链接在一起。
通过消费 Caddy 的日志,你可以实现无数的可能性!
