Skip to content

Commit 2a7586d

Browse files
ioquatixsamuel-williams-shopify
authored andcommitted
Add Traces.current_context and Traces.with_context.
1 parent 8a5bf9f commit 2a7586d

File tree

7 files changed

+865
-10
lines changed

7 files changed

+865
-10
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Context Propagation
2+
3+
This guide explains how to propagate trace context between different execution contexts within your application using `Traces.current_context` and `Traces.with_context`.
4+
5+
## Overview
6+
7+
The `traces` library provides two complementary approaches for managing trace context:
8+
9+
- **Local context propagation** (`Traces.current_context` / `Traces.with_context`): For passing context between execution contexts within the same process (threads, fibers, async tasks).
10+
- **Distributed context propagation** (`Traces.inject` / `Traces.extract`): For transmitting context across process and service boundaries via serialization (HTTP headers, message metadata, etc.).
11+
12+
There is a legacy interface `Traces.trace_context` and `Traces.trace_context=` but you should prefer to use the new methods outlined above.
13+
14+
## Local Context Propagation
15+
16+
Local context propagation involves passing trace context between different execution contexts within the same process. This is essential for maintaining trace continuity when code execution moves between threads, fibers, async tasks, or other concurrent execution contexts. Unlike distributed propagation which requires serialization over network boundaries, local propagation uses Context objects directly.
17+
18+
### Capturing the Current Context
19+
20+
Use `Traces.current_context` to capture the current trace context as a Context object:
21+
22+
~~~ ruby
23+
current_context = Traces.current_context
24+
# Returns a Traces::Context object or nil if no active trace
25+
~~~
26+
27+
### Using the Context
28+
29+
Use `Traces.with_context(context)` to execute code within a specific trace context:
30+
31+
~~~ ruby
32+
# With block (automatic restoration):
33+
Traces.with_context(context) do
34+
# Code runs with the specified context.
35+
end
36+
37+
# Without block (permanent switch):
38+
Traces.with_context(context)
39+
# Context remains active.
40+
~~~
41+
42+
### Use Cases
43+
44+
#### Thread-Safe Context Propagation
45+
46+
When spawning background threads, you often want them to inherit the current trace context:
47+
48+
~~~ ruby
49+
require 'traces'
50+
51+
# Main thread has active tracing
52+
Traces.trace("main_operation") do
53+
# Capture current context before spawning thread:
54+
current_context = Traces.current_context
55+
56+
# Spawn background thread:
57+
Thread.new do
58+
# Restore context in the new thread:
59+
Traces.with_context(current_context) do
60+
# This thread now has the same trace context as main thread:
61+
Traces.trace("background_work") do
62+
perform_heavy_computation
63+
end
64+
end
65+
end.join
66+
end
67+
~~~
68+
69+
#### Fiber-Based Async Operations
70+
71+
For fiber-based concurrency (like in async frameworks), context propagation ensures trace continuity:
72+
73+
~~~ ruby
74+
require 'traces'
75+
76+
Traces.trace("main_operation") do
77+
current_context = Traces.current_context
78+
79+
# Create fiber for async work:
80+
fiber = Fiber.new do
81+
Traces.with_context(current_context) do
82+
# Fiber inherits the trace context:
83+
Traces.trace("fiber_work") do
84+
perform_async_operation
85+
end
86+
end
87+
end
88+
89+
fiber.resume
90+
end
91+
~~~
92+
93+
### Context Propagation vs. New Spans
94+
95+
Remember that context propagation maintains the same trace, while `trace()` creates new spans:
96+
97+
~~~ ruby
98+
Traces.trace("parent") do
99+
context = Traces.current_context
100+
101+
Thread.new do
102+
# This maintains the same trace context:
103+
Traces.with_context(context) do
104+
# This creates a NEW span within the same trace:
105+
Traces.trace("child") do
106+
# Child span, same trace as parent
107+
end
108+
end
109+
end
110+
end
111+
~~~
112+
113+
## Distributed Context Propagation
114+
115+
Distributed context propagation involves transmitting trace context across process and service boundaries. Unlike local propagation which works within a single process, distributed propagation requires serializing context data and transmitting it over network protocols.
116+
117+
### Injecting Context into Headers
118+
119+
Use `Traces.inject(headers, context = nil)` to add W3C Trace Context headers to a headers hash for transmission over network boundaries:
120+
121+
~~~ ruby
122+
require 'traces'
123+
124+
# Capture current context:
125+
context = Traces.current_context
126+
headers = {'Content-Type' => 'application/json'}
127+
128+
# Inject trace headers:
129+
Traces.inject(headers, context)
130+
# headers now contains: {'Content-Type' => '...', 'traceparent' => '00-...'}
131+
132+
# Or use current context by default:
133+
Traces.inject(headers) # Uses current trace context
134+
~~~
135+
136+
### Extracting Context from Headers
137+
138+
Use `Traces.extract(headers)` to extract trace context from W3C headers received over the network:
139+
140+
~~~ ruby
141+
# Receive headers from incoming request:
142+
incoming_headers = request.headers
143+
144+
# Extract context:
145+
context = Traces.extract(incoming_headers)
146+
# Returns a Traces::Context object or nil if no valid context
147+
148+
# Use the extracted context:
149+
if context
150+
Traces.with_context(context) do
151+
# Process request with distributed trace context
152+
end
153+
end
154+
~~~
155+
156+
### Use Cases
157+
158+
#### Outgoing HTTP Requests
159+
160+
~~~ ruby
161+
require 'traces'
162+
163+
class ApiClient
164+
def make_request(endpoint, data)
165+
Traces.trace("api_request", attributes: {endpoint: endpoint}) do
166+
headers = {
167+
'Content-Type' => 'application/json'
168+
}
169+
170+
# Add trace context to outgoing request:
171+
Traces.inject(headers)
172+
173+
http_client.post(endpoint,
174+
body: data.to_json,
175+
headers: headers
176+
)
177+
end
178+
end
179+
end
180+
~~~
181+
182+
#### Incoming HTTP Requests
183+
184+
~~~ ruby
185+
require 'traces'
186+
187+
class WebController
188+
def handle_request(request)
189+
# Extract trace context from incoming headers:
190+
context = Traces.extract(request.headers)
191+
192+
# Process request with inherited context:
193+
if context
194+
Traces.with_context(context) do
195+
Traces.trace("web_request", attributes: {
196+
path: request.path,
197+
method: request.method
198+
}) do
199+
process_business_logic
200+
end
201+
end
202+
else
203+
Traces.trace("web_request", attributes: {
204+
path: request.path,
205+
method: request.method
206+
}) do
207+
process_business_logic
208+
end
209+
end
210+
end
211+
end
212+
~~~

