使用 Loki、Node.js 和 Fastify.js 进行日志监控
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
你好👋
在过去的几个月里,我花了很多时间使用Loki在Grafana上为MyUnisoft(我工作的公司)创建仪表板。
所以我决定写一篇文章,从后端 Node.js开发人员的角度来解释我的经历。
👀 为什么是格拉法纳和洛基?
简单介绍一下背景,这个工具是一位两年前离职的DevOps员工部署的。所以,我其实并没有自己选择这个工具。我对这两个工具之前都没有使用经验,只能边摸索边尝试。
Grafana 相当知名且成熟,所以我并不担心。另一方面,我很快就喜欢上了 Loki,它能很好地管理和搜索我的日志。于是我心想:“我得学习如何用它们来制作仪表盘。”
📃 写好日志
💬 一些日志示例采用多行显示,以避免滚动并简化阅读。
编写日志时,必须不断思考它们将提供哪些信息,并确保它们能够被轻松分析😵。通常需要多次迭代才能获得理想的结果。
根据我的经验,日志可以分为两种:
- 你知道,调试日志对你的仪表盘来说毫无用处。它们只有在出现错误或你需要了解特定请求的更多上下文信息时才有用。
- 所有其他能够为仪表盘(以及供人阅读)提供可利用数据的日志
以下是服务启动时打印的调试日志示例(对自定义--max-old-space-size标志非常有用)。
const availableHeapSize = prettyBytes(
v8.getHeapStatistics().total_available_size
);
server.log.info(`Total available heap size: ${availableHeapSize}`);
另一个常见的例子是记录 JSON 有效负载(当然,你可以利用这些数据,但这通常不是最终目的)。
this.entry.once("payloadReady", (payload) => {
this.log("payload before publishing:");
this.log(JSON.stringify(payload, null, 2));
});
😱 务必对数据进行编辑(您可能会在不知不觉中泄露机密信息)。
大多数情况下,你无法从这些日志中获取任何有价值的信息,而且它们也不需要任何特别的分析。在本文中,我将重点介绍我们可以利用到仪表盘中的日志。
🐤 实际示例
以下是我们最初为文档上传中间件/插件设置的错误日志示例。
Successfully uploaded '...'
//
Failed to upload '...'
存在以下几个问题:
- 没有范围(我们无法轻松搜索与我们的中间件相关的所有日志)。
- 可以很容易地与其他日志(来自其他中间件或服务)混合在一起。
- 解析状态(失败或成功)可能相当具有挑战性。
- 缺少关于请求或用户的信息。
更好的写法是:
[uploader|req-x5d4] document 'name.jpg' uploaded (state: ok)
您可以轻松地在日志中添加其他信息,例如扩展名、大小、运行时间等(而不会破坏 Loki 模式或正则表达式)。
(state: ok|ext: .jpg|size: 52.5 kB|upload-time: 0.503 ms)
⚠️ 记录大小(字节)或时间(毫秒)时,务必使用相同的单位。在 Grafana 中设置正确的“单位”会自动清理数值。
📜 格式说明
一个好的日志必须结构合理,才能方便使用 LogQL 进行搜索。以下是一个反例,在这种情况下,提取信息会非常困难。
hello-world.jpg 52.5 kB|0.503 ms
我建议在每个值前添加标签(这样也更容易阅读)。添加起始符、结束符和分隔符也很有帮助。
doc 'hello-world.jpg' [size: 52.5 kB|exec: 0.503 ms]
这里是使用 Loki 解析所有标签所需的正则表达式
doc '(?P<doc>\S+)' \[size: (?P<size>\S+) kB|exec: (?P<exec>\S+) ms\]
Loki 2.0 还允许您使用名为 pattern 的简化语法检索标签。
pattern `doc '<doc>' [size: <size> kB|exec: <exec> ms]`
🔎 在框架/代码中实现
⚠️ 代码示例已简化/截断
Fastify 框架默认包含Pino日志记录器(一个功能强大且性能优异的日志记录器)。该框架本身也支持许多非常酷的功能,例如在运行时控制日志级别。
我的团队选择自定义默认的请求和响应日志,以包含更多信息。要实现这一点,您需要:
- 设置 Fastify 构造函数选项disableRequestLogging为
true - 利用两个钩子(onRequest 和 onResponse)。
server.decorateRequest("standardLog", null);
server.addHook("onRequest", async(request: FastifyRequest) => {
request.log.info(
`(${request.id}) receiving request ...`
);
request.standardLog = standardLog.bind(request);
});
server.addHook("onResponse", async(request: FastifyRequest) => {
request.log.info(
request.standardLog(
`response returned "${request.method} ${request.raw.url}"
)
);
});
standardLog 装饰器允许我们显示有关请求和正在使用的令牌的信息(对于经过身份验证的端点)。
我们需要处理几种类型的令牌(这完全取决于 API 的使用者,例如普通用户或通过我们的合作伙伴 API 使用的合作伙伴)。
它们各自包含:
- 客户的 PostgreSQL 模式
- 用户或第三方的ID
- 会计文件夹(可以为空)
function standardLog(
this: FastifyRequest,
msg: string
): string {
if (
this.server.hasRequestDecorator("tokenInfo") &&
this.tokenInfo !== null) {
const { tokenInfo } = this;
let tokenInfoLog = `${tokenInfo.type}`;
switch (tokenInfo.type) {
case "api":
tokenInfoLog += `|s:${...}|t:${...}|acf:${...}`;
break;
case "user":
tokenInfoLog += `|s:${...}|p:${...}|acf:${...}`;
break;
default:
tokenInfoLog = "";
break;
}
return `(${this.id}|${tokenInfoLog}) ${msg}`;
}
return `(${this.id}|none) ${msg}`;
}
以下是日志中的内容
(req-1rti) receiving request "POST /ged/base-docs/docs"
(req-1rti|user|s:538|p:1|acf:23) response returned
"POST /ged/base-docs/docs", statusCode: 201 (460.106ms)
这些信息对于在我们的仪表盘中填充有关客户和行动范围的信息至关重要。
🌀 日志摄取
我的团队目前使用promtail和 YML 配置来检索服务日志。
server:
http_listen_port: 9080
positions:
filename: /var/lib/promtail/positions.yml
clients:
- url: https://xxx.fr/loki/api/v1/push
scrape_configs:
- job_name: svc_api_dev
static_configs:
- labels:
__path__: /home/xxx/logs/service-dev-*.log
app: api
env: dev
host: xxx.fr
job: svc_api_dev
targets:
- localhost
我不会在本章上花费太多时间,因为你可以在网上找到很多关于如何设置和配置 promtail 的教程。
💡 如果您不想依赖 promtail 将日志发送到 Loki,可以使用 fastify 插件pino-loki。
📊 OCR 控制面板
MyUnisoft 是一款法国会计软件,因此光学字符识别是我们软件的一项重要功能。监控对于我们适应客户需求和及时响应突发事件至关重要。
👀😍 我们赢得了由 Le Monde du Chiffre 颁发的2023 年 OCR 银奖。
以下是一个我们仅使用以下日志构建的仪表板示例:
(req-2987|user|s:24|p:5710|acf:7398) OCR xxx.jpg
[type: invoice|ext: .jpg|size: 2925.73 kB]
按标签分组
我们可以使用以下 logQL 构建上述图。注意扩展求和。
sum(
count_over_time(
{app="ocr",env="$env"}
|= "OCR"
|= "|ext:"
| regexp `\|ext: \.(?P<extension>\S+)\|`
[$__range])
) by (extension)
正则表达式是用 Go 语言语法构建的(我个人使用regex101.com来测试它们)。在这里,它可以用来提取/检测
extension标签。
我们可以使用语法$env注入仪表板变量(在这里,我只需在 select 中移动一个值,即可在生产环境、测试环境或开发环境中查看我的仪表板)。
我还使用了内置$__range变量,它可以加载 Grafana 中当前选定范围内的数据。
拆开包装
我花了很长时间才真正理解 unwrap 函数。网上几乎没有任何清晰的文档或解释!
这里会使用标签并提取所有值,然后使用相关函数size进行计算。这对于提取数值(例如计数器、执行时间等)非常有用。min_over_timemin
min(
min_over_time(
{app="ocr",env="$env"}
|= "OCR"
| regexp `\|size: (?P<size>[.0-9]+) kB\]`
| unwrap size
| __error__ = ""
[$__range])
) by (app)
💬
| __error__ = ""避免因意外错误导致 Loki 崩溃(如果由于任何原因导致大小解包失败,则可能会发生这种情况)。
然后,在统计图的右侧,我们选择相应的单位。
💡 小贴士和技巧
显示名称
很长一段时间以来,我的图表中存在一些非常糟糕的原始标签(格式类似于 JSON)。
您可以通过编辑该display name选项进行自定义。您可以在此字段中使用变量直接检索标签值。
无数据
有时,由于在所选范围内未检测到任何日志,图表会显示“无数据”。这在某些图形(例如仪表盘)中可能会造成问题。
但也许你更希望是零。没问题,只需选择该No value选项即可。
使用正则表达式和变量进行过滤
在某些仪表盘中,您可能需要根据多个条件动态筛选数据。一种方法是使用正则表达式和仪表盘变量。
这是我在其中一个仪表盘中用来筛选一个或多个合作伙伴结果的方法。
count(
sum(
count_over_time(
{app="api",env="production"}
|= "] CALL"
|= "$endpoint"
|~ `\]\[$thirdparty\]`
| regexp `\((?P<schemaId>[0-9]+):(?P<folderId>[0-9]+)\)`
[$__range])
) by (schemaId, folderId)
)
实现此功能的代码行是这样的:你需要使用反引号语法才能在其中注入变量。
|~ `\]\[$thirdparty\]`
🚀 数据是成功的关键
作为开发者,我们往往忽略了监控及其生成的数据对于改进工作的强大作用。MyUnisoft 与一百多家合作伙伴携手共进。
了解他们如何使用我们的 API 以及他们遇到的各种异常情况,对于我们继续实现指数级增长至关重要。
这使我们能够不断改进,从而为我们的会计客户和维护集成的开发合作伙伴提供更好的体验。
我们能够将这些数据转化为实际应用并加以利用,这已成为我们团队的核心专长之一。看到简单的日志就能带来如此多的成果,真是令人兴奋。
👋 结论
我很高兴终于完成了这篇文章(希望它对您有所帮助)。非常感谢我的团队,尤其要感谢 Cédric,没有他,这一切都不可能实现。
我仍然不太会使用 Grafana 的很多功能,比如数据转换和警报,但我会继续深入研究和改进。
🙏感谢阅读🙏
文章来源:https://dev.to/myunisoft/logs-monitoring-with-loki-nodejs-and-fastifyjs-3h8k











