#--
# Copyright (c) 2016 SolarWinds, LLC.
# All rights reserved.
#++
module AppOpticsAPM
module SDK
##
# Traces are best created with an AppOpticsAPM::SDK.start_trace block and
# AppOpticsAPM::SDK.trace blocks around calls to be traced.
# These two methods guarantee proper nesting of traces, handling of the tracing context, as well as avoiding
# broken traces in case of exceptions.
#
# Some optional keys that can be used in the +opts+ hash:
# * +:Controller+
# * +:Action+
# * +:HTTP-Host+
# * +:URL+
# * +:Method+
#
# as well as custom keys. The information will show up in the raw data view of a span.
#
# Invalid keys: +:Label+, +:Layer+, +:Edge+, +:Timestamp+, +:Timestamp_u+, +:TransactionName+ (allowed in start_trace)
#
# The methods are exposed as singleton methods for AppOpticsAPM::SDK.
#
# === Usage:
# * +AppOpticsAPM::SDK.appoptics_ready?+
# * +AppOpticsAPM::SDK.get_transaction_name+
# * +AppOpticsAPM::SDK.set_transaction_name+
# * +AppOpticsAPM::SDK.start_trace+
# * +AppOpticsAPM::SDK.start_trace_with_target+
# * +AppOpticsAPM::SDK.trace+
# * +AppOpticsAPM::SDK.tracing?+
#
# === Example:
# class MonthlyCouponEmailJob
# def perform(*args)
#
# # KVs to report to the dashboard
# report_kvs = {}
# report_kvs[:Spec] = :job
# report_kvs[:Controller] = :MonthlyEmailJob
# report_kvs[:Action] = :CouponEmailer
#
# # Start tracing this job with start_trace
# AppOpticsAPM::SDK.start_trace('starling', nil, report_kvs) do
# monthly = MonthlyEmail.new(:CouponEmailer)
#
# # Trace a sub-component of this trace
# AppOpticsAPM::SDK.trace(self.class.name) do
#
# # The work to be done
# users = User.all
# users.each do |u|
# monthly.send(u.email)
# end
#
# end
# end
# end
# end
#
module Tracing
# Trace a given block of code.
#
# Also detects any exceptions thrown by the block and report errors.
#
# === Arguments:
# * +:span+ - The span the block of code belongs to.
# * +:opts+ - (optional) A hash containing key/value pairs that will be reported along with the first event of this span.
# * +:protect_op+ - (optional) The operation being traced. Used to avoid double tracing operations that call each other.
#
# === Example:
#
# def computation_with_appoptics(n)
# AppOpticsAPM::SDK.trace('computation', { :number => n }, :computation) do
# return n if n == 0
# n + computation_with_appoptics(n-1)
# end
# end
#
# result = computation_with_appoptics(100)
#
# === Returns:
# * The result of the block.
#
def trace(span, opts = {}, protect_op = nil)
return yield if !AppOpticsAPM.loaded || !AppOpticsAPM.tracing? || AppOpticsAPM.tracing_layer_op?(protect_op)
opts.delete(:TransactionName)
opts.delete('TransactionName')
AppOpticsAPM::API.log_entry(span, opts, protect_op)
opts[:Backtrace] && opts.delete(:Backtrace) # to avoid sending backtrace twice (faster to check presence here)
begin
yield
rescue Exception => e
AppOpticsAPM::API.log_exception(span, e)
raise
ensure
AppOpticsAPM::API.log_exit(span, opts, protect_op)
end
end
# Collect metrics and start tracing a given block of code.
#
# This will start a trace depending on configuration and probability, detect any exceptions
# thrown by the block, and report errors.
#
# When start_trace returns control to the calling context, the trace will be
# completed and the tracing context will be cleared.
#
# === Arguments:
#
# * +span+ - Name for the span to be used as label in the trace view.
# * +xtrace+ - (optional) incoming X-Trace identifier to be continued.
# * +opts+ - (optional) hash containing key/value pairs that will be reported with this span.
# The value of :TransactionName will set the transaction_name.
#
# === Example:
#
# def handle_request(request, response)
# # ... code that processes request and response ...
# end
#
# def handle_request_with_appoptics(request, response)
# AppOpticsAPM::SDK.start_trace('custom_trace', nil, :TransactionName => 'handle_request') do
# handle_request(request, response)
# end
# end
#
# === Returns:
# * The result of the block.
#
def start_trace(span, xtrace = nil, opts = {})
start_trace_with_target(span, xtrace, {}, opts) { yield }
end
# Collect metrics, trace a given block of code, and assign trace info to target.
#
# This will start a trace depending on configuration and probability, detect any exceptions
# thrown by the block, report errors, and assign an X-Trace to the target.
#
# The motivating use case for this is HTTP streaming in rails3. We need
# access to the exit event's trace id so we can set the header before any
# work is done, and before any headers are sent back to the client.
#
# === Arguments:
# * +span+ - The span the block of code belongs to.
# * +xtrace+ - (optional) incoming X-Trace identifier to be continued.
# * +target+ - (optional) has to respond to #[]=, The target object in which to place the trace information.
# * +opts+ - (optional) hash containing key/value pairs that will be reported with this span.
#
# === Example:
#
# def handle_request(request, response)
# # ... code that processes request and response ...
# end
#
# def handle_request_with_appoptics(request, response)
# AppOpticsAPM::SDK.start_trace_with_target('rails', request['X-Trace'], response) do
# handle_request(request, response)
# end
# end
#
# === Returns:
# * The result of the block.
#
def start_trace_with_target(span, xtrace, target, opts = {})
return yield unless AppOpticsAPM.loaded
if AppOpticsAPM::Context.isValid # not an entry span!
result = trace(span, opts) { yield }
target['X-Trace'] = AppOpticsAPM::Context.toString
return result
end
# :TransactionName and 'TransactionName' need to be removed from opts
AppOpticsAPM.transaction_name = opts.delete('TransactionName') || opts.delete(:TransactionName)
AppOpticsAPM::API.log_start(span, xtrace, opts)
opts[:Backtrace] && opts.delete(:Backtrace) # to avoid sending backtrace twice (faster to check presence here)
# AppOpticsAPM::Event.startTrace creates an Event without an Edge
exit_evt = AppOpticsAPM::Event.startTrace(AppOpticsAPM::Context.get)
result = begin
AppOpticsAPM::API.send_metrics(span, opts) do
target['X-Trace'] = AppOpticsAPM::EventUtil.metadataString(exit_evt)
yield
end
rescue Exception => e
AppOpticsAPM::API.log_exception(span, e)
exit_evt.addEdge(AppOpticsAPM::Context.get)
xtrace = AppOpticsAPM::API.log_end(span, opts, exit_evt)
e.instance_variable_set(:@xtrace, xtrace)
raise
end
exit_evt.addEdge(AppOpticsAPM::Context.get)
AppOpticsAPM::API.log_end(span, opts, exit_evt)
result
end
# Provide a custom transaction name
#
# The AppOpticsAPM gem tries to create meaningful transaction names from controller+action
# or something similar depending on the framework used. However, you may want to override the
# transaction name to better describe your instrumented operation.
#
# Take note that on the dashboard the transaction name is converted to lowercase, and might be
# truncated with invalid characters replaced. Method calls with an empty string or a non-string
# argument won't change the current transaction name.
#
# The configuration +AppOpticsAPM.Config+['transaction_name']+['prepend_domain']+ can be set to
# true to have the domain name prepended to the transaction name when an event or a metric are
# logged. This is a global setting.
#
# === Argument:
#
# * +name+ - A non-empty string with the custom transaction name
#
# === Example:
#
# class DogfoodsController < ApplicationController
#
# def create
# @dogfood = Dogfood.new(params.permit(:brand, :name))
# @dogfood.save
#
# AppOpticsAPM::SDK.set_transaction_name("dogfoodscontroller.create_for_#{params[:brand]}")
#
# redirect_to @dogfood
# end
#
# end
#
# === Returns:
# * (String or nil) the current transaction name
#
def set_transaction_name(name)
if name.is_a?(String) && name.strip != ''
AppOpticsAPM.transaction_name = name
else
AppOpticsAPM.logger.debug "[appoptics_apm/api] Could not set transaction name, provided name is empty or not a String."
end
AppOpticsAPM.transaction_name
end
# Get the currently set custom transaction name.
#
# This is provided for testing
#
# === Returns:
# * (String or nil) the current transaction name (without domain prepended)
#
def get_transaction_name
AppOpticsAPM.transaction_name
end
# Determine if this transaction is being traced.
#
# Tracing puts some extra load on a system, therefor not all transaction are traced.
# The +tracing?+ method helps to determine this so that extra work can be avoided when not tracing.
#
# === Example:
#
# kvs = expensive_info_gathering_method if AppOpticsAPM::SDK.tracing?
# AppOpticsAPM::SDK.trace('some_span', kvs) do
# # this may not create a trace every time it runs
# db_request
# end
#
def tracing?
AppOpticsAPM.tracing?
end
# Wait for AppOptics to be ready to send traces.
#
# This may be useful in short lived background processes when it is important to capture
# information during the whole time the process is running. Usually AppOptics doesn't block an
# application while it is starting up.
#
# === Argument:
#
# * +wait_milliseconds+ (int, default 3000) the maximum time to wait in milliseconds
#
# === Example:
#
# unless AppopticsAPM::SDK.appoptics_ready?(10_000)
# Logger.info "AppOptics not ready after 10 seconds, no metrics will be sent"
# end
#
def appoptics_ready?(wait_milliseconds = 3000)
return false unless AppOpticsAPM.loaded
# These codes are returned by isReady:
# OBOE_SERVER_RESPONSE_UNKNOWN 0
# OBOE_SERVER_RESPONSE_OK 1
# OBOE_SERVER_RESPONSE_TRY_LATER 2
# OBOE_SERVER_RESPONSE_LIMIT_EXCEEDED 3
# OBOE_SERVER_RESPONSE_INVALID_API_KEY 4
# OBOE_SERVER_RESPONSE_CONNECT_ERROR 5
AppopticsAPM::Context.isReady(wait_milliseconds) == 1
end
end
extend Tracing
end
end