deno.com
本页内容

使用 OpenTelemetry 和 Deno Deploy 监控您的应用

Deno DeployEA 内置 OpenTelemetry 支持,可自动捕获 HTTP 请求、数据库查询和其他操作的跟踪。本教程将展示如何向您的应用程序添加自定义 OpenTelemetry 仪器,以实现更详细的可观测性。

先决条件 跳转到标题

  1. 一个 GitHub 帐户
  2. 在您的本地机器上安装 Deno
  3. 访问 Deno Deploy 抢先体验计划
  4. 熟悉 OpenTelemetry 概念基础知识

创建一个基本的 API 应用 跳转到标题

首先,我们创建一个简单的 API 服务器,然后用 OpenTelemetry 对其进行仪器化。

main.ts
const dataStore: Record<string, string> = {};

async function handler(req: Request): Promise<Response> {
  const url = new URL(req.url);

  // Simulate random latency
  await new Promise((resolve) => setTimeout(resolve, Math.random() * 200));

  try {
    // Handle product listing
    if (url.pathname === "/products" && req.method === "GET") {
      return new Response(JSON.stringify(Object.values(dataStore)), {
        headers: { "Content-Type": "application/json" },
      });
    }

    // Handle product creation
    if (url.pathname === "/products" && req.method === "POST") {
      const data = await req.json();
      const id = crypto.randomUUID();
      dataStore[id] = data;
      return new Response(JSON.stringify({ id, ...data }), {
        status: 201,
        headers: { "Content-Type": "application/json" },
      });
    }

    // Handle product retrieval by ID
    if (url.pathname.startsWith("/products/") && req.method === "GET") {
      const id = url.pathname.split("/")[2];
      const product = dataStore[id];

      if (!product) {
        return new Response("Product not found", { status: 404 });
      }

      return new Response(JSON.stringify(product), {
        headers: { "Content-Type": "application/json" },
      });
    }

    // Handle root route
    if (url.pathname === "/") {
      return new Response("Product API - Try /products endpoint");
    }

    return new Response("Not Found", { status: 404 });
  } catch (error) {
    console.error("Error handling request:", error);
    return new Response("Internal Server Error", { status: 500 });
  }
}

console.log("Server running on http://localhost:8000");
Deno.serve(handler, { port: 8000 });

保存此文件并在本地运行

deno run --allow-net main.ts

使用 curl 或浏览器测试 API,确保其正常工作

# List products (empty at first)
curl http://localhost:8000/products

# Add a product
curl -X POST http://localhost:8000/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Product", "price": 19.99}'

添加 OpenTelemetry 仪器 跳转到标题

现在,让我们为应用程序添加自定义 OpenTelemetry 仪器。创建一个名为 instrumented-main.ts 的新文件。

instrumented-main.ts
import { trace } from "npm:@opentelemetry/api@1";

// Get the OpenTelemetry tracer
const tracer = trace.getTracer("product-api");

const dataStore: Record<string, string> = {};

// Simulate a database operation with custom span
async function queryDatabase(
  operation: string,
  data?: unknown,
): Promise<unknown> {
  return await tracer.startActiveSpan(`database.${operation}`, async (span) => {
    try {
      // Add attributes to the span for better context
      span.setAttributes({
        "db.system": "memory-store",
        "db.operation": operation,
      });

      // Simulate database latency
      const delay = Math.random() * 100;
      await new Promise((resolve) => setTimeout(resolve, delay));

      // Add latency information to the span
      span.setAttributes({ "db.latency_ms": delay });

      if (operation === "list") {
        return Object.values(dataStore);
      } else if (operation === "get") {
        return dataStore[data as string];
      } else if (operation === "insert") {
        const id = crypto.randomUUID();
        dataStore[id] = data as string;
        return { id, data };
      }

      return null;
    } catch (error) {
      // Record any errors to the span
      span.recordException(error);
      span.setStatus({ code: trace.SpanStatusCode.ERROR });
      throw error;
    } finally {
      // End the span when we're done
      span.end();
    }
  });
}

