appsignal/appsignal

View on GitHub
lib/appsignal/helpers/instrumentation.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

module Appsignal
  module Helpers
    module Instrumentation # rubocop:disable Metrics/ModuleLength
      # Creates an AppSignal transaction for the given block.
      #
      # If AppSignal is not {.active?} it will still execute the block, but not
      # create a transaction for it.
      #
      # A event is created for this transaction with the name given in the
      # `name` argument. The event name must start with either `perform_job` or
      # `process_action` to differentiate between the "web" and "background"
      # namespace. Custom namespaces are not supported by this helper method.
      #
      # This helper method also captures any exception that occurs in the given
      # block.
      #
      # @example
      #   Appsignal.monitor_transaction("perform_job.nightly_update") do
      #     # your code
      #   end
      #
      # @example with an environment
      #   Appsignal.monitor_transaction(
      #     "perform_job.nightly_update",
      #     :metadata => { "user_id" => 1 }
      #   ) do
      #     # your code
      #   end
      #
      # @param name [String] main event name.
      # @param env [Hash<Symbol, Object>]
      # @option env [Hash<Symbol/String, Object>] :params Params for the
      #   monitored request/job, see {Appsignal::Transaction#params=} for more
      #   information.
      # @option env [String] :controller name of the controller in which the
      #   transaction was recorded.
      # @option env [String] :class name of the Ruby class in which the
      #   transaction was recorded. If `:controller` is also given,
      #   `:controller` is used instead.
      # @option env [String] :action name of the controller action in which the
      #   transaction was recorded.
      # @option env [String] :method name of the Ruby method in which the
      #   transaction was recorded. If `:action` is also given, `:action`
      #   is used instead.
      # @option env [Integer] :queue_start the moment the request/job was
      #   queued. Used to track how long requests/jobs were queued before being
      #   executed.
      # @option env [Hash<Symbol/String, String/Fixnum>] :metadata Additional
      #   metadata for the transaction, see
      #   {Appsignal::Transaction#set_metadata} for more information.
      # @yield the block to monitor.
      # @raise [Exception] any exception that occurs within the given block is
      #   re-raised by this method.
      # @return [Object] the value of the given block is returned.
      # @since 0.10.0
      def monitor_transaction(name, env = {})
        # Always verify input, even when Appsignal is not active.
        # This makes it more likely invalid arguments get flagged in test/dev
        # environments.
        if name.start_with?("perform_job".freeze)
          namespace = Appsignal::Transaction::BACKGROUND_JOB
          request   = Appsignal::Transaction::GenericRequest.new(env)
        elsif name.start_with?("process_action".freeze)
          namespace = Appsignal::Transaction::HTTP_REQUEST
          request   = ::Rack::Request.new(env)
        else
          logger.error "Unrecognized name '#{name}': names must start with " \
            "either 'perform_job' (for jobs and tasks) or 'process_action' " \
            "(for HTTP requests)"
          return yield
        end

        return yield unless active?

        transaction = Appsignal::Transaction.create(
          SecureRandom.uuid,
          namespace,
          request
        )
        begin
          Appsignal.instrument(name) do
            yield
          end
        rescue Exception => error # rubocop:disable Lint/RescueException
          transaction.set_error(error)
          raise error
        ensure
          transaction.set_http_or_background_action(request.env)
          transaction.set_http_or_background_queue_start
          Appsignal::Transaction.complete_current!
        end
      end

      # Monitor a transaction, stop AppSignal and wait for this single
      # transaction to be flushed.
      #
      # Useful for cases such as Rake tasks and Resque-like systems where a
      # process is forked and immediately exits after the transaction finishes.
      #
      # @see monitor_transaction
      def monitor_single_transaction(name, env = {}, &block)
        monitor_transaction(name, env, &block)
      ensure
        stop("monitor_single_transaction")
      end

      # Listen for an error to occur and send it to AppSignal.
      #
      # Uses {.send_error} to directly send the error in a separate
      # transaction. Does not add the error to the current transaction.
      #
      # Make sure that AppSignal is integrated in your application beforehand.
      # AppSignal won't record errors unless {Config#active?} is `true`.
      #
      # @example
      #   # my_app.rb
      #   # setup AppSignal beforehand
      #
      #   Appsignal.listen_for_error do
      #     # my code
      #     raise "foo"
      #   end
      #
      # @see Transaction.set_tags
      # @see Transaction.set_namespace
      # @see .send_error
      # @see https://docs.appsignal.com/ruby/instrumentation/integrating-appsignal.html
      #   AppSignal integration guide
      #
      # @param tags [Hash, nil]
      # @param namespace [String] the namespace for this error.
      # @yield yields the given block.
      # @return [Object] returns the return value of the given block.
      def listen_for_error(
        tags = nil,
        namespace = Appsignal::Transaction::HTTP_REQUEST
      )
        yield
      rescue Exception => error # rubocop:disable Lint/RescueException
        send_error(error, tags, namespace)
        raise error
      end
      alias :listen_for_exception :listen_for_error

      # Send an error to AppSignal regardless of the context.
      #
      # Records and send the exception to AppSignal.
      #
      # This instrumentation helper does not require a transaction to be
      # active, it starts a new transaction by itself.
      #
      # Use {.set_error} if your want to add an exception to the current
      # transaction.
      #
      # **Note**: Does not do anything if AppSignal is not active or when the
      # "error" is not a class extended from Ruby's Exception class.
      #
      # @example Send an exception
      #   begin
      #     raise "oh no!"
      #   rescue => e
      #     Appsignal.send_error(e)
      #   end
      #
      # @example Send an exception with tags
      #   begin
      #     raise "oh no!"
      #   rescue => e
      #     Appsignal.send_error(e, :key => "value")
      #   end
      #
      # @example Add more metadata to transaction
      #   Appsignal.send_error(e, :key => "value") do |transaction|
      #     transaction.params(:search_query => params[:search_query])
      #     transaction.set_action("my_action_name")
      #     transaction.set_namespace("my_namespace")
      #   end
      #
      # @param error [Exception] The error to send to AppSignal.
      # @param tags [Hash{String, Symbol => String, Symbol, Integer}]
      #   Additional tags to add to the error. See also {.tag_request}.
      # @param namespace [String] The namespace in which the error occurred.
      #   See also {.set_namespace}.
      # @yield [transaction] yields block to allow modification of the
      #   transaction before it's send.
      # @yieldparam transaction [Transaction] yields the AppSignal transaction
      #   used to send the error.
      # @return [void]
      #
      # @see http://docs.appsignal.com/ruby/instrumentation/exception-handling.html
      #   Exception handling guide
      # @see http://docs.appsignal.com/ruby/instrumentation/tagging.html
      #   Tagging guide
      # @since 0.6.0
      def send_error(
        error,
        tags = nil,
        namespace = Appsignal::Transaction::HTTP_REQUEST
      )
        return unless active?
        unless error.is_a?(Exception)
          logger.error "Appsignal.send_error: Cannot send error. The given " \
            "value is not an exception: #{error.inspect}"
          return
        end
        transaction = Appsignal::Transaction.new(
          SecureRandom.uuid,
          namespace,
          Appsignal::Transaction::GenericRequest.new({})
        )
        transaction.set_tags(tags) if tags
        transaction.set_error(error)
        yield transaction if block_given?
        transaction.complete
      end
      alias :send_exception :send_error

      # Set an error on the current transaction.
      #
      # **Note**: Does not do anything if AppSignal is not active, no
      # transaction is currently active or when the "error" is not a class
      # extended from Ruby's Exception class.
      #
      # @example Manual instrumentation of set_error.
      #   # Manually starting AppSignal here
      #   # Manually starting a transaction here.
      #   begin
      #     raise "oh no!"
      #   rescue => e
      #     Appsignal.set_error(error)
      #   end
      #   # Manually completing the transaction here.
      #   # Manually stopping AppSignal here
      #
      # @example In a Rails application
      #   class SomeController < ApplicationController
      #     # The AppSignal transaction is created by our integration for you.
      #     def create
      #       # Do something that breaks
      #     rescue => e
      #       Appsignal.set_error(e)
      #     end
      #   end
      #
      # @param exception [Exception] The error to add to the current
      #   transaction.
      # @param tags [Hash{String, Symbol => String, Symbol, Integer}]
      #   Additional tags to add to the error. See also {.tag_request}.
      # @param namespace [String] The namespace in which the error occurred.
      #   See also {.set_namespace}.
      # @return [void]
      #
      # @see Transaction#set_error
      # @see http://docs.appsignal.com/ruby/instrumentation/exception-handling.html
      #   Exception handling guide
      # @since 0.6.6
      def set_error(exception, tags = nil, namespace = nil)
        unless exception.is_a?(Exception)
          logger.error "Appsignal.set_error: Cannot set error. The given " \
            "value is not an exception: #{exception.inspect}"
          return
        end
        return if !active? || Appsignal::Transaction.current.nil?
        transaction = Appsignal::Transaction.current
        transaction.set_error(exception)
        transaction.set_tags(tags) if tags
        transaction.set_namespace(namespace) if namespace
      end
      alias :set_exception :set_error
      alias :add_exception :set_error

      # Set a custom action name for the current transaction.
      #
      # When using an integration such as the Rails or Sinatra AppSignal will
      # try to find the action name from the controller or endpoint for you.
      #
      # If you want to customize the action name as it appears on AppSignal.com
      # you can use this method. This overrides the action name AppSignal
      # generates in an integration.
      #
      # @example in a Rails controller
      #   class SomeController < ApplicationController
      #     before_action :set_appsignal_action
      #
      #     def set_appsignal_action
      #       Appsignal.set_action("DynamicController#dynamic_method")
      #     end
      #   end
      #
      # @param action [String]
      # @return [void]
      # @see Transaction#set_action
      # @since 2.2.0
      def set_action(action)
        return if !active? ||
            Appsignal::Transaction.current.nil? ||
            action.nil?
        Appsignal::Transaction.current.set_action(action)
      end

      # Set a custom namespace for the current transaction.
      #
      # When using an integration such as Rails or Sidekiq AppSignal will try
      # to find a appropriate namespace for the transaction.
      #
      # A Rails controller will be automatically put in the "http_request"
      # namespace, while a Sidekiq background job is put in the
      # "background_job" namespace.
      #
      # Note: The "http_request" namespace gets transformed on AppSignal.com to
      # "Web" and "background_job" gets transformed to "Background".
      #
      # If you want to customize the namespace in which transactions appear you
      # can use this method. This overrides the namespace AppSignal uses by
      # default.
      #
      # A common request we've seen is to split the administration panel from
      # the main application.
      #
      # @example create a custom admin namespace
      #   class AdminController < ApplicationController
      #     before_action :set_appsignal_namespace
      #
      #     def set_appsignal_namespace
      #       Appsignal.set_namespace("admin")
      #     end
      #   end
      #
      # @param namespace [String]
      # @return [void]
      # @see Transaction#set_namespace
      # @since 2.2.0
      def set_namespace(namespace)
        return if !active? ||
            Appsignal::Transaction.current.nil? ||
            namespace.nil?
        Appsignal::Transaction.current.set_namespace(namespace)
      end

      # Set tags on the current transaction.
      #
      # Tags are extra bits of information that are added to transaction and
      # appear on sample details pages on AppSignal.com.
      #
      # @example
      #   Appsignal.tag_request(:locale => "en")
      #   Appsignal.tag_request("locale" => "en")
      #   Appsignal.tag_request("user_id" => 1)
      #
      # @example Nested hashes are not supported
      #   # Bad
      #   Appsignal.tag_request(:user => { :locale => "en" })
      #
      # @example in a Rails controller
      #   class SomeController < ApplicationController
      #     before_action :set_appsignal_tags
      #
      #     def set_appsignal_tags
      #       Appsignal.tag_request(:locale => I18n.locale)
      #     end
      #   end
      #
      # @param tags [Hash] Collection of tags.
      # @option tags [String, Symbol, Integer] :any
      #   The name of the tag as a Symbol.
      # @option tags [String, Symbol, Integer] "any"
      #   The name of the tag as a String.
      # @return [void]
      #
      # @see Transaction.set_tags
      # @see http://docs.appsignal.com/ruby/instrumentation/tagging.html
      #   Tagging guide
      def tag_request(tags = {})
        return unless active?
        transaction = Appsignal::Transaction.current
        return false unless transaction
        transaction.set_tags(tags)
      end
      alias :tag_job :tag_request

      # Instrument helper for AppSignal.
      #
      # For more help, read our custom instrumentation guide, listed under "See
      # also".
      #
      # @example Simple instrumentation
      #   Appsignal.instrument("fetch.issue_fetcher") do
      #     # To be instrumented code
      #   end
      #
      # @example Instrumentation with title and body
      #   Appsignal.instrument(
      #     "fetch.issue_fetcher",
      #     "Fetching issue",
      #     "GitHub API"
      #   ) do
      #     # To be instrumented code
      #   end
      #
      # @param name [String] Name of the instrumented event. Read our event
      #   naming guide listed under "See also".
      # @param title [String, nil] Human readable name of the event.
      # @param body [String, nil] Value of importance for the event, such as
      #   the server against an API call is made.
      # @param body_format [Integer] Enum for the type of event that is
      #   instrumented. Accepted values are {EventFormatter::DEFAULT} and
      #   {EventFormatter::SQL_BODY_FORMAT}, but we recommend you use
      #   {.instrument_sql} instead of {EventFormatter::SQL_BODY_FORMAT}.
      # @yield yields the given block of code instrumented in an AppSignal
      #   event.
      # @return [Object] Returns the block's return value.
      #
      # @see Appsignal::Transaction#instrument
      # @see .instrument_sql
      # @see http://docs.appsignal.com/ruby/instrumentation/instrumentation.html
      #   AppSignal custom instrumentation guide
      # @see http://docs.appsignal.com/api/event-names.html
      #   AppSignal event naming guide
      # @since 1.3.0
      def instrument(
        name,
        title = nil,
        body = nil,
        body_format = Appsignal::EventFormatter::DEFAULT
      )
        Appsignal::Transaction.current.start_event
        yield if block_given?
      ensure
        Appsignal::Transaction
          .current
          .finish_event(name, title, body, body_format)
      end

      # Instrumentation helper for SQL queries.
      #
      # This helper filters out values from SQL queries so you don't have to.
      #
      # @example SQL query instrumentation
      #   body = "SELECT * FROM ..."
      #   Appsignal.instrument_sql("perform.query", nil, body) do
      #     # To be instrumented code
      #   end
      #
      # @example SQL query instrumentation
      #   body = "WHERE email = 'foo@..'"
      #   Appsignal.instrument_sql("perform.query", nil, body) do
      #     # query value will replace 'foo..' with a question mark `?`.
      #   end
      #
      # @param name [String] Name of the instrumented event. Read our event
      #   naming guide listed under "See also".
      # @param title [String, nil] Human readable name of the event.
      # @param body [String, nil] SQL query that's being executed.
      # @yield yields the given block of code instrumented in an AppSignal
      #   event.
      # @return [Object] Returns the block's return value.
      #
      # @see .instrument
      # @see http://docs.appsignal.com/ruby/instrumentation/instrumentation.html
      #   AppSignal custom instrumentation guide
      # @see http://docs.appsignal.com/api/event-names.html
      #   AppSignal event naming guide
      # @since 2.0.0
      def instrument_sql(name, title = nil, body = nil, &block)
        instrument(
          name,
          title,
          body,
          Appsignal::EventFormatter::SQL_BODY_FORMAT,
          &block
        )
      end

      # Convenience method for skipping instrumentations around a block of code.
      #
      # @example
      #   Appsignal.without_instrumentation do
      #     # Complex code here
      #   end
      #
      # @yield block of code that shouldn't be instrumented.
      # @return [Object] Returns the return value of the block.
      # @since 0.8.7
      def without_instrumentation
        Appsignal::Transaction.current.pause! if Appsignal::Transaction.current
        yield
      ensure
        Appsignal::Transaction.current.resume! if Appsignal::Transaction.current
      end
    end
  end
end