本页内容
使用 OpenTelemetry 和 Deno Deploy 监控您的应用
Deno DeployEA 内置 OpenTelemetry 支持,可自动捕获 HTTP 请求、数据库查询和其他操作的跟踪。本教程将展示如何向您的应用程序添加自定义 OpenTelemetry 仪器,以实现更详细的可观测性。
先决条件 跳转到标题
- 一个 GitHub 帐户
- 在您的本地机器上安装 Deno
- 访问 Deno Deploy 抢先体验计划
- 熟悉 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 仓库 跳转到标题
-
前往 GitHub 并创建一个新仓库。
-
将本地目录初始化为 Git 仓库
git init
git add .
git commit -m "Add OpenTelemetry instrumented API"
- 将您的 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 抢先体验版 跳转到标题
-
导航到 app.deno.com
-
选择您的组织,如果需要可以创建一个新组织
-
点击 “+ 新建应用”
-
选择您之前创建的 GitHub 仓库
-
配置构建设置
- 框架预设:无预设
- 运行时配置:动态
- 入口点:
instrumented-main.ts
-
点击 “创建应用” 开始部署过程
生成示例流量 跳转到标题
为了生成示例跟踪和指标,让我们向已部署的应用程序发送一些流量。
-
从 Deno Deploy 控制台复制您的部署 URL
-
向不同端点发送多条请求
# 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 收集的可观测性数据
-
从您的应用程序控制台,点击侧边栏中的 “跟踪”
- 您将看到应用程序每个请求的跟踪列表
- 您可以使用搜索栏按 HTTP 方法或状态码过滤跟踪
-
选择一个您的
/products
POST 跟踪以查看详细信息- 整个请求的父 span
- 数据库操作的子 span
- 用于解析请求体的 span
-
点击单个 span 查看其详细信息
- 持续时间和时间信息
- 您设置的属性,例如
db.operation
和db.latency_ms
- 任何记录的异常
-
点击侧边栏中的 “日志” 以查看包含跟踪上下文的控制台输出
- 注意在跟踪操作期间发出的日志如何自动链接到跟踪
- 点击日志行上的 “查看跟踪” 以查看关联的跟踪
-
点击 “指标” 查看应用程序性能指标
- 按端点划分的 HTTP 请求计数
- 错误率
- 响应时间分布
🦕 Deno DeployEA 中的自动仪器化与您的自定义仪器化相结合,为您的应用程序性能和行为提供了全面的可见性。
有关 Deno 中 OpenTelemetry 的更多信息,请查阅这些资源