Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion lib/scout_apm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ module ScoutApm
require 'scout_apm/server_integrations/thin'
require 'scout_apm/server_integrations/unicorn'
require 'scout_apm/server_integrations/webrick'
require 'scout_apm/server_integrations/iodine'
require 'scout_apm/server_integrations/null'

require 'scout_apm/background_job_integrations/sidekiq'
Expand All @@ -72,6 +73,7 @@ module ScoutApm

require 'scout_apm/framework_integrations/rails_2'
require 'scout_apm/framework_integrations/rails_3_or_4'
require 'scout_apm/framework_integrations/rage'
require 'scout_apm/framework_integrations/sinatra'
require 'scout_apm/framework_integrations/ruby'

Expand Down Expand Up @@ -102,6 +104,7 @@ module ScoutApm
require 'scout_apm/instruments/middleware_detailed' # Disabled by default, see the file for more details
require 'scout_apm/instruments/rails_router'
require 'scout_apm/instruments/grape'
require 'scout_apm/instruments/rage'
require 'scout_apm/instruments/sinatra'
require 'allocations'

Expand Down Expand Up @@ -233,7 +236,7 @@ class Railtie < Rails::Railtie
if defined?(Prism) || defined?(Parser::TreeRewriter)
ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
require 'scout_apm/auto_instrument'
else
else
# AutoInstruments is turned on, but we don't have the prerequisites to use it
# Prism should be available for Ruby >= 3.3.0
ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled, but Parser::TreeRewriter is missing. Update 'parser' gem to >= 2.5.0.")
Expand All @@ -259,6 +262,25 @@ class Railtie < Rails::Railtie
end
end
end
elsif defined?(::Rage) && defined?(::Rage::Configuration)
# Rage framework integration. Install the agent in an after_initialize hook
# so it runs before Rage::Telemetry.__setup compiles the tracer methods.
# This is analogous to the Rails Railtie initializer above.
::Rage.config.after_initialize do
ScoutApm::Agent.instance.install
end
elsif defined?(::Rage) && !defined?(::Rails)
# Rage gem is loaded but rage/all.rb hasn't run yet, so Rage::Configuration
# isn't available. Intercept Rage.configure (called after rage/all.rb loads)
# to register our after_initialize hook at the right time.
::Rage.singleton_class.prepend(Module.new do
def configure(&block)
super(&block)
config.after_initialize do
ScoutApm::Agent.instance.install
end
end
end)
else
ScoutApm::Agent.instance.install
end
4 changes: 4 additions & 0 deletions lib/scout_apm/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Environment
ScoutApm::ServerIntegrations::Puma.new(STDOUT_LOGGER),
ScoutApm::ServerIntegrations::Thin.new(STDOUT_LOGGER),
ScoutApm::ServerIntegrations::Webrick.new(STDOUT_LOGGER),
ScoutApm::ServerIntegrations::Iodine.new(STDOUT_LOGGER),
ScoutApm::ServerIntegrations::Null.new(STDOUT_LOGGER), # must be last
]

Expand All @@ -38,6 +39,7 @@ class Environment
FRAMEWORK_INTEGRATIONS = [
ScoutApm::FrameworkIntegrations::Rails2.new,
ScoutApm::FrameworkIntegrations::Rails3Or4.new,
ScoutApm::FrameworkIntegrations::Rage.new,
ScoutApm::FrameworkIntegrations::Sinatra.new,
ScoutApm::FrameworkIntegrations::Ruby.new, # Fallback if none match
]
Expand Down Expand Up @@ -111,6 +113,8 @@ def framework_root
RAILS_ROOT.to_s
elsif framework == :rails3_or_4
Rails.root
elsif framework == :rage
::Rage.root.to_s
elsif framework == :sinatra
Sinatra::Application.root || "."
else
Expand Down
4 changes: 2 additions & 2 deletions lib/scout_apm/error_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ def self.capture(exception, params = {})
return if disabled?

context = ScoutApm::Agent.instance.context
return if context.ignored_exceptions.ignore?(exception)
return if context.ignored_exceptions.ignored?(exception)

context.errors_buffer.capture(exception, env)
context.error_buffer.capture(exception, params)
end

def self.enabled?
Expand Down
56 changes: 56 additions & 0 deletions lib/scout_apm/framework_integrations/rage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module ScoutApm
module FrameworkIntegrations
class Rage
def name
:rage
end

def human_name
"Rage"
end

def version
::Rage::VERSION
end

def present?
defined?(::Rage) && defined?(::Rage::VERSION) && !defined?(::Rails)
end

def application_name
nil
end

def env
::Rage.env.to_s
end

def database_engine
return @database_engine if @database_engine
default = :postgres

@database_engine = if defined?(ActiveRecord::Base)
adapter = raw_database_adapter

case adapter.to_s
when "postgres" then :postgres
when "postgresql" then :postgres
when "postgis" then :postgres
when "sqlite3" then :sqlite
when "sqlite" then :sqlite
when "mysql" then :mysql
when "mysql2" then :mysql
when "sqlserver" then :sqlserver
else default
end
else
default
end
end

def raw_database_adapter
ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] rescue nil
end
end
end
end
2 changes: 2 additions & 0 deletions lib/scout_apm/instrument_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def install!
else
install_instrument(ScoutApm::Instruments::MiddlewareSummary)
end
when :rage then
install_instrument(ScoutApm::Instruments::Rage)
end

install_instrument(ScoutApm::Instruments::ActionView)
Expand Down
35 changes: 35 additions & 0 deletions lib/scout_apm/instruments/rage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Instrument wrapper for the Rage framework.
#
# Follows the standard Scout instrument interface (initialize/install/installed?)
# so it can be managed by InstrumentManager. The actual instrumentation is done
# by RageTelemetryHandler, which is registered with Rage's telemetry system.

