diff --git a/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/instrumentation.rb b/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/instrumentation.rb index cad8a14a4..d5e1908ac 100644 --- a/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/instrumentation.rb +++ b/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/instrumentation.rb @@ -33,10 +33,12 @@ def gem_version def patch Handlers.subscribe + ActionController::Live.include(Patches::ActionController::Live) end def require_dependencies require_relative 'handlers' + require_relative 'patches/action_controller/live' end def require_railtie diff --git a/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/patches/action_controller/live.rb b/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/patches/action_controller/live.rb new file mode 100644 index 000000000..cea49d262 --- /dev/null +++ b/instrumentation/action_pack/lib/opentelemetry/instrumentation/action_pack/patches/action_controller/live.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module ActionPack + module Patches + module ActionController + # Module to append to ActionController::Live for instrumentation + module Live + def process_action(*) + current_context = OpenTelemetry::Context.current + + # Unset thread local to avoid modifying stack array shared with parent thread + Thread.current[:__opentelemetry_context_storage__] = nil + + attributes = { + OpenTelemetry::SemanticConventions::Trace::CODE_NAMESPACE => self.class.name, + OpenTelemetry::SemanticConventions::Trace::CODE_FUNCTION => action_name + } + + OpenTelemetry::Context.with_current(current_context) do + Instrumentation.instance.tracer.in_span("#{self.class.name}##{action_name} stream", attributes: attributes) do + super + end + end + end + end + end + end + end + end +end diff --git a/instrumentation/action_pack/test/opentelemetry/instrumentation/action_pack/patches/action_controller/live_test.rb b/instrumentation/action_pack/test/opentelemetry/instrumentation/action_pack/patches/action_controller/live_test.rb new file mode 100644 index 000000000..3896c36b7 --- /dev/null +++ b/instrumentation/action_pack/test/opentelemetry/instrumentation/action_pack/patches/action_controller/live_test.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Instrumentation::ActionPack::Patches::ActionController::Live do + include Rack::Test::Methods + + let(:instrumentation) { OpenTelemetry::Instrumentation::ActionPack::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:spans) { exporter.finished_spans } + let(:span) { exporter.finished_spans.last } + let(:rails_app) { DEFAULT_RAILS_APP } + let(:config) { {} } + + # Clear captured spans + before do + OpenTelemetry::Instrumentation::ActionPack::Handlers.unsubscribe + + instrumentation.instance_variable_set(:@config, config) + instrumentation.instance_variable_set(:@installed, false) + + instrumentation.install(config) + + exporter.reset + end + + it 'creates a child span for the new thread' do + get '/stream' + + parent_span = spans[-2] + + _(last_response.ok?).must_equal true + _(span.name).must_equal 'ExampleLiveController#stream stream' + _(span.kind).must_equal :internal + _(span.status.ok?).must_equal true + + _(span.instrumentation_library.name).must_equal 'OpenTelemetry::Instrumentation::ActionPack' + _(span.instrumentation_library.version).must_equal OpenTelemetry::Instrumentation::ActionPack::VERSION + + _(span.attributes['code.namespace']).must_equal 'ExampleLiveController' + _(span.attributes['code.function']).must_equal 'stream' + + _(span.parent_span_id).must_equal parent_span.span_id + end + + def app + rails_app + end +end diff --git a/instrumentation/action_pack/test/test_helpers/controllers.rb b/instrumentation/action_pack/test/test_helpers/controllers.rb index 1f58e148f..03cf03294 100644 --- a/instrumentation/action_pack/test/test_helpers/controllers.rb +++ b/instrumentation/action_pack/test/test_helpers/controllers.rb @@ -5,4 +5,5 @@ # SPDX-License-Identifier: Apache-2.0 require_relative 'controllers/example_controller' +require_relative 'controllers/example_live_controller' require_relative 'controllers/exceptions_controller' diff --git a/instrumentation/action_pack/test/test_helpers/controllers/example_live_controller.rb b/instrumentation/action_pack/test/test_helpers/controllers/example_live_controller.rb new file mode 100644 index 000000000..046b61d46 --- /dev/null +++ b/instrumentation/action_pack/test/test_helpers/controllers/example_live_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +class ExampleLiveController < ActionController::Base + include ActionController::Live + + def stream + response.headers['Content-Type'] = 'text/event-stream' + 10.times do + response.stream.write "hello world\n" + sleep 0.1 + end + ensure + response.stream.close + end +end diff --git a/instrumentation/action_pack/test/test_helpers/routes.rb b/instrumentation/action_pack/test/test_helpers/routes.rb index 07d72708d..e263b7f67 100644 --- a/instrumentation/action_pack/test/test_helpers/routes.rb +++ b/instrumentation/action_pack/test/test_helpers/routes.rb @@ -11,5 +11,6 @@ def draw_routes(rails_app) get '/items/new', to: 'example#new_item' get '/items/:id', to: 'example#item' get '/internal_server_error', to: 'example#internal_server_error' + get '/stream', to: 'example_live#stream' end end