如何使用 OTel 和 Deno 在 Node.js 后端获取深度追踪数据
运行生产软件的一个重要方面是可观测性——监控日志、追踪和指标,以便您能快速识别并解决问题。在 Node.js 服务器中,添加可观测性并非易事。您必须添加插装代码、修改日志记录器并配置各种服务。如果您可以立即查看日志和追踪数据,而无需任何额外工作,那会怎么样?
在本文中,我们将展示如何通过在 Deno 中运行 Node.js 后端,从而在不进行任何代码更改的情况下,立即查看其正在执行的操作。Deno 现在可以运行 Node 程序并内置 OpenTelemetry 支持,这使得操作比以往任何时候都更容易。
Node 中的深度追踪
在我们使用 Deno 设置 OTel 之前,让我们先回顾一下如何在 Node 中设置它。
这是一个简单的应用程序(GitHub 源代码),它显示一个聊天框,接收您的输入并将其传递给 ChatGPT。
要跟着操作,请克隆此仓库并在 node-express
子目录中运行该应用程序。
目前,我们在单文件应用程序的关键点使用 console.log
node server.js
Server is running on http://localhost:8000
Serving the chat interface...
Prompt: Tell me about the last time you felt sad.
Sending request to OpenAI...
Received response from OpenAI
让我们向程序中添加 OpenTelemetry,并使用 Grafana 的开源 LGTM 堆栈来对其进行内省。由于本文不涉及从零开始设置 OTel 堆栈,我们将对其进行高层次的描述
- 在应用程序级别添加插装代码,以将日志和追踪数据发送到正确的端点。
- 运行 Grafana 的OpenTelemetry LGTM 堆栈,这可以通过一个简单的
docker run
命令完成。 - 设置关键环境变量以配置遥测数据的路由。
让我们使用新的环境变量启动服务器
OTEL_SERVICE_NAME=chat-app OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node server.js
然后,我们将启动 Grafana 的 OTel LGTM 堆栈
docker run --name lgtm -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti \
-v "$PWD"/lgtm/grafana:/data/grafana \
-v "$PWD"/lgtm/prometheus:/data/prometheus \
-v "$PWD"/lgtm/loki:/data/loki \
-e GF_PATHS_DATA=/data/grafana \
docker.io/grafana/otel-lgtm:0.8.1
现在,当您在 localhost:3000
打开 Grafana 时,点击“Explore”(探索),然后选择“Tempo”,您将看到追踪数据。
点击追踪会显示一个 POST
请求,这是该追踪中的一个跨度。点击“Logs for this span”(此跨度的日志)会显示与此请求相关的日志。
在 Node 中,设置 OTel 需要导入 8 个新的依赖项、额外 84 行插装代码,以及将 console.log
替换为配置为将日志输出发送到 OTel 端点的自定义日志记录器。
要准确查看将 OpenTelemetry 集成到我们的 Node 应用程序中所需的更改,请看差异对比
package.json
中添加依赖项
在 "main": "server.js",
"type": "module",
"scripts": {
- "start": "node server.js"
+ "start": "OTEL_SERVICE_NAME=chat-app OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 OTEL_LOG_LEVEL=error node server.js"
},
"dependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/auto-instrumentations-node": "^0.56.1",
+ "@opentelemetry/exporter-logs-otlp-http": "^0.57.2",
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2",
+ "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
+ "@opentelemetry/resources": "^1.30.1",
+ "@opentelemetry/sdk-node": "^0.57.2",
+ "@opentelemetry/semantic-conventions": "^1.30.0",
"dotenv": "^16.4.7",
"express": "^4.18.3"
}
server.js
中的 console.log
调用
替换 import dotenv from 'dotenv';
+import { logger } from './telemetry.js';
import express from 'express';
dotenv.config();
// Code...
// Middleware to parse JSON bodies
+app.use((req, _res, next) => {
+ logger.info("Incoming request", {
+ attributes: {
+ path: req.path,
+ method: req.method,
+ },
+ });
+ next();
+});
app.use(express.json());
// Serve static HTML
app.get("/", (_req, res) => {
- console.log("Serving the chat interface");
+ logger.info("Serving the chat interface");
res.send(`<!DOCTYPE html>
<html>
<head>
// Code...
app.post("/api/chat", async (req, res) => {
try {
const { prompt } = req.body;
- console.log("Prompt:", prompt);
+ logger.info(`Prompt: ${prompt}`);
// Add your hardcoded system prompt here
const systemPrompt =
"You are a helpful AI assistant. Please provide clear and concise responses.";
- console.log("Sending request to OpenAI...");
+ logger.info("Sending request to OpenAI...");
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
}),
});
- console.log("Received response from OpenAI");
+ logger.info("Received response from OpenAI");
const data = await response.json();
res.json({
response: data.choices[0].message.content,
});
} catch (error) {
- console.error("Error:", error);
+ logger.error(`Error: ${error}`);
res.status(500).json({
error: error.message,
});
// Code...
// Start the server
app.listen(port, () => {
- console.log(`Server is running on http://localhost:${port}`);
+ logger.info(`Server is running on http://localhost:${port}`);
});
+import process from 'process';
+import { NodeSDK } from '@opentelemetry/sdk-node';
+import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
+import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
+import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
+import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
+import {
+ LoggerProvider,
+ SimpleLogRecordProcessor,
+} from '@opentelemetry/sdk-logs';
+import { Resource } from '@opentelemetry/resources';
+import { trace, context } from '@opentelemetry/api';
+
+const resource = new Resource({
+ "service.name": process.env.OTEL_SERVICE_NAME || "chat-app",
+});
+
+const logExporter = new OTLPLogExporter({
+ url: "http://localhost:4318/v1/logs",
+});
+
+const loggerProvider = new LoggerProvider({
+ resource, // ✅ Attach the resource with the updated attribute
+});
+
+loggerProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(logExporter));
+
+const logger = loggerProvider.getLogger("chat-app-logger");
+
+const customLogger = {
+ info: (message, attributes = {}) => {
+ const activeSpan = trace.getSpan(context.active());
+ const traceId = activeSpan
+ ? activeSpan.spanContext().traceId
+ : "no-trace-id";
+ logger.emit({
+ traceId,
+ body: message,
+ severityText: "INFO",
+ attributes,
+ });
+ },
+ error: (message, attributes = {}) => {
+ const activeSpan = trace.getSpan(context.active());
+ const traceId = activeSpan
+ ? activeSpan.spanContext().traceId
+ : "no-trace-id";
+ logger.emit({
+ traceId,
+ body: message,
+ severityText: "ERROR",
+ attributes,
+ });
+ },
+};
+
+const sdk = new NodeSDK({
+ traceExporter: new OTLPTraceExporter({
+ url: "http://localhost:4318/v1/traces",
+ }),
+ metricExporter: new OTLPMetricExporter({
+ url: "http://localhost:4318/v1/metrics",
+ }),
+ instrumentations: [
+ new HttpInstrumentation(),
+ ],
+ logExporter,
+ resource,
+});
+
+// Start the SDK and handle the promise properly
+sdk.start();
+
+process.on("SIGTERM", () => {
+ sdk.shutdown()
+ .then(() => logger.info("Telemetry shutdown complete"))
+ .finally(() => process.exit(0));
+});
+
+export { customLogger as logger };
telemetry.js
的全部内容都是为了配置自动生成追踪、指标和日志,以及将遥测数据导出到 OTel 收集器端点而创建的。注意:如果您想直接探索这些文件,请查看“原始”的 Node 应用程序(不带 OTel)和相同的 Node 应用程序但带有 OTel 设置。
但是,如果有一种更简单的方法可以在不进行插装和配置步骤的情况下获取日志和追踪数据呢?
Deno 内置的 OTel 支持
Deno 在 2.2 版本中添加了内置 OTel 支持,允许我们通过一个命令启动 OTel 堆栈。它自动收集并导出来自 console.log
、fetch
和 Deno.serve()
的追踪、指标和日志。
使用 Deno 2.2+,您只需传递一些额外的环境变量和 --unstable-otel
标志,即可立即在 Grafana 中查看追踪和日志
OTEL_DENO=true OTEL_SERVICE_NAME=chat-app deno \
--unstable-otel -NRE --env-file server.js
注意: --unstable-otel
标志启用 Deno 内置的 OpenTelemetry,该功能仍处于实验阶段,可能会有所更改。
我们可以启动相同的OTel-LGTM 堆栈
docker run --name lgtm -p 3000:3000 -p 4317:4317 -p 4318:4318 --rm -ti \
-v "$PWD"/lgtm/grafana:/data/grafana \
-v "$PWD"/lgtm/prometheus:/data/prometheus \
-v "$PWD"/lgtm/loki:/data/loki \
-e GF_PATHS_DATA=/data/grafana \
docker.io/grafana/otel-lgtm:0.8.1
在稍微操作应用程序以生成一些日志后,当我们访问 localhost:3000
,点击“Explore”(探索),然后选择“Tempo”,我们就可以看到追踪数据了。
点击“Logs for this span”(此跨度的日志)会显示与同一 HTTP 请求相关的日志。
我们展开了最后一个 console.log
,可以看到 Deno 和 OTel 已经自动附加了元数据以帮助关联此日志。如果 JavaScript 代码中的日志发生在活动的跨度内,它们将随相关跨度上下文一起导出。
所有这些都无需添加任何额外的插装代码或配置即可工作。如果您的 Node.js 服务器需要立即进行日志记录和追踪,请尝试使用启用 OTel 的 Deno 运行它。
下一步是什么?
OpenTelemetry 极大地简化了遥测数据的摄取和导出,现在它已内置于 Deno 中,添加可观测性比以往任何时候都更容易。无需额外的配置或插装,您就可以立即从 HTTP 请求和控制台日志中获取日志和追踪数据。
我们将继续改进我们内置的 OTel 集成,许多更新正在进行中。我们还将发布更多关于设置 OTel 和将遥测数据发送到您首选可观测性堆栈的资源。
🚨️ Deno 2.2 发布! 🚨️