文档
一个 项目

日志工作原理

Caddy 拥有强大而灵活的日志功能,但它们可能与您习惯的不同,特别是如果您来自更古老的共享主机或其他传统 Web 服务器。

概述

日志记录有两个主要方面:发送和消费。

发送意味着产生消息。它包含三个步骤:

  1. 收集相关信息(上下文)
  2. 构建有用的表示(编码)
  3. 将该表示发送到输出(写入)

此功能已内置到 Caddy 的核心中,使 Caddy 代码库或模块(插件)的任何部分都能发送日志。

消费是消息的接收和处理。为了使日志有用,发送的日志必须被消费。仅写入但从未读取的日志没有价值。消费日志可以像管理员阅读控制台输出一样简单,也可以像附加日志聚合工具或云服务来过滤、计数和索引日志消息一样高级。

Caddy 的角色

Caddy 是一个日志发送器。除了编码和写入日志所需的最小处理外,它不消费日志。这很重要,因为它使 Caddy 的核心更简单,导致更少的错误和边缘情况,同时减少维护负担。最终,日志处理超出了 Caddy 核心的范围。

但是,始终存在 Caddy 应用模块消费日志的可能性。(据我们所知,目前还不存在这样的模块。)

结构化日志

与大多数现代应用程序一样,Caddy 的日志是_结构化的_。这意味着消息中的信息不仅仅是简单的字符串或字节切片。相反,数据保持强类型,并由单独的_字段名_键控,直到需要编码消息并写出。

比较传统非结构化日志(如传统的通用日志格式 (CLF))与传统的 HTTP 服务器一起使用:

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 分离到不同文件的传统软件,您可以使用或编写一个简单的工具,读取 JSON,调用 sprintf() 创建 CLF 字符串,然后根据 request.host 字段中的值将其写入文件。

Caddy 的日志功能也可以用于实现指标和跟踪:指标基本上计算具有特定特征的消息,而跟踪基于它们之间的共同点链接多个消息。

通过消费 Caddy 的日志,您可以做无数种事情!