guides/links.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
getting-started:
22
order: 1
3-
testing:
3+
context-propagation:
44
order: 2
5-
capture:
5+
testing:
66
order: 3
7+
capture:
8+
order: 4

lib/traces.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
require_relative "traces/version"
77
require_relative "traces/provider"
8+
require_relative "traces/context"
89

910
# @namespace
1011
module Traces

lib/traces/backend.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,101 @@
44
# Copyright, 2021-2025, by Samuel Williams.
55

66
require_relative "config"
7+
require_relative "context"
78

89
module Traces
910
# The backend implementation is responsible for recording and reporting traces.
1011
module Backend
1112
end
1213

14+
# Capture the current trace context for remote propagation.
15+
#
1316
# This is a default implementation, which can be replaced by the backend.
17+
#
18+
# You should prefer to use the new `Traces.current_context` family of methods.
19+
#
1420
# @returns [Object] The current trace context.
1521
def self.trace_context
1622
nil
1723
end
1824

25+
# Whether there is an active trace context.
26+
#
1927
# This is a default implementation, which can be replaced by the backend.
28+
#
2029
# @returns [Boolean] Whether there is an active trace.
2130
def self.active?
2231
!!self.trace_context
2332
end
2433

34+
# Capture the current trace context for local propagation between execution contexts.
35+
#
36+
# This method returns the current trace context that can be safely passed between threads, fibers, or other execution contexts within the same process.
37+
#
38+
# The returned object is opaque, in other words, you should not make assumptions about its structure.
39+
#
40+
# This is a default implementation, which can be replaced by the backend.
41+
#
42+
# @returns [Context | Nil] The current trace context, or nil if no active trace.
43+
def self.current_context
44+
trace_context
45+
end
46+
47+
# Execute a block within a specific trace context for local execution.
48+
#
49+
# This method is designed for propagating trace context between execution contexts within the same process (threads, fibers, etc.). It temporarily switches to the specified trace context for the duration of the block execution, then restores the previous context.
50+
#
51+
# When called without a block, permanently switches to the specified context. This enables manual context management for scenarios where automatic restoration isn't desired.
52+
#
53+
# This is a default implementation, which can be replaced by the backend.
54+
#
55+
# @parameter context [Context] A trace context obtained from `Traces.current_context`.
56+
# @yields {...} If a block is given, the block is executed within the specified trace context.
57+
def self.with_context(context)
58+
if block_given?
59+
# This implementation is not ideal but the best we can do with the current interface.
60+
previous_context = self.trace_context
61+
begin
62+
self.trace_context = context
63+
yield
64+
ensure
65+
self.trace_context = previous_context
66+
end
67+
else
68+
self.trace_context = context
69+
end
70+
end
71+
72+
# Inject trace context into a headers hash for distributed propagation.
73+
#
74+
# This method adds W3C Trace Context headers (traceparent, tracestate) and W3C Baggage headers to the provided headers hash, enabling distributed tracing across service boundaries. The headers hash is mutated in place.
75+
#
76+
# This is a default implementation, which can be replaced by the backend.
77+
#
78+
# @parameter headers [Hash] The headers object to mutate with trace context headers.
79+
# @parameter context [Context] A trace context, or nil to use current context.
80+
def self.inject(headers = nil, context = nil)
81+
context ||= self.trace_context
82+
83+
if context
84+
headers ||= Hash.new
85+
context.inject(headers)
86+
end
87+
88+
return headers
89+
end
90+
91+
# Extract trace context from headers for distributed propagation.
92+
#
93+
# The returned object is opaque, in other words, you should not make assumptions about its structure.
94+
#
95+
# This is a default implementation, which can be replaced by the backend.
96+
#
97+
# @parameter headers [Hash] The headers object containing trace context.
98+
# @returns [Context, nil] The extracted trace context, or nil if no valid context found.
99+
def self.extract(headers)
100+
Context.extract(headers)
101+
end
102+
25103
Config::DEFAULT.require_backend
26104
end

0 commit comments

Comments
 (0)