Skip to content

Commit

Permalink
Add support for ActionController::Live
Browse files Browse the repository at this point in the history
This commit adds a patch to ensure `ActionController::Live` actions,
which are processed in a new thread, are instrumented as a child span
in the current trace.

This is a patch rather than an `ActiveSupport::Notifications` handler.
Although the `action_controller.process_action` event is processed in
the new thread, we don't have a reference to the controller or anything
to indicate that it is an `ActionController::Live` controller.

The patch needs to be included rather than prepended, as
`ActionController::Live` is a module that is included in each controller
as required by the host app, rather than a class to inherit from. As
such, `include` is the only way to get our `#process_action` method into
the right place.

We need to manually unset the `:__opentelemetry_context_storage__`
thread local, as this gets copied across from the parent thread and
points to the same stack array. Calling `#clear` would modify that
shared stack array, so we need to unset it instead.
  • Loading branch information
thomasmarshall committed Jan 31, 2024
1 parent 734814b commit d278eb4
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions instrumentation/action_pack/test/test_helpers/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit d278eb4

Please sign in to comment.