Skip to content

Commit e14ba99

Browse files
Deploy otel tutorial (#1854)
1 parent 7836930 commit e14ba99

File tree

3 files changed

+376
-0
lines changed

3 files changed

+376
-0
lines changed

examples/_data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,11 @@ export const sidebar = [
375375
href: "/examples/otel_span_propagation_tutorial/",
376376
type: "tutorial",
377377
},
378+
{
379+
title: "OpenTelemetry with Deno Deploy",
380+
href: "/examples/deploy_otel_tutorial/",
381+
type: "tutorial",
382+
},
378383
],
379384
},
380385
{

examples/tutorials/deploy_otel.md

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
---
2+
title: "Monitor your app with OpenTelemetry and Deno Deploy"
3+
description: "A step-by-step tutorial for adding custom OpenTelemetry instrumentation to your Deno Deploy application."
4+
url: /examples/deploy_otel_tutorial/
5+
---
6+
7+
Deno Deploy<sup>EA</sup> includes built-in OpenTelemetry support that
8+
automatically captures traces for HTTP requests, database queries, and other
9+
operations. This tutorial shows how to add custom OpenTelemetry instrumentation
10+
to your applications for more detailed observability.
11+
12+
## Prerequisites
13+
14+
1. A [GitHub](https://github.com) account
15+
2. [Deno installed](https://docs.deno.com/runtime/manual/getting_started/installation)
16+
on your local machine
17+
3. Access to the
18+
[Deno Deploy Early Access program](https://dash.deno.com/account#early-access)
19+
4. Basic familiarity with
20+
[OpenTelemetry concepts](https://opentelemetry.io/docs/concepts/)
21+
22+
## Create a basic API application
23+
24+
First, let's create a simple API server that we'll instrument with
25+
OpenTelemetry:
26+
27+
```ts title="main.ts"
28+
const dataStore: Record<string, string> = {};
29+
30+
async function handler(req: Request): Promise<Response> {
31+
const url = new URL(req.url);
32+
33+
// Simulate random latency
34+
await new Promise((resolve) => setTimeout(resolve, Math.random() * 200));
35+
36+
try {
37+
// Handle product listing
38+
if (url.pathname === "/products" && req.method === "GET") {
39+
return new Response(JSON.stringify(Object.values(dataStore)), {
40+
headers: { "Content-Type": "application/json" },
41+
});
42+
}
43+
44+
// Handle product creation
45+
if (url.pathname === "/products" && req.method === "POST") {
46+
const data = await req.json();
47+
const id = crypto.randomUUID();
48+
dataStore[id] = data;
49+
return new Response(JSON.stringify({ id, ...data }), {
50+
status: 201,
51+
headers: { "Content-Type": "application/json" },
52+
});
53+
}
54+
55+
// Handle product retrieval by ID
56+
if (url.pathname.startsWith("/products/") && req.method === "GET") {
57+
const id = url.pathname.split("/")[2];
58+
const product = dataStore[id];
59+
60+
if (!product) {
61+
return new Response("Product not found", { status: 404 });
62+
}
63+
64+
return new Response(JSON.stringify(product), {
65+
headers: { "Content-Type": "application/json" },
66+
});
67+
}
68+
69+
// Handle root route
70+
if (url.pathname === "/") {
71+
return new Response("Product API - Try /products endpoint");
72+
}
73+
74+
return new Response("Not Found", { status: 404 });
75+
} catch (error) {
76+
console.error("Error handling request:", error);
77+
return new Response("Internal Server Error", { status: 500 });
78+
}
79+
}
80+
81+
console.log("Server running on http://localhost:8000");
82+
Deno.serve(handler, { port: 8000 });
83+
```
84+
85+
Save this file and run it locally:
86+
87+
```sh
88+
deno run --allow-net main.ts
89+
```
90+
91+
Test the API with curl or a browser to ensure it works:
92+
93+
```sh
94+
# List products (empty at first)
95+
curl http://localhost:8000/products
96+
97+
# Add a product
98+
curl -X POST http://localhost:8000/products \
99+
-H "Content-Type: application/json" \
100+
-d '{"name": "Test Product", "price": 19.99}'
101+
```
102+
103+
## Add OpenTelemetry instrumentation
104+
105+
Now, let's add custom OpenTelemetry instrumentation to our application. Create a
106+
new file called `instrumented-main.ts`:
107+
108+
```ts title="instrumented-main.ts"
109+
import { trace } from "npm:@opentelemetry/api@1";
110+
111+
// Get the OpenTelemetry tracer
112+
const tracer = trace.getTracer("product-api");
113+
114+
const dataStore: Record<string, string> = {};
115+
116+
// Simulate a database operation with custom span
117+
async function queryDatabase(
118+
operation: string,
119+
data?: unknown,
120+
): Promise<unknown> {
121+
return await tracer.startActiveSpan(`database.${operation}`, async (span) => {
122+
try {
123+
// Add attributes to the span for better context
124+
span.setAttributes({
125+
"db.system": "memory-store",
126+
"db.operation": operation,
127+
});
128+
129+
// Simulate database latency
130+
const delay = Math.random() * 100;
131+
await new Promise((resolve) => setTimeout(resolve, delay));
132+
133+
// Add latency information to the span
134+
span.setAttributes({ "db.latency_ms": delay });
135+
136+
if (operation === "list") {
137+
return Object.values(dataStore);
138+
} else if (operation === "get") {
139+
return dataStore[data as string];
140+
} else if (operation === "insert") {
141+
const id = crypto.randomUUID();
142+
dataStore[id] = data as string;
143+
return { id, data };
144+
}
145+
146+
return null;
147+
} catch (error) {
148+
// Record any errors to the span
149+
span.recordException(error);
150+
span.setStatus({ code: trace.SpanStatusCode.ERROR });
151+
throw error;
152+
} finally {
153+
// End the span when we're done
154+
span.end();
155+
}
156+
});
157+
}
158+
159+
async function handler(req: Request): Promise<Response> {
160+
// Create a parent span for the entire request
161+
return await tracer.startActiveSpan(
162+
`${req.method} ${new URL(req.url).pathname}`,
163+
async (parentSpan) => {
164+
const url = new URL(req.url);
165+
166+
// Add request details as span attributes
167+
parentSpan.setAttributes({
168+
"http.method": req.method,
169+
"http.url": req.url,
170+
"http.route": url.pathname,
171+
});
172+
173+
try {
174+
// Handle product listing
175+
if (url.pathname === "/products" && req.method === "GET") {
176+
const products = await queryDatabase("list");
177+
return new Response(JSON.stringify(products), {
178+
headers: { "Content-Type": "application/json" },
179+
});
180+
}
181+
182+
// Handle product creation
183+
if (url.pathname === "/products" && req.method === "POST") {
184+
// Create a span for parsing request JSON
185+
const data = await tracer.startActiveSpan(
186+
"parse.request.body",
187+
async (span) => {
188+
try {
189+
const result = await req.json();
190+
return result;
191+
} catch (error) {
192+
span.recordException(error);
193+
span.setStatus({ code: trace.SpanStatusCode.ERROR });
194+
throw error;
195+
} finally {
196+
span.end();
197+
}
198+
},
199+
);
200+
201+
const result = await queryDatabase("insert", data);
202+
return new Response(JSON.stringify(result), {
203+
status: 201,
204+
headers: { "Content-Type": "application/json" },
205+
});
206+
}
207+
208+
// Handle product retrieval by ID
209+
if (url.pathname.startsWith("/products/") && req.method === "GET") {
210+
const id = url.pathname.split("/")[2];
211+
parentSpan.setAttributes({ "product.id": id });
212+
213+
const product = await queryDatabase("get", id);
214+
215+
if (!product) {
216+
parentSpan.setAttributes({
217+
"error": true,
218+
"error.type": "not_found",
219+
});
220+
return new Response("Product not found", { status: 404 });
221+
}
222+
223+
return new Response(JSON.stringify(product), {
224+
headers: { "Content-Type": "application/json" },
225+
});
226+
}
227+
228+
// Handle root route
229+
if (url.pathname === "/") {
230+
return new Response("Product API - Try /products endpoint");
231+
}
232+
233+
parentSpan.setAttributes({ "error": true, "error.type": "not_found" });
234+
return new Response("Not Found", { status: 404 });
235+
} catch (error) {
236+
console.error("Error handling request:", error);
237+
// Record the error in the span
238+
parentSpan.recordException(error);
239+
parentSpan.setAttributes({
240+
"error": true,
241+
"error.type": error.name,
242+
"error.message": error.message,
243+
});
244+
parentSpan.setStatus({ code: trace.SpanStatusCode.ERROR });
245+
246+
return new Response("Internal Server Error", { status: 500 });
247+
} finally {
248+
// End the parent span when we're done
249+
parentSpan.end();
250+
}
251+
},
252+
);
253+
}
254+
255+
console.log(
256+
"Server running with OpenTelemetry instrumentation on http://localhost:8000",
257+
);
258+
Deno.serve(handler, { port: 8000 });
259+
```
260+
261+
Run the instrumented version locally:
262+
263+
```sh
264+
deno run --allow-net instrumented-main.ts
265+
```
266+
267+
Test the API again with curl to generate some traces.
268+
269+
## Create a GitHub repository
270+
271+
1. Go to [GitHub](https://github.com) and create a new repository.
272+
273+
2. Initialize your local directory as a Git repository:
274+
275+
```sh
276+
git init
277+
git add .
278+
git commit -m "Add OpenTelemetry instrumented API"
279+
```
280+
281+
3. Add your GitHub repository as a remote and push your code:
282+
283+
```sh
284+
git remote add origin https://github.com/your-username/otel-demo-app.git
285+
git branch -M main
286+
git push -u origin main
287+
```
288+
289+
## Deploy to Deno Deploy Early Access
290+
291+
1. Navigate to [app.deno.com](https://app.deno.com)
292+
2. Select your organization or create a new one if needed
293+
3. Click "+ New App"
294+
4. Select the GitHub repository you created earlier
295+
5. Configure the build settings:
296+
- Framework preset: No preset
297+
- Runtime configuration: Dynamic
298+
- Entrypoint: `instrumented-main.ts`
299+
300+
6. Click "Create App" to start the deployment process
301+
302+
## Generate sample traffic
303+
304+
To generate sample traces and metrics, let's send some traffic to your deployed
305+
application:
306+
307+
1. Copy your deployment URL from the Deno Deploy dashboard
308+
309+
2. Send several requests to different endpoints:
310+
311+
```sh
312+
# Store your app URL in a variable
313+
APP_URL=https://your-app-name.your-org-name.deno.net
314+
315+
# Get the root route
316+
curl $APP_URL/
317+
318+
# List products (empty at first)
319+
curl $APP_URL/products
320+
321+
# Add some products
322+
curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Laptop", "price": 999.99}'
323+
curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Headphones", "price": 129.99}'
324+
curl -X POST $APP_URL/products -H "Content-Type: application/json" -d '{"name": "Mouse", "price": 59.99}'
325+
326+
# List products again
327+
curl $APP_URL/products
328+
329+
# Try to access a non-existent product (will generate an error span)
330+
curl $APP_URL/products/nonexistent-id
331+
```
332+
333+
## Explore OpenTelemetry traces and metrics
334+
335+
Now let's explore the observability data collected by Deno Deploy:
336+
337+
1. From your application dashboard, click "Traces" in the sidebar
338+
- You'll see a list of traces for each request to your application
339+
- You can filter traces by HTTP method or status code using the search bar
340+
341+
2. Select one of your `/products` POST traces to see detailed information:
342+
- The parent span for the entire request
343+
- Child spans for database operations
344+
- The span for parsing the request body
345+
346+
![Trace waterfall view](./images/early-access/otel_trace.png)
347+
348+
3. Click on individual spans to see their details:
349+
- Duration and timing information
350+
- Attributes you set like `db.operation` and `db.latency_ms`
351+
- Any recorded exceptions
352+
353+
4. Click "Logs" in the sidebar to see console output with trace context:
354+
- Notice how logs emitted during a traced operation are automatically linked
355+
to the trace
356+
- Click "View trace" on a log line to see the associated trace
357+
358+
5. Click "Metrics" to view application performance metrics:
359+
- HTTP request counts by endpoint
360+
- Error rates
361+
- Response time distributions
362+
363+
🦕 The automatic instrumentation in Deno Deploy<sup>EA</sup> combined with your
364+
custom instrumentation provides comprehensive visibility into your application's
365+
performance and behavior.
366+
367+
For more information about OpenTelemetry in Deno, check out these resources:
368+
369+
- [OpenTelemetry in Deno documentation](/runtime/fundamentals/open_telemetry/)
370+
- [Deno Deploy<sup>EA</sup> Observability reference](/deploy/early-access/reference/observability/)
371+
- [OpenTelemetry official documentation](https://opentelemetry.io/docs/)
54.5 KB
Loading

0 commit comments

Comments
 (0)