v2中文文档
项目

日志如何工作

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": 1585597114.7687502,
	"logger": "http.log.access",
	"msg": "handled request",
	"request": {
		"method": "GET",
		"uri": "/",
		"proto": "HTTP/2.0",
		"remote_addr": "127.0.0.1:50876",
		"host": "example.com",
		"headers": {
			"User-Agent": [
				"curl/7.64.1"
			],
			"Accept": [
				"*/*"
			]
		},
		"tls": {
			"resumed": false,
			"version": 771,
			"ciphersuite": 49196,
			"proto": "h2",
			"proto_mutual": true,
			"server_name": "example.com"
		}
	},
	"user_id": "",
	"duration": 0.000014711,
	"size": 2326,
	"status": 200,
	"resp_headers": {
		"Server": [
			"Caddy"
		],
		"Content-Type": ["text/html"]
	}
}

你可以看到结构化日志如何更有用并包含更多信息。此日志消息中的大量信息不仅有用,而且几乎没有性能开销:Caddy 的日志是零分配的。结构化日志对数据类型或上下文没有限制:它们可以用于任何代码路径并包含任何类型的信息。

因为日志是结构化的和强类型的,它们可以被编码成任何格式。因此,如果你不想使用 JSON,可以将日志编码为任何其他表示形式。Caddy 通过log编码器模块支持其他人,甚至可以添加更多。

最重要的是结构化日志和遗留格式之间的区别,结构化日志可以编码为通用日志格式(或其他任何格式!),但不能反过来。从 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,其中包括名称和数据字段。这允许记录器很好地从父上下文“继承”,从而启用高级跟踪和度量。

从那里,消息通过一个高效的处理管道发送,并在其中进行编码和写入。

日志管道

正如你在上面看到的,消息是由loggers生产的。然后将消息发送到logs进行处理。

Caddy允许你配置处理消息的多个日志。日志由编码器、写入器、最低级别、采样率和要包含或排除的记录器列表组成。在 Caddy 中,总是有一个名为default。你可以在配置文件的这个对象中指定一个以“default”作为键的日志来定义它。

  • Encoder: 日志的格式。将内存中的数据表示转换为字节切片。编码器可以访问日志消息的所有字段。
  • Writer: 日志输出。可以是任何日志写入器模块,例如文件或网络套接字。它只是写入字节。
  • Level: 日志有不同的级别,从 DEBUG 到 FATAL。低于指定级别的消息将被日志忽略。
  • Sampling: 非常热的路径可能会发出比有效处理更多的日志;启用采样是一种减少负载的方法,同时仍会产生具有代表性的消息样本。
  • Include/exclude: 每条消息都由一个记录器发出,它有一个名称(通常来自模块 ID)。日志可以包括或排除来自某些记录器的消息。

当从 Caddy 发出日志消息时:

  • 根据每个日志的包含/排除列表检查原始记录器的名称;如果包含(或不排除),则将其纳入该日志。
  • 如果启用了采样,则快速计算确定是否保留日志消息。
  • 消息使用日志的配置编码器进行编码。
  • 然后将编码的字节写入日志的配置写入器。

默认情况下,所有消息都会转到所有配置的日志。这符合上述结构化日志记录的值。你可以通过设置它们的包含/排除列表来限制哪些消息进入哪些日志,但这主要用于过滤来自不同模块的消息;它不打算像日志聚合服务一样使用。为了保持 Caddy 的日志流水线精简和高效,日志消息的高级处理被推迟到消费。

消耗

消息发送到输出后,消费者将读入、解析并相应地处理它们。

这是一个与发出日志非常不同的问题域,并且 Caddy 的核心不处理消耗(尽管 Caddy 应用程序模块当然可以)。你可以使用许多工具来处理 JSON 消息流(或其他格式)以及查看、过滤、索引和查询日志。你甚至可以编写或实现你自己的。

例如,如果你运行需要根据特定字段(例如主机名)将CLF分成不同文件的旧软件,你可以使用或编写一个简单的工具来读取 JSON,调用sprintf()以创建CLF字符串,然后将其写入基于request.host字段中的值的文件。

Caddy的日志记录工具也可用于实现度量和跟踪:度量基本上计算具有某些特征的消息,跟踪基于它们之间的共性将多条消息链接在一起。

你可以通过使用Caddy的日志来完成无数种可能性!