diff --git a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/dup/event_handler.rb b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/dup/event_handler.rb index b86b9c9710..1b3c75a08d 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/dup/event_handler.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/dup/event_handler.rb @@ -43,7 +43,7 @@ module Dup class EventHandler include ::Rack::Events::Abstract - OTEL_TOKEN_AND_SPAN = 'otel.rack.token_and_span' + OTEL_CONTEXT_INFO = 'otel.context_info' EMPTY_HASH = {}.freeze # Creates a server span for this current request using the incoming parent context @@ -62,7 +62,8 @@ def on_start(request, _) span = create_span(parent_context, request) span_ctx = OpenTelemetry::Trace.context_with_span(span, parent_context: parent_context) rack_ctx = OpenTelemetry::Instrumentation::Rack.context_with_span(span, parent_context: span_ctx) - request.env[OTEL_TOKEN_AND_SPAN] = [OpenTelemetry::Context.attach(rack_ctx), span] + token = OpenTelemetry::Context.attach(rack_ctx) + request.env[OTEL_CONTEXT_INFO] = [Fiber.current, token, span] rescue StandardError => e OpenTelemetry.handle_error(exception: e) end @@ -209,11 +210,17 @@ def request_span_attributes(env) end def detach_context(request) - return nil unless request.env[OTEL_TOKEN_AND_SPAN] + otel_context_info = request.env[OTEL_CONTEXT_INFO] + return unless otel_context_info - token, span = request.env[OTEL_TOKEN_AND_SPAN] + original_fiber, token, span = otel_context_info span.finish - OpenTelemetry::Context.detach(token) + + if Fiber.current.equal?(original_fiber) + OpenTelemetry::Context.detach(token) + else + OpenTelemetry.logger.debug { '[rack] Skipping context detach: response processed in different fiber than request.' } + end rescue StandardError => e OpenTelemetry.handle_error(exception: e) end diff --git a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/old/event_handler.rb b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/old/event_handler.rb index 145cc9d034..e3cd132ae8 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/old/event_handler.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/old/event_handler.rb @@ -43,7 +43,7 @@ module Old class EventHandler include ::Rack::Events::Abstract - OTEL_TOKEN_AND_SPAN = 'otel.rack.token_and_span' + OTEL_CONTEXT_INFO = 'otel.context_info' EMPTY_HASH = {}.freeze # Creates a server span for this current request using the incoming parent context @@ -62,7 +62,8 @@ def on_start(request, _) span = create_span(parent_context, request) span_ctx = OpenTelemetry::Trace.context_with_span(span, parent_context: parent_context) rack_ctx = OpenTelemetry::Instrumentation::Rack.context_with_span(span, parent_context: span_ctx) - request.env[OTEL_TOKEN_AND_SPAN] = [OpenTelemetry::Context.attach(rack_ctx), span] + token = OpenTelemetry::Context.attach(rack_ctx) + request.env[OTEL_CONTEXT_INFO] = [Fiber.current, token, span] rescue StandardError => e OpenTelemetry.handle_error(exception: e) end @@ -198,11 +199,17 @@ def request_span_attributes(env) end def detach_context(request) - return nil unless request.env[OTEL_TOKEN_AND_SPAN] + otel_context_info = request.env[OTEL_CONTEXT_INFO] + return unless otel_context_info - token, span = request.env[OTEL_TOKEN_AND_SPAN] + original_fiber, token, span = otel_context_info span.finish - OpenTelemetry::Context.detach(token) + + if Fiber.current.equal?(original_fiber) + OpenTelemetry::Context.detach(token) + else + OpenTelemetry.logger.debug { '[rack] Skipping context detach: response processed in different fiber than request.' } + end rescue StandardError => e OpenTelemetry.handle_error(exception: e) end diff --git a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/stable/event_handler.rb b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/stable/event_handler.rb index 5780aab47d..dff0dc8b5d 100644 --- a/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/stable/event_handler.rb +++ b/instrumentation/rack/lib/opentelemetry/instrumentation/rack/middlewares/stable/event_handler.rb @@ -43,7 +43,7 @@ module Stable class EventHandler include ::Rack::Events::Abstract - OTEL_TOKEN_AND_SPAN = 'otel.rack.token_and_span' + OTEL_CONTEXT_INFO = 'otel.context_info' EMPTY_HASH = {}.freeze # Creates a server span for this current request using the incoming parent context @@ -62,7 +62,8 @@ def on_start(request, _) span = create_span(parent_context, request) span_ctx = OpenTelemetry::Trace.context_with_span(span, parent_context: parent_context) rack_ctx = OpenTelemetry::Instrumentation::Rack.context_with_span(span, parent_context: span_ctx) - request.env[OTEL_TOKEN_AND_SPAN] = [OpenTelemetry::Context.attach(rack_ctx), span] + token = OpenTelemetry::Context.attach(rack_ctx) + request.env[OTEL_CONTEXT_INFO] = [Fiber.current, token, span] rescue StandardError => e OpenTelemetry.handle_error(exception: e) end @@ -197,11 +198,17 @@ def request_span_attributes(env) end def detach_context(request) - return nil unless request.env[OTEL_TOKEN_AND_SPAN] + otel_context_info = request.env[OTEL_CONTEXT_INFO] + return unless otel_context_info - token, span = request.env[OTEL_TOKEN_AND_SPAN] + original_fiber, token, span = otel_context_info span.finish - OpenTelemetry::Context.detach(token) + + if Fiber.current.equal?(original_fiber) + OpenTelemetry::Context.detach(token) + else + OpenTelemetry.logger.debug { '[rack] Skipping context detach: response processed in different fiber than request.' } + end rescue StandardError => e OpenTelemetry.handle_error(exception: e) end diff --git a/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/dup/event_handler_test.rb b/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/dup/event_handler_test.rb index f96ed7939c..ba2f4aeadc 100644 --- a/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/dup/event_handler_test.rb +++ b/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/dup/event_handler_test.rb @@ -508,4 +508,38 @@ class EventHandlerError < StandardError _(proxy_event).must_be_nil end end + + describe 'cross-fiber context detachment' do + it 'finishes span without error when on_finish is called from different fiber' do + request = Rack::MockRequest.env_for('/') + rack_request = Rack::Request.new(request) + response = Rack::Response.new + + fiber1 = Fiber.new do + handler.on_start(rack_request, nil) + Fiber.yield + end + fiber1.resume + + fiber2 = Fiber.new do + handler.on_finish(rack_request, response) + end + fiber2.resume + + _(finished_spans.size).must_equal 1 + _(rack_span.name).must_equal 'GET' + end + + it 'detaches context normally when same fiber' do + request = Rack::MockRequest.env_for('/') + rack_request = Rack::Request.new(request) + response = Rack::Response.new + + handler.on_start(rack_request, nil) + handler.on_finish(rack_request, response) + + _(finished_spans.size).must_equal 1 + _(rack_span.name).must_equal 'GET' + end + end end diff --git a/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/old/event_handler_test.rb b/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/old/event_handler_test.rb index b983921e0c..429b5bb936 100644 --- a/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/old/event_handler_test.rb +++ b/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/old/event_handler_test.rb @@ -483,4 +483,38 @@ class EventHandlerError < StandardError _(proxy_event).must_be_nil end end + + describe 'cross-fiber context detachment' do + it 'finishes span without error when on_finish is called from different fiber' do + request = Rack::MockRequest.env_for('/') + rack_request = Rack::Request.new(request) + response = Rack::Response.new + + fiber1 = Fiber.new do + handler.on_start(rack_request, nil) + Fiber.yield + end + fiber1.resume + + fiber2 = Fiber.new do + handler.on_finish(rack_request, response) + end + fiber2.resume + + _(finished_spans.size).must_equal 1 + _(rack_span.name).must_equal 'HTTP GET' + end + + it 'detaches context normally when same fiber' do + request = Rack::MockRequest.env_for('/') + rack_request = Rack::Request.new(request) + response = Rack::Response.new + + handler.on_start(rack_request, nil) + handler.on_finish(rack_request, response) + + _(finished_spans.size).must_equal 1 + _(rack_span.name).must_equal 'HTTP GET' + end + end end diff --git a/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/stable/event_handler_test.rb b/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/stable/event_handler_test.rb index 0316898226..456de4b815 100644 --- a/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/stable/event_handler_test.rb +++ b/instrumentation/rack/test/opentelemetry/instrumentation/rack/middlewares/stable/event_handler_test.rb @@ -479,4 +479,38 @@ class EventHandlerError < StandardError _(proxy_event).must_be_nil end end + + describe 'cross-fiber context detachment' do + it 'finishes span without error when on_finish is called from different fiber' do + request = Rack::MockRequest.env_for('/') + rack_request = Rack::Request.new(request) + response = Rack::Response.new + + fiber1 = Fiber.new do + handler.on_start(rack_request, nil) + Fiber.yield + end + fiber1.resume + + fiber2 = Fiber.new do + handler.on_finish(rack_request, response) + end + fiber2.resume + + _(finished_spans.size).must_equal 1 + _(rack_span.name).must_equal 'GET' + end + + it 'detaches context normally when same fiber' do + request = Rack::MockRequest.env_for('/') + rack_request = Rack::Request.new(request) + response = Rack::Response.new + + handler.on_start(rack_request, nil) + handler.on_finish(rack_request, response) + + _(finished_spans.size).must_equal 1 + _(rack_span.name).must_equal 'GET' + end + end end