module ScoutApm
module Instruments
class Rage
attr_reader :context

def initialize(context)
@context = context
@installed = false
end

def installed?
@installed
end

def install(prepend: false)
return unless defined?(::Rage::Telemetry::Handler)
return if @installed

require 'scout_apm/instruments/rage_telemetry_handler'

handler = ScoutApm::Instruments::RageTelemetryHandler.new
::Rage.config.telemetry.use(handler)

@installed = true
context.logger.info("Instrumenting Rage via Telemetry Handler")
end
end
end
end
126 changes: 126 additions & 0 deletions lib/scout_apm/instruments/rage_telemetry_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Rage telemetry handler that creates Scout APM layers for controller actions,
# cable/WebSocket actions, and deferred tasks (background jobs).
#
# Registered with Rage's telemetry system via:
# Rage.config.telemetry.use(ScoutApm::Instruments::RageTelemetryHandler.new)
#
# Each handler method wraps a Rage span with Scout layers, enabling automatic
# tracking of request timing, database queries, external HTTP calls, and errors.

module ScoutApm
module Instruments
class RageTelemetryHandler < ::Rage::Telemetry::Handler
handle "controller.action.process", with: :track_controller
handle "cable.connection.process", with: :track_cable_connection
handle "cable.action.process", with: :track_cable_action
handle "deferred.task.process", with: :track_deferred_task

# Instruments HTTP controller actions.
# Creates a "Controller" root layer (e.g. "UsersController#index").
def track_controller(name:, request:, env:)
req = ScoutApm::RequestManager.lookup
req.annotate_request(:uri => request.path) rescue nil

if ScoutApm::Agent.instance.context.config.value("collect_remote_ip")
req.context.add_user(:ip => env["REMOTE_ADDR"]) rescue nil
end

layer = ScoutApm::Layer.new("Controller", name)
req.start_layer(layer)
begin
result = yield
if result.error?
req.error!
capture_error(result.exception, env)
end
result
rescue => e
req.error!
raise
ensure
req.stop_layer
end
end

# Instruments cable connection lifecycle (connect/disconnect).
# Creates a "Controller" layer (e.g. "ApplicationCable::Connection#connect").
def track_cable_connection(name:, env:)
req = ScoutApm::RequestManager.lookup
layer = ScoutApm::Layer.new("Controller", name)
req.start_layer(layer)
begin
result = yield
if result.error?
req.error!
capture_error(result.exception, env)
end
result
rescue => e
req.error!
raise
ensure
req.stop_layer
end
end

# Instruments cable channel actions (e.g. receiving a message).
# Creates a "Controller" layer (e.g. "ChatChannel#receive").
def track_cable_action(name:, env:)
req = ScoutApm::RequestManager.lookup
layer = ScoutApm::Layer.new("Controller", name)
req.start_layer(layer)
begin
result = yield
if result.error?
req.error!
capture_error(result.exception, env)
end
result
rescue => e
req.error!
raise
ensure
req.stop_layer
end
end

# Instruments deferred task execution (Rage's background job system).
# Creates a "Queue" + "Job" layer pair (e.g. "SendEmailTask#perform").
def track_deferred_task(name:, task_class:)
req = ScoutApm::RequestManager.lookup
req.start_layer(ScoutApm::Layer.new("Queue", "rage_deferred"))
started_queue = true
req.start_layer(ScoutApm::Layer.new("Job", name))
started_job = true
begin
result = yield
if result.error?
req.error!
capture_error(result.exception, {custom_controller: name})
end
result
rescue => e
req.error!
raise
ensure
req.stop_layer if started_job
req.stop_layer if started_queue
end
end

private

def capture_error(exception, env)
return unless exception
return unless ScoutApm::ErrorService.enabled?

context = ScoutApm::Agent.instance.context
return if context.ignored_exceptions.ignored?(exception)

context.error_buffer.capture(exception, env)
rescue
# Don't let error capture failures affect the request
end
end
end
end
26 changes: 20 additions & 6 deletions lib/scout_apm/request_manager.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
# Request manager handles the threadlocal variable that holds the current
# request. If there isn't one, then create one
# request. If there isn't one, then create one.
#
# Under Rage (fiber-per-request concurrency), uses Fiber-local storage
# instead of Thread-local storage to isolate concurrent requests.

module ScoutApm
class RequestManager
STORAGE_KEY = :scout_request

def self.lookup
find || create
end

# Get the current Thread local, and detecting, and not returning a stale request
# Get the current request, detecting and not returning a stale request
def self.find
req = Thread.current[:scout_request]
req = storage[STORAGE_KEY]

if req && (req.stopping? || req.recorded?)
nil
Expand All @@ -18,12 +23,21 @@ def self.find
end
end

# Create a new TrackedRequest object for this thread
# XXX: Figure out who is in charge of creating a `FakeStore` - previously was here
# Create a new TrackedRequest object for this fiber/thread
def self.create
agent_context = ScoutApm::Agent.instance.context
store = agent_context.store
Thread.current[:scout_request] = TrackedRequest.new(agent_context, store)
storage[STORAGE_KEY] = TrackedRequest.new(agent_context, store)
end

# Use Fiber-local storage under Rage (fiber-per-request),
# Thread-local storage everywhere else (thread-per-request).
def self.storage
if defined?(::Rage) && !defined?(::Rails)
Fiber
else
Thread.current
end
end
end
end
Loading
Loading