async function handler(req: Request): Promise<Response> {
  // Create a parent span for the entire request
  return await tracer.startActiveSpan(
    `${req.method} ${new URL(req.url).pathname}`,
    async (parentSpan) => {
      const url = new URL(req.url);

      // Add request details as span attributes
      parentSpan.setAttributes({
        "http.method": req.method,
        "http.url": req.url,
        "http.route": url.pathname,
      });

      try {
        // Handle product listing
        if (url.pathname === "/products" && req.method === "GET") {
          const products = await queryDatabase("list");
          return new Response(JSON.stringify(products), {
            headers: { "Content-Type": "application/json" },
          });
        }

        // Handle product creation
        if (url.pathname === "/products" && req.method === "POST") {
          // Create a span for parsing request JSON
          const data = await tracer.startActiveSpan(
            "parse.request.body",
            async (span) => {
              try {
                const result = await req.json();
                return result;
              } catch (error) {
                span.recordException(error);
                span.setStatus({ code: trace.SpanStatusCode.ERROR });
                throw error;
              } finally {
                span.end();
              }
            },
          );

          const result = await queryDatabase("insert", data);
          return new Response(JSON.stringify(result), {
            status: 201,
            headers: { "Content-Type": "application/json" },
          });
        }

        // Handle product retrieval by ID
        if (url.pathname.startsWith("/products/") && req.method === "GET") {
          const id = url.pathname.split("/")[2];
          parentSpan.setAttributes({ "product.id": id });

          const product = await queryDatabase("get", id);

          if (!product) {
            parentSpan.setAttributes({
              "error": true,
              "error.type": "not_found",
            });
            return new Response("Product not found", { status: 404 });
          }

          return new Response(JSON.stringify(product), {
            headers: { "Content-Type": "application/json" },
          });
        }

        // Handle root route
        if (url.pathname === "/") {
          return new Response("Product API - Try /products endpoint");
        }

        parentSpan.setAttributes({ "error": true, "error.type": "not_found" });
        return new Response("Not Found", { status: 404 });
      } catch (error) {
        console.error("Error handling request:", error);
        // Record the error in the span
        parentSpan.recordException(error);
        parentSpan.setAttributes({
          "error": true,
          "error.type": error.name,
          "error.message": error.message,
        });
        parentSpan.setStatus({ code: trace.SpanStatusCode.ERROR });

        return new Response("Internal Server Error", { status: 500 });
      } finally {
        // End the parent span when we're done
        parentSpan.end();
      }
    },
  );
}

console.log(
  "Server running with OpenTelemetry instrumentation on http://localhost:8000",
);
Deno.serve(handler, { port: 8000 });

在本地运行仪器化版本

deno run --allow-net instrumented-main.ts

再次使用 curl 测试 API 以生成一些跟踪。

创建 GitHub 仓库 跳转到标题

  1. 前往 GitHub 并创建一个新仓库。

  2. 将本地目录初始化为 Git 仓库

git init
git add .
git commit -m "Add OpenTelemetry instrumented API"
  1. 将您的 GitHub 仓库添加为远程仓库并推送您的代码
git remote add origin https://github.com/your-username/otel-demo-app.git
git branch -M main
git push -u origin main

部署到 Deno Deploy 抢先体验版 跳转到标题

  1. 导航到 app.deno.com

  2. 选择您的组织,如果需要可以创建一个新组织

  3. 点击 “+ 新建应用”

  4. 选择您之前创建的 GitHub 仓库

  5. 配置构建设置

    • 框架预设:无预设
    • 运行时配置:动态
    • 入口点:instrumented-main.ts
  6. 点击 “创建应用” 开始部署过程

生成示例流量 跳转到标题

为了生成示例跟踪和指标,让我们向已部署的应用程序发送一些流量。

  1. 从 Deno Deploy 控制台复制您的部署 URL

  2. 向不同端点发送多条请求

# Store your app URL in a variable
APP_URL=https://your-app-name.your-org-name.deno.net

# Get the root route
curl $APP_URL/

# List products (empty at first)
curl $APP_URL/products

# Add some products
curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Laptop", "price": 999.99}'
curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Headphones", "price": 129.99}'
curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Mouse", "price": 59.99}'

# List products again
curl $APP_URL/products

# Try to access a non-existent product (will generate an error span)
curl $APP_URL/products/nonexistent-id

探索 OpenTelemetry 跟踪和指标 跳转到标题

现在我们来探索 Deno Deploy 收集的可观测性数据

  1. 从您的应用程序控制台,点击侧边栏中的 “跟踪”

    • 您将看到应用程序每个请求的跟踪列表
    • 您可以使用搜索栏按 HTTP 方法或状态码过滤跟踪
  2. 选择一个您的 /products POST 跟踪以查看详细信息

    • 整个请求的父 span
    • 数据库操作的子 span
    • 用于解析请求体的 span

    Trace waterfall view

  3. 点击单个 span 查看其详细信息

    • 持续时间和时间信息
    • 您设置的属性,例如 db.operationdb.latency_ms
    • 任何记录的异常
  4. 点击侧边栏中的 “日志” 以查看包含跟踪上下文的控制台输出

    • 注意在跟踪操作期间发出的日志如何自动链接到跟踪
    • 点击日志行上的 “查看跟踪” 以查看关联的跟踪
  5. 点击 “指标” 查看应用程序性能指标

    • 按端点划分的 HTTP 请求计数
    • 错误率
    • 响应时间分布

🦕 Deno DeployEA 中的自动仪器化与您的自定义仪器化相结合,为您的应用程序性能和行为提供了全面的可见性。

有关 Deno 中 OpenTelemetry 的更多信息,请查阅这些资源

您找到所需内容了吗?

隐私政策