#--
# Copyright (c) 2016 SolarWinds, LLC.
# All rights reserved.
#++
module AppOpticsAPM
module API
##
# Module to create profiling traces for blocks of code or methods
module Profiling
##
# Public: Profile a given block of code. Detect any exceptions thrown by
# the block and report errors.
#
# ==== Arguments
#
# * +profile_name+ - A name used to identify the block being profiled.
# * +report_kvs+ - A hash containing key/value pairs that will be reported along
# with the event of this profile (optional).
# * +with_backtrace+ - Boolean to indicate whether a backtrace should
# be collected with this trace event.
#
# ==== Example
#
# def computation(n)
# AppOpticsAPM::API.profile('fib', { :n => n }) do
# fib(n)
# end
# end
#
# Returns the result of the block.
#
def profile(profile_name, report_kvs = {}, with_backtrace = false)
return yield unless AppOpticsAPM.tracing?
begin
report_kvs[:Language] ||= :ruby
report_kvs[:ProfileName] ||= profile_name
report_kvs[:Backtrace] = AppOpticsAPM::API.backtrace if with_backtrace
AppOpticsAPM::API.log(nil, :profile_entry, report_kvs)
begin
yield
rescue => e
log_exception(nil, e)
raise
ensure
exit_kvs = {}
exit_kvs[:Language] = :ruby
exit_kvs[:ProfileName] = report_kvs[:ProfileName]
AppOpticsAPM::API.log(nil, :profile_exit, exit_kvs)
end
end
end
##
# Public: Add profiling to a method on a class or module. That method can be of any (accessible)
# type (instance, singleton, private, protected etc.).
#
# ==== Arguments
#
# * +klass+ - the class or module that has the method to profile
# * +method+ - the method to profile. Can be singleton, instance, private etc...
# * +opts+ - a hash specifying the one or more of the following options:
# * +:arguments+ - report the arguments passed to method on each profile (default: false)
# * +:result+ - report the return value of method on each profile (default: false)
# * +:backtrace+ - report the return value of method on each profile (default: false)
# * +:name+ - alternate name for the profile reported in the dashboard (default: method name)
# * +extra_kvs+ - a hash containing any additional key/value pairs you would like reported with the profile
#
# ==== Example
#
# opts = {}
# opts[:backtrace] = true
# opts[:arguments] = false
# opts[:name] = :array_sort
#
# AppOpticsAPM::API.profile_method(Array, :sort, opts)
#
def profile_method(klass, method, opts = {}, extra_kvs = {})
# If we're on an unsupported platform (ahem Mac), just act
# like we did something to nicely play the no-op part.
return true unless AppOpticsAPM.loaded
if !klass.is_a?(Module)
AppOpticsAPM.logger.warn "[appoptics_apm/error] profile_method: Not sure what to do with #{klass}. Send a class or module."
return false
end
if method.is_a?(String)
method = method.to_sym
elsif !method.is_a?(Symbol)
AppOpticsAPM.logger.warn "[appoptics_apm/error] profile_method: Not sure what to do with #{method}. Send a string or symbol for method."
return false
end
instance_method = klass.instance_methods.include?(method) || klass.private_instance_methods.include?(method)
class_method = klass.singleton_methods.include?(method)
# Make sure the request klass::method exists
if !instance_method && !class_method
AppOpticsAPM.logger.warn "[appoptics_apm/error] profile_method: Can't instrument #{klass}.#{method} as it doesn't seem to exist."
AppOpticsAPM.logger.warn "[appoptics_apm/error] #{__FILE__}:#{__LINE__}"
return false
end
# Strip '!' or '?' from method if present
safe_method_name = method.to_s.chop if method.to_s =~ /\?$|\!$/
safe_method_name ||= method
without_appoptics = "#{safe_method_name}_without_appoptics"
with_appoptics = "#{safe_method_name}_with_appoptics"
# Check if already profiled
if klass.instance_methods.include?(with_appoptics.to_sym) ||
klass.singleton_methods.include?(with_appoptics.to_sym)
AppOpticsAPM.logger.warn "[appoptics_apm/error] profile_method: #{klass}::#{method} already profiled."
AppOpticsAPM.logger.warn "[appoptics_apm/error] profile_method: #{__FILE__}:#{__LINE__}"
return false
end
source_location = []
if instance_method
AppOpticsAPM::Util.send_include(klass, AppOpticsAPM::MethodProfiling)
source_location = klass.instance_method(method).source_location
elsif class_method
AppOpticsAPM::Util.send_extend(klass, AppOpticsAPM::MethodProfiling)
source_location = klass.method(method).source_location
end
report_kvs = collect_profile_kvs(klass, method, opts, extra_kvs, source_location)
report_kvs[:MethodName] = safe_method_name
if instance_method
klass.class_eval do
define_method(with_appoptics) do |*args, &block|
profile_wrapper(without_appoptics, report_kvs, opts, *args, &block)
end
alias_method without_appoptics, method.to_s
alias_method method.to_s, with_appoptics
end
elsif class_method
klass.define_singleton_method(with_appoptics) do |*args, &block|
profile_wrapper(without_appoptics, report_kvs, opts, *args, &block)
end
klass.singleton_class.class_eval do
alias_method without_appoptics, method.to_s
alias_method method.to_s, with_appoptics
end
end
true
end
private
##
# Private: Helper method to aggregate KVs to report
#
# klass - the class or module that has the method to profile
# method - the method to profile. Can be singleton, instance, private etc...
# opts - a hash specifying the one or more of the following options:
# * :arguments - report the arguments passed to method on each profile (default: false)
# * :result - report the return value of method on each profile (default: false)
# * :backtrace - report the return value of method on each profile (default: false)
# * :name - alternate name for the profile reported in the dashboard (default: method name)
# extra_kvs - a hash containing any additional KVs you would like reported with the profile
# source_location - array returned from klass.method(:name).source_location
#
def collect_profile_kvs(klass, method, opts, extra_kvs, source_location)
report_kvs = {}
report_kvs[:Language] ||= :ruby
report_kvs[:ProfileName] ||= opts[:name] ? opts[:name] : method
klass.is_a?(Class) ? report_kvs[:Class] = klass.to_s : report_kvs[:Module] = klass.to_s
# If this is a Rails Controller, report the KVs
if defined?(::AbstractController::Base) && klass.ancestors.include?(::AbstractController::Base)
report_kvs[:Controller] = klass.to_s
report_kvs[:Action] = method.to_s
end
# We won't have access to this info for native methods (those not defined in Ruby).
if source_location.is_a?(Array) && source_location.length == 2
report_kvs[:File] = source_location[0]
report_kvs[:LineNumber] = source_location[1]
end
# Merge in any extra_kvs requested
report_kvs.merge!(extra_kvs)
end
# need to set the context to public, otherwise the following `extends` will be private in api.rb
public
end
end
end