# A struct holding the information required to measure a controller
# action. This is put on the thread local. Handles the issue of
# re-entrancy, or nested action calls.
#
# This class is not part of the public API. Avoid making calls on it directly.
#
module NewRelic::Agent::Instrumentation
class MetricFrame
attr_accessor :start, :apdex_start, :exception,
:filtered_params, :force_flag,
:jruby_cpu_start, :process_cpu_start, :database_metric_name
# Return the currently active metric frame, or nil. Call with +true+
# to create a new metric frame if one is not already on the thread.
def self.current(create_if_empty=nil)
Thread.current[:newrelic_metric_frame] ||= create_if_empty && new
end
# This is the name of the model currently assigned to database
# measurements, overriding the default.
def self.database_metric_name
current && current.database_metric_name
end
@@java_classes_loaded = false
if defined? JRuby
begin
require 'java'
include_class 'java.lang.management.ManagementFactory'
include_class 'com.sun.management.OperatingSystemMXBean'
@@java_classes_loaded = true
rescue Exception => e
end
end
attr_reader :depth
def initialize
@start = Time.now.to_f
@path_stack = [] # stack of [controller, path] elements
@jruby_cpu_start = jruby_cpu_time
@process_cpu_start = process_cpu
end
# Indicate that we are entering a measured controller action or task.
# Make sure you unwind every push with a pop call.
def push(category, path)
NewRelic::Agent.instance.transaction_sampler.notice_first_scope_push(start)
@path_stack.push [category, path]
end
# Indicate that you don't want to keep the currently saved transaction
# information
def self.abort_transaction!
current.abort_transaction! if current
end
# Give the current metric frame a request context. Use this to
# get the URI and referer. The request is interpreted loosely
# as a Rack::Request or an ActionController::AbstractRequest.
def request=(request)
@request = request
end
# For the current web transaction, return the path of the URI minus the host part and query string, or nil.
def uri
return @uri if @uri || @request.nil?
approximate_uri = case
when @request.respond_to?(:url) then @request.url
when @request.respond_to?(:uri) then @request.uri
when @request.respond_to?(:fullpath) then @request.fullpath
when @request.respond_to?(:path) then @request.path
end
@uri = approximate_uri.split('?').first || '/' if approximate_uri
end
# For the current web transaction, return the full referer, minus the host string, or nil.
def referer
return @referer if @referer || @request.nil? || !@request.respond_to?(:referer) || !@request.referer
@referer = @request.referer.split('?').first
end
# Call this to ensure that the current transaction is not saved
def abort_transaction!
NewRelic::Agent.instance.transaction_sampler.ignore_transaction
end
# This needs to be called after entering the call to trace the controller action, otherwise
# the controller action blames itself. It gets reset in the normal #pop call.
def start_transaction
NewRelic::Agent.instance.stats_engine.start_transaction metric_name
# Only push the transaction context info once, on entry:
if @path_stack.size == 1
NewRelic::Agent.instance.transaction_sampler.notice_transaction(metric_name, uri, filtered_params)
end
end
def category
@path_stack.last.first
end
def path
@path_stack.last.last
end
# Unwind one stack level. It knows if it's back at the outermost caller and
# does the appropriate wrapup of the context.
def pop
category, path = @path_stack.pop
if category.nil?
NewRelic::Control.instance.log.error "Underflow in metric frames: #{caller.join("\n ")}"
end
if @path_stack.empty?
if NewRelic::Agent.is_execution_traced?
cpu_burn = nil
if @process_cpu_start
cpu_burn = process_cpu - @process_cpu_start
elsif @jruby_cpu_start
cpu_burn = jruby_cpu_time - @jruby_cpu_start
NewRelic::Agent.get_stats_no_scope(NewRelic::Metrics::USER_TIME).record_data_point(cpu_burn)
end
NewRelic::Agent.instance.transaction_sampler.notice_transaction_cpu_time(cpu_burn) if cpu_burn
NewRelic::Agent.instance.histogram.process(Time.now.to_f - start) if recording_web_transaction?(category)
NewRelic::Agent.instance.transaction_sampler.notice_scope_empty
end
NewRelic::Agent.instance.stats_engine.end_transaction
Thread.current[:newrelic_metric_frame] = nil
else # path stack not empty
# change the transaction name back to whatever was on the stack.
NewRelic::Agent.instance.stats_engine.scope_name = metric_name
end
end
# If we have an active metric frame, notice the error and increment the error metric.
def self.notice_error(e, custom_params=nil)
if current
current.notice_error(e, custom_params)
else
NewRelic::Agent.instance.error_collector.notice_error(e, :custom_params => custom_params)
end
end
def notice_error(e, custom_params=nil)
if exception != e
NewRelic::Agent.instance.error_collector.notice_error(e, :referer => referer, :uri => uri, :metric => metric_name, :request_params => filtered_params, :custom_params => custom_params)
self.exception = e
end
end
# Add context parameters to the metric frame. This information will be passed in to errors
# and transaction traces. Keys and Values should be strings, numbers or date/times.
def self.add_custom_parameters(p)
current.add_custom_parameters(p) if current
end
def self.custom_parameters
(current && current.custom_parameters) ? current.custom_parameters : {}
end
def record_apdex
return unless recording_web_transaction? && NewRelic::Agent.is_execution_traced?
ending = Time.now.to_f
summary_stat = NewRelic::Agent.instance.stats_engine.get_custom_stats("Apdex", NewRelic::ApdexStats)
controller_stat = NewRelic::Agent.instance.stats_engine.get_custom_stats("Apdex/#{path}", NewRelic::ApdexStats)
update_apdex(summary_stat, ending - apdex_start, exception)
update_apdex(controller_stat, ending - start, exception)
end
def metric_name
return nil if @path_stack.empty?
category + '/' + path
end
# Return the array of metrics to record for the current metric frame.
def recorded_metrics
metrics = [ metric_name ]
if @path_stack.size == 1
if recording_web_transaction?
metrics += ["HttpDispatcher"]
else
metrics += ["#{category}/all", "OtherTransaction/all"]
end
end
metrics
end
# Yield to a block that is run with a database metric name context. This means
# the Database instrumentation will use this for the metric name if it does not
# otherwise know about a model. This is re-entrant.
#
# * model is the DB model class
# * method is the name of the finder method or other method to identify the operation with.
#
def with_database_metric_name(model, method)
previous = @database_metric_name
model_name = case model
when Class
model.name
when String
model
else
model.to_s
end
@database_metric_name = "ActiveRecord/#{model_name}/#{method}"
yield
ensure
@database_metric_name=previous
end
def custom_parameters
@custom_parameters ||= {}
end
def add_custom_parameters(p)
custom_parameters.merge!(p)
end
def self.recording_web_transaction?
if c = Thread.current[:newrelic_metric_frame]
c.recording_web_transaction?
end
end
def recording_web_transaction?(cat = category)
0 == cat.index("Controller")
end
private
def update_apdex(stat, duration, failed)
apdex_t = NewRelic::Control.instance.apdex_t
case
when failed
stat.record_apdex_f
when duration <= apdex_t
stat.record_apdex_s
when duration <= 4 * apdex_t
stat.record_apdex_t
else
stat.record_apdex_f
end
end
def process_cpu
return nil if defined? JRuby
p = Process.times
p.stime + p.utime
end
def jruby_cpu_time # :nodoc:
return nil unless @@java_classes_loaded
threadMBean = ManagementFactory.getThreadMXBean()
java_utime = threadMBean.getCurrentThreadUserTime() # ns
-1 == java_utime ? 0.0 : java_utime/1e9
end
end
end