Skip to content

Commit 28e9e50

Browse files
Fix active? and trace_context to properly handle inactive traces
- Fix Traces.active? to return false when there is no active trace (previously always returned true even for INVALID spans) - Fix Traces.trace_context to return nil when there is no active trace (previously returned invalid Context objects with zero trace_ids) - Both methods now use OpenTelemetry::Trace::Span::INVALID for proper detection of inactive traces instead of manual trace_id checking - Add test case to ensure trace_context returns nil in clean Fiber context - Resolves issues with Async incorrectly detecting active traces
1 parent c710537 commit 28e9e50

File tree

4 files changed

+186
-134
lines changed

4 files changed

+186
-134
lines changed

gems.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
gem "sus"
2222
gem "covered"
2323
gem "decode"
24-
24+
2525
gem "rubocop"
2626
gem "rubocop-socketry"
2727

lib/traces/backend/open_telemetry/interface.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ def trace_context=(context)
3636
end
3737
end
3838

39+
def active?
40+
# Check if there's a real active trace using OpenTelemetry's INVALID span:
41+
::OpenTelemetry::Trace.current_span != ::OpenTelemetry::Trace::Span::INVALID
42+
end
43+
3944
def trace_context(span = ::OpenTelemetry::Trace.current_span)
45+
# Return nil if no active trace (using INVALID span check):
46+
return nil if span == ::OpenTelemetry::Trace::Span::INVALID
47+
4048
if span_context = span.context
4149
flags = 0
4250

releases.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Fixed `Traces.active?` to correctly return `false` when there is no active trace, instead of always returning `true`.
6+
- Fixed `Traces.trace_context` to return `nil` when there is no active trace, instead of returning invalid Context objects.
7+
38
## v0.3.0
49

510
### New Context Propagation Interface

test/traces/backend/open_telemetry.rb

Lines changed: 172 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ def my_span_and_context
100100
trace_id: be != nil
101101
)
102102
end
103+
104+
it "returns nil when there is no active trace" do
105+
# Use a new Fiber to create a clean context without any active traces
106+
result = Fiber.new do
107+
# trace_context should return nil when there's no active trace
108+
Traces.trace_context
109+
end.resume
110+
111+
expect(result).to be_nil
112+
end
103113
end
104114

105115
with "#trace_context=" do
@@ -117,172 +127,201 @@ def my_span_and_context
117127
)
118128
end
119129
end
120-
end
121-
122-
describe "Context Propagation Methods" do
123-
with "#current_context" do
124-
it "returns current OpenTelemetry context" do
125-
current = Traces.current_context
126-
expect(current).to be_a(::OpenTelemetry::Context)
130+
131+
with "#active?" do
132+
it "returns true when there is an active trace" do
133+
Traces.trace("test") do
134+
expect(Traces.active?).to be == true
135+
end
127136
end
128137

129-
it "returns different contexts in different spans" do
130-
context1 = nil
131-
context2 = nil
138+
it "returns false when there is no active trace" do
139+
# Use a new Fiber to create a clean context without any active traces
140+
result = Fiber.new do
141+
Traces.active?
142+
end.resume
132143

133-
Traces.trace("span1") do
134-
context1 = Traces.current_context
144+
expect(result).to be == false
145+
end
146+
147+
it "returns false after a trace completes in a new execution context" do
148+
Traces.trace("test") do |span|
149+
# Should be active inside the trace
150+
expect(Traces.active?).to be == true
135151
end
136152

137-
Traces.trace("span2") do
138-
context2 = Traces.current_context
139-
end
153+
# In a new Fiber, should not be active
154+
result = Fiber.new do
155+
Traces.active?
156+
end.resume
140157

141-
# Contexts should be different objects (different spans):
142-
expect(context1).not.to be_equal(context2)
158+
expect(result).to be == false
143159
end
144160
end
161+
end
162+
163+
with "#current_context" do
164+
it "returns current OpenTelemetry context" do
165+
current = Traces.current_context
166+
expect(current).to be_a(::OpenTelemetry::Context)
167+
end
145168

