Skip to content

Commit 8d5fffe

Browse files
add docs for context propagation of span data (#1670)
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
1 parent 210c002 commit 8d5fffe

File tree

4 files changed

+240
-2
lines changed

4 files changed

+240
-2
lines changed

deno.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Loading
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: "Distributed Tracing with Context Propagation in Deno"
3+
description: "Learn how to implement end-to-end distributed tracing with automatic context propagation in Deno applications. This tutorial covers creating traced services, automatic propagation of trace context, and visualizing distributed traces."
4+
url: /examples/otel_span_propagation_tutorial/
5+
---
6+
7+
Modern applications are often built as distributed systems with multiple
8+
services communicating with each other. When debugging issues or optimizing
9+
performance in these systems, it's crucial to be able to trace requests as they
10+
flow through different services. This is where distributed tracing comes in.
11+
12+
As of Deno 2.3, the runtime now automatically preserves trace context across
13+
service boundaries, making end-to-end tracing in distributed systems simpler and
14+
more powerful. This means that when one service makes a request to another, the
15+
trace context is automatically propagated, allowing you to see the entire
16+
request flow as a single trace.
17+
18+
## Setting up a distributed system
19+
20+
Our example system will consist of two parts:
21+
22+
1. A server that provides an API endpoint
23+
2. A client that makes requests to the server
24+
25+
### The server
26+
27+
We'll set up a simple HTTP server that responds to GET requests with a JSON
28+
message:
29+
30+
```ts title="server.ts"
31+
import { trace } from "npm:@opentelemetry/api@1";
32+
33+
const tracer = trace.getTracer("api-server", "1.0.0");
34+
35+
// Create a simple API server with Deno.serve
36+
Deno.serve({ port: 8000 }, (req) => {
37+
return tracer.startActiveSpan("process-api-request", async (span) => {
38+
// Add attributes to the span for better context
39+
span.setAttribute("http.route", "/");
40+
span.updateName("GET /");
41+
42+
// Add a span event to see in traces
43+
span.addEvent("processing_request", {
44+
request_id: crypto.randomUUID(),
45+
timestamp: Date.now(),
46+
});
47+
48+
// Simulate processing time
49+
await new Promise((resolve) => setTimeout(resolve, 50));
50+
51+
console.log("Server: Processing request in trace context");
52+
53+
// End the span when we're done
54+
span.end();
55+
56+
return new Response(JSON.stringify({ message: "Hello from server!" }), {
57+
headers: { "Content-Type": "application/json" },
58+
});
59+
});
60+
});
61+
```
62+
63+
### The client
64+
65+
Now, let's create a client that will make requests to our server:
66+
67+
```ts title="client.ts"
68+
import { SpanStatusCode, trace } from "npm:@opentelemetry/api@1";
69+
70+
const tracer = trace.getTracer("api-client", "1.0.0");
71+
72+
// Create a parent span for the client operation
73+
await tracer.startActiveSpan("call-api", async (parentSpan) => {
74+
try {
75+
console.log("Client: Starting API call");
76+
77+
// The fetch call inside this span will automatically:
78+
// 1. Create a child span for the fetch operation
79+
// 2. Inject the trace context into the outgoing request headers
80+
const response = await fetch("http://localhost:8000/");
81+
const data = await response.json();
82+
83+
console.log(`Client: Received response: ${JSON.stringify(data)}`);
84+
85+
parentSpan.addEvent("received_response", {
86+
status: response.status,
87+
timestamp: Date.now(),
88+
});
89+
} catch (error) {
90+
console.error("Error calling API:", error);
91+
if (error instanceof Error) {
92+
parentSpan.recordException(error);
93+
}
94+
parentSpan.setStatus({
95+
code: SpanStatusCode.ERROR,
96+
message: error instanceof Error ? error.message : String(error),
97+
});
98+
} finally {
99+
parentSpan.end();
100+
}
101+
});
102+
```
103+
104+
## Tracing with OpenTelemetry
105+
106+
Both the client and server code already include basic OpenTelemetry
107+
instrumentation:
108+
109+
1. Create a tracer - both files create a tracer using `trace.getTracer()` with a
110+
name and version.
111+
112+
2. Create spans - We use `startActiveSpan()` to create spans that represent
113+
operations.
114+
115+
3. Add context - We add attributes and events to spans to provide more context.
116+
117+
4. Ending spans - We make sure to end spans when operations are complete.
118+
119+
## Automatic context propagation
120+
121+
The magic happens when the client makes a request to the server. In the client
122+
code there is a fetch call to the server:
123+
124+
```ts
125+
const response = await fetch("http://localhost:8000/");
126+
```
127+
128+
Since this fetch call happens inside an active span, Deno automatically creates
129+
a child span for the fetch operation and Injects the trace context into the
130+
outgoing request headers.
131+
132+
When the server receives this request, Deno extracts the trace context from the
133+
request headers and establishes the server span as a child of the client's span.
134+
135+
## Running the example
136+
137+
To run this example, first, start the server, giving your otel service a name:
138+
139+
```sh
140+
OTEL_DENO=true OTEL_SERVICE_NAME=server deno run --unstable-otel --allow-net server.ts
141+
```
142+
143+
Then, in another terminal, run the client, giving the client a different service
144+
name to make observing the propagation clearer:
145+
146+
```sh
147+
OTEL_DENO=true OTEL_SERVICE_NAME=client deno run --unstable-otel --allow-net client.ts
148+
```
149+
150+
You should see:
151+
152+
1. The client logs "Client: Starting API call"
153+
2. The server logs "Server: Processing request in trace context"
154+
3. The client logs the response received from the server
155+
156+
## Viewing traces
157+
158+
To actually see the traces, you'll need an OpenTelemetry collector and a
159+
visualization tool,
160+
[for example Grafana Tempo](/runtime/fundamentals/open_telemetry/#quick-start).
161+
162+
When you visualize the traces, you'll see:
163+
164+
1. A parent span from the client
165+
2. Connected to a child span for the HTTP request
166+
3. Connected to a span from the server
167+
4. All as part of a single trace!
168+
169+
For example, in Grafana, the trace visualization may look like this:
170+
171+
![Viewing expanded traces in Grafana](./images/how-to/grafana/propagation.png)
172+
173+
🦕 Now that you understand distributed tracing with Deno, you could extend this
174+
to more complex systems with multiple services and async operations.
175+
176+
With Deno's automatic context propagation, implementing distributed tracing in
177+
your applications has never been easier!

runtime/fundamentals/open_telemetry.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,13 @@ variable, the following attributes are automatically set:
648648
- `telemetry.sdk.version`: The version of the Deno runtime, plus the version of
649649
the `opentelemetry` Rust crate being used by Deno, separated by a `-`.
650650

651+
Propagators can be configured using the `OTEL_PROPAGATORS` environment variable.
652+
The default value is `tracecontext,baggage`. Multiple propagators can be
653+
specified by separating them with commas. Currently supported propagators are:
654+
655+
- `tracecontext`: W3C Trace Context propagation format
656+
- `baggage`: W3C Baggage propagation format
657+
651658
Metric collection frequency can be configured using the
652659
`OTEL_METRIC_EXPORT_INTERVAL` environment variable. The default value is `60000`
653660
milliseconds (60 seconds).
@@ -660,15 +667,65 @@ Log exporter batching can be configured using the batch log record processor
660667
environment variables described in the
661668
[OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#batch-log-record-processor).
662669

670+
## Propagators
671+
672+
Deno supports context propagators which enable automatic propagation of trace
673+
context across process boundaries for distributed tracing, allowing you to track
674+
requests as they flow through different services.
675+
676+
Propagators are responsible for encoding and decoding context information (like
677+
trace and span IDs) into and from carrier formats (like HTTP headers). This
678+
enables the trace context to be maintained across service boundaries.
679+
680+
By default, Deno supports the following propagators:
681+
682+
- `tracecontext`: The W3C Trace Context propagation format, which is the
683+
standard way to propagate trace context via HTTP headers.
684+
- `baggage`: The W3C Baggage propagation format, which allows passing key-value
685+
pairs across service boundaries.
686+
687+
:::note
688+
689+
These propagators automatically work with Deno's `fetch` API and `Deno.serve`,
690+
enabling end-to-end tracing across HTTP requests without manual context
691+
management.
692+
693+
:::
694+
695+
You can access the propagation API through the `@opentelemetry/api` package:
696+
697+
```ts
698+
import { context, propagation, trace } from "npm:@opentelemetry/api@1";
699+
700+
// Extract context from incoming headers
701+
function extractContextFromHeaders(headers: Headers) {
702+
const ctx = context.active();
703+
return propagation.extract(ctx, headers);
704+
}
705+
706+
// Inject context into outgoing headers
707+
function injectContextIntoHeaders(headers: Headers) {
708+
const ctx = context.active();
709+
propagation.inject(ctx, headers);
710+
return headers;
711+
}
712+
713+
// Example: Making a fetch request that propagates trace context
714+
async function tracedFetch(url: string) {
715+
const headers = new Headers();
716+
injectContextIntoHeaders(headers);
717+
718+
return await fetch(url, { headers });
719+
}
720+
```
721+
663722
## Limitations
664723

665724
While the OpenTelemetry integration for Deno is in development, there are some
666725
limitations to be aware of:
667726

668727
- Traces are always sampled (i.e. `OTEL_TRACE_SAMPLER=parentbased_always_on`).
669728
- Traces only support links with no attributes.
670-
- Automatic propagation of the trace context in `Deno.serve` and `fetch` is not
671-
supported.
672729
- Metric exemplars are not supported.
673730
- Custom log streams (e.g. logs other than `console.log` and `console.error`)
674731
are not supported.

0 commit comments

Comments
 (0)