跳到主要内容
Deno 2.4 现已发布,带来 deno bundle、bytes/text 导入、稳定的 OTel 等功能
了解更多

如何使用 OTel 和 Deno 在 Node.js 后端获取深度追踪数据

运行生产软件的一个重要方面是可观测性——监控日志、追踪和指标,以便您能快速识别并解决问题。在 Node.js 服务器中,添加可观测性并非易事。您必须添加插装代码、修改日志记录器并配置各种服务。如果您可以立即查看日志和追踪数据,而无需任何额外工作,那会怎么样?

在本文中,我们将展示如何通过在 Deno 中运行 Node.js 后端,从而在不进行任何代码更改的情况下,立即查看其正在执行的操作。Deno 现在可以运行 Node 程序并内置 OpenTelemetry 支持,这使得操作比以往任何时候都更容易。

Node 中的深度追踪

在我们使用 Deno 设置 OTel 之前,让我们先回顾一下如何在 Node 中设置它。

这是一个简单的应用程序(GitHub 源代码),它显示一个聊天框,接收您的输入并将其传递给 ChatGPT。

Screenshot of our very simple chat app.

要跟着操作,请克隆此仓库并在 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”,您将看到追踪数据。

Seeing Node traces in Tempo and Grafana

点击追踪会显示一个 POST 请求,这是该追踪中的一个跨度。点击“Logs for this span”(此跨度的日志)会显示与此请求相关的日志。

Seeing Node logs from traces in Tempo and Grafana

在 Node 中,设置 OTel 需要导入 8 个新的依赖项额外 84 行插装代码,以及console.log 替换为配置为将日志输出发送到 OTel 端点的自定义日志记录器

要准确查看将 OpenTelemetry 集成到我们的 Node 应用程序中所需的更改,请看差异对比

package.json 中添加依赖项

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"
   }
添加 OpenTelemetry 需要导入八个新的依赖项。

替换 server.js 中的 console.log 调用

server.js
 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}`);
 });
我们已将 `console.log` 调用替换为自定义的 `logger`,该 `logger` 将日志写入 OTel 收集器。
telemetry.js
+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.logfetchDeno.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”,我们就可以看到追踪数据了。

Seeing Deno traces in Tempo and Grafana

点击“Logs for this span”(此跨度的日志)会显示与同一 HTTP 请求相关的日志。

Seeing Deno traces in Tempo and Grafana

我们展开了最后一个 console.log,可以看到 Deno 和 OTel 已经自动附加了元数据以帮助关联此日志。如果 JavaScript 代码中的日志发生在活动的跨度内,它们将随相关跨度上下文一起导出。

所有这些都无需添加任何额外的插装代码或配置即可工作。如果您的 Node.js 服务器需要立即进行日志记录和追踪,请尝试使用启用 OTel 的 Deno 运行它。

了解更多 Deno 内置 OTel 支持的高级用例.

下一步是什么?

OpenTelemetry 极大地简化了遥测数据的摄取和导出,现在它已内置于 Deno 中,添加可观测性比以往任何时候都更容易。无需额外的配置或插装,您就可以立即从 HTTP 请求和控制台日志中获取日志和追踪数据。

我们将继续改进我们内置的 OTel 集成,许多更新正在进行中。我们还将发布更多关于设置 OTel 和将遥测数据发送到您首选可观测性堆栈的资源。

🚨️ Deno 2.2 发布! 🚨️