146-
with "#with_context" do
147-
it "executes block within specified context" do
148-
original_context = Traces.current_context
149-
test_context = ::OpenTelemetry::Context.empty
150-
executed = false
151-
152-
Traces.with_context(test_context) do
153-
executed = true
154-
expect(::OpenTelemetry::Context.current).to be_equal(test_context)
155-
end
156-
157-
expect(executed).to be == true
158-
# Context should be restored after block:
159-
expect(::OpenTelemetry::Context.current).to be_equal(original_context)
169+
it "returns different contexts in different spans" do
170+
context1 = nil
171+
context2 = nil
172+
173+
Traces.trace("span1") do
174+
context1 = Traces.current_context
160175
end
161176

162-
it "permanently sets context when called without block" do
163-
original_context = Traces.current_context
164-
test_context = ::OpenTelemetry::Context.empty
165-
166-
token = Traces.with_context(test_context)
177+
Traces.trace("span2") do
178+
context2 = Traces.current_context
179+
end
180+
181+
# Contexts should be different objects (different spans):
182+
expect(context1).not.to be_equal(context2)
183+
end
184+
end
185+
186+
with "#with_context" do
187+
it "executes block within specified context" do
188+
original_context = Traces.current_context
189+
test_context = ::OpenTelemetry::Context.empty
190+
executed = false
191+
192+
Traces.with_context(test_context) do
193+
executed = true
167194
expect(::OpenTelemetry::Context.current).to be_equal(test_context)
168-
169-
# Clean up by detaching:
170-
::OpenTelemetry::Context.detach(token)
171195
end
196+
197+
expect(executed).to be == true
198+
# Context should be restored after block:
199+
expect(::OpenTelemetry::Context.current).to be_equal(original_context)
172200
end
173201

174-
with "#inject" do
175-
it "injects trace context into headers" do
176-
headers = {}
177-
178-
Traces.trace("test") do
179-
Traces.inject(headers)
180-
end
181-
182-
expect(headers).to have_keys(
183-
"traceparent" => be_a(String)
184-
)
185-
186-
traceparent = headers["traceparent"]
187-
expect(traceparent).to be =~ /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
188-
end
202+
it "permanently sets context when called without block" do
203+
original_context = Traces.current_context
204+
test_context = ::OpenTelemetry::Context.empty
189205

190-
it "creates new headers hash when none provided" do
191-
Traces.trace("test") do
192-
headers = Traces.inject()
193-
expect(headers).to be_a(Hash)
194-
expect(headers["traceparent"]).to be =~ /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
195-
end
196-
end
206+
token = Traces.with_context(test_context)
207+
expect(::OpenTelemetry::Context.current).to be_equal(test_context)
197208

198-
it "uses specific context when provided" do
199-
headers = {}
200-
201-
Traces.trace("test") do
202-
specific_context = Traces.current_context
203-
Traces.inject(headers, specific_context)
204-
expect(headers["traceparent"]).to be =~ /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
205-
end
209+
# Clean up by detaching:
210+
::OpenTelemetry::Context.detach(token)
211+
end
212+
end
213+
214+
with "#inject" do
215+
it "injects trace context into headers" do
216+
headers = {}
217+
218+
Traces.trace("test") do
219+
Traces.inject(headers)
206220
end
207221

208-
it "returns nil when no headers provided and no active trace" do
209-
# Clear any active trace:
210-
::OpenTelemetry::Context.clear
211-
212-
result = Traces.inject()
213-
expect(result).to be == nil
222+
expect(headers).to have_keys(
223+
"traceparent" => be_a(String)
224+
)
225+
226+
traceparent = headers["traceparent"]
227+
expect(traceparent).to be =~ /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
228+
end
229+
230+
it "creates new headers hash when none provided" do
231+
Traces.trace("test") do
232+
headers = Traces.inject()
233+
expect(headers).to be_a(Hash)
234+
expect(headers["traceparent"]).to be =~ /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
214235
end
236+
end
237+
238+
it "uses specific context when provided" do
239+
headers = {}
215240

216-
it "returns nil when headers provided but no active trace" do
217-
# Clear any active trace:
218-
::OpenTelemetry::Context.clear
219-
headers = {"existing" => "value"}
220-
221-
result = Traces.inject(headers)
222-
expect(result).to be == nil
223-
# Original headers should remain unchanged:
224-
expect(headers["existing"]).to be == "value"
225-
expect(headers.key?("traceparent")).to be == false
241+
Traces.trace("test") do
242+
specific_context = Traces.current_context
243+
Traces.inject(headers, specific_context)
244+
expect(headers["traceparent"]).to be =~ /^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/
226245
end
227246
end
228247

