Skip to content
Merged
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
3 changes: 3 additions & 0 deletions app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@
# Initialize the app
require_relative 'init'

# Custom method tracers (must load after init so traced methods exist)
require_relative 'config/newrelic_method_tracers'

# Enter console mode
if settings.environment == :console
require 'rack/test'
Expand Down
65 changes: 65 additions & 0 deletions config/newrelic_method_tracers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Custom New Relic method tracers for hot request paths.
#
# High-traffic transactions (GET /search, GET .../classes/{cls}/tree)
# spend most of their time in application code that the agent reports as
# a single opaque "Sinatra::Application#GET (unknown)" segment. These
# tracers split that block into named Custom/* child segments so
# transaction traces and the breakdown table show where the time goes.
#
# To instrument another endpoint, add its module/class and methods to the
# registry below; test/helpers/test_newrelic_method_tracers.rb consumes
# the same registry, so new entries are covered automatically.
#
# Must be loaded after init.rb so all traced methods are already defined.
# When the agent is disabled (development/test), the tracers are no-ops.
#
# Kill switch: set NEWRELIC_CUSTOM_TRACERS=off (and restart) to skip
# registration entirely, e.g. to rule instrumentation out while
# debugging. Requires a process restart to take effect either way.
module NewRelicMethodTracers
# Instance methods, keyed by module/class name. Methods inherited from
# concerns or mixins (e.g. the tree methods, defined in an
# ontologies_linked_data concern) can be traced here without touching
# the defining repo.
INSTANCE_METHODS = {
# GET /search pipeline
'Sinatra::Helpers::SearchHelper' => %i[process_search get_term_search_query
add_matched_fields filter_attrs_by_language],
# App-wide: ontology access list load + response serialization entry
'Sinatra::Helpers::ApplicationHelper' => %i[restricted_ontologies reply],
# GET /ontologies/{ontology}/classes/{cls}/tree pipeline
'LinkedData::Models::Class' => %i[tree tree_sorted paths_to_root]
}.freeze

# Class (singleton) methods, keyed by class name.
CLASS_METHODS = {
# Hypermedia links + JSON generation for every API response
'LinkedData::Serializer' => %i[build_response serialize],
# Solr query execution incl. Ruby-side response parsing (the HTTP
# call itself already appears as an External segment)
'LinkedData::Models::Class' => %i[search]
}.freeze

def self.register
INSTANCE_METHODS.each do |const_name, methods|
install(Object.const_get(const_name), const_name, methods)
end

CLASS_METHODS.each do |const_name, methods|
install(Object.const_get(const_name).singleton_class, const_name, methods)
end
end

def self.install(target, const_name, methods)
target.include(::NewRelic::Agent::MethodTracer)

methods.each do |method_name|
target.send(:add_method_tracer, method_name, "Custom/#{const_name}/#{method_name}")
end
end
end

unless ENV['NEWRELIC_CUSTOM_TRACERS'] == 'off'
require 'new_relic/agent/method_tracer'
NewRelicMethodTracers.register
end
6 changes: 6 additions & 0 deletions helpers/search_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ def process_search(params = nil)
params.delete('query')
text = params["q"]

# The agent strips query strings from request.uri, so record whether
# this search is ontology-scoped as a queryable transaction attribute
if defined?(::NewRelic::Agent)
::NewRelic::Agent.add_custom_attributes(search_scoped: !params[ONTOLOGIES_PARAM].to_s.empty?)
end

query = get_term_search_query(text, params)
# puts "Edismax query: #{query}, params: #{params}"
set_page_params(params)
Expand Down
45 changes: 45 additions & 0 deletions test/helpers/test_newrelic_method_tracers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require_relative '../test_case_helpers'

# Guards the custom New Relic instrumentation registered by
# config/newrelic_method_tracers.rb. If a traced method is renamed or
# moved, add_method_tracer logs a warning and silently no-ops, so without
# these assertions the instrumentation could vanish unnoticed while the
# rest of the suite stays green.
#
# The tests iterate the registry itself, so entries added to
# NewRelicMethodTracers::INSTANCE_METHODS / CLASS_METHODS are covered
# automatically.
class TestNewRelicMethodTracers < TestCaseHelpers

def test_instance_method_tracers_registered
NewRelicMethodTracers::INSTANCE_METHODS.each do |const_name, methods|
mod = Object.const_get(const_name)
methods.each do |method_name|
assert_wrapped_by_newrelic(mod.instance_method(method_name),
"#{const_name}##{method_name}")
end
end
end

def test_class_method_tracers_registered
NewRelicMethodTracers::CLASS_METHODS.each do |const_name, methods|
singleton = Object.const_get(const_name).singleton_class
methods.each do |method_name|
assert_wrapped_by_newrelic(singleton.instance_method(method_name),
"#{const_name}.#{method_name}")
end
end
end

private

# The tracer wrapper is defined inside the newrelic_rpm gem, so the
# wrapped method's source_location must point there. This distinguishes
# a genuine tracer from the untraced original (defined in this repo or
# its gems) and from any other module that happens to prepend.
def assert_wrapped_by_newrelic(unbound_method, label)
source_file = unbound_method.source_location&.first.to_s
assert_match(/newrelic/, source_file,
"#{label} is not wrapped by a newrelic_rpm method tracer (source: #{source_file})")
end
end