229-
with "#extract" do
230-
it "extracts context from headers" do
231-
headers = {
248+
it "returns nil when no headers provided and no active trace" do
249+
# Clear any active trace:
250+
::OpenTelemetry::Context.clear
251+
252+
result = Traces.inject()
253+
expect(result).to be == nil
254+
end
255+
256+
it "returns nil when headers provided but no active trace" do
257+
# Clear any active trace:
258+
::OpenTelemetry::Context.clear
259+
headers = {"existing" => "value"}
260+
261+
result = Traces.inject(headers)
262+
expect(result).to be == nil
263+
# Original headers should remain unchanged:
264+
expect(headers["existing"]).to be == "value"
265+
expect(headers.key?("traceparent")).to be == false
266+
end
267+
end
268+
269+
with "#extract" do
270+
it "extracts context from headers" do
271+
headers = {
232272
"traceparent" => "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
233273
}
234-
235-
context = Traces.extract(headers)
236-
expect(context).to be_a(::OpenTelemetry::Context)
237-
238-
# Verify we can use the extracted context:
239-
Traces.with_context(context) do
240-
span = ::OpenTelemetry::Trace.current_span
241-
# Convert binary trace_id to hex for comparison:
242-
trace_id_hex = span.context.trace_id.unpack1("H*")
243-
expect(trace_id_hex).to be == "4bf92f3577b34da6a3ce929d0e0e4736"
244-
end
245-
end
246274

247-
it "returns original context for invalid headers" do
248-
headers = {"traceparent" => "invalid"}
249-
original_context = ::OpenTelemetry::Context.current
250-
251-
result = Traces.extract(headers)
252-
expect(result).to be_equal(original_context)
253-
end
275+
context = Traces.extract(headers)
276+
expect(context).to be_a(::OpenTelemetry::Context)
254277

255-
it "handles missing headers gracefully" do
256-
headers = {}
257-
original_context = ::OpenTelemetry::Context.current
258-
259-
result = Traces.extract(headers)
260-
expect(result).to be_equal(original_context)
278+
# Verify we can use the extracted context:
279+
Traces.with_context(context) do
280+
span = ::OpenTelemetry::Trace.current_span
281+
# Convert binary trace_id to hex for comparison:
282+
trace_id_hex = span.context.trace_id.unpack1("H*")
283+
expect(trace_id_hex).to be == "4bf92f3577b34da6a3ce929d0e0e4736"
261284
end
262285
end
263286

264-
with "round-trip inject/extract" do
265-
it "preserves context through inject and extract cycle" do
266-
original_headers = {}
267-
268-
# Create a trace and inject it:
269-
Traces.trace("test") do
270-
Traces.inject(original_headers)
271-
end
272-
273-
expect(original_headers).to have_keys(
287+
it "returns original context for invalid headers" do
288+
headers = {"traceparent" => "invalid"}
289+
original_context = ::OpenTelemetry::Context.current
290+
291+
result = Traces.extract(headers)
292+
expect(result).to be_equal(original_context)
293+
end
294+
295+
it "handles missing headers gracefully" do
296+
headers = {}
297+
original_context = ::OpenTelemetry::Context.current
298+
299+
result = Traces.extract(headers)
300+
expect(result).to be_equal(original_context)
301+
end
302+
end
303+
304+
with "round-trip inject/extract" do
305+
it "preserves context through inject and extract cycle" do
306+
original_headers = {}
307+
308+
# Create a trace and inject it:
309+
Traces.trace("test") do
310+
Traces.inject(original_headers)
311+
end
312+
313+
expect(original_headers).to have_keys(
274314
"traceparent" => be_a(String)
275315
)
276-
277-
# Extract the context:
278-
extracted_context = Traces.extract(original_headers)
279-
expect(extracted_context).to be_a(::OpenTelemetry::Context)
280-
281-
# Use extracted context to create another trace:
282-
Traces.with_context(extracted_context) do
283-
span = ::OpenTelemetry::Trace.current_span
284-
expect(span.context.remote?).to be == true
285-
end
316+
317+
# Extract the context:
318+
extracted_context = Traces.extract(original_headers)
319+
expect(extracted_context).to be_a(::OpenTelemetry::Context)
320+
321+
# Use extracted context to create another trace:
322+
Traces.with_context(extracted_context) do
323+
span = ::OpenTelemetry::Trace.current_span
324+
expect(span.context.remote?).to be == true
286325
end
287326
end
288327
end

0 commit comments

Comments
 (0)