appsignal/appsignal

View on GitHub
lib/appsignal.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

require "json"
require "securerandom"

require "appsignal/logger"
require "appsignal/helpers/instrumentation"
require "appsignal/helpers/metrics"
require "appsignal/utils/deprecation_message"

# AppSignal for Ruby gem's main module.
#
# Provides method to control the AppSignal instrumentation and the system
# agent. Also provides direct access to instrumentation helpers (from
# {Appsignal::Helpers::Instrumentation}) and metrics helpers (from
# {Appsignal::Helpers::Metrics}) for ease of use.
module Appsignal
  class << self
    include Helpers::Instrumentation
    include Helpers::Metrics
    include Utils::DeprecationMessage

    # Accessor for the AppSignal configuration.
    # Return the current AppSignal configuration.
    #
    # Can return `nil` if no configuration has been set or automatically loaded
    # by an automatic integration or by calling {.start}.
    #
    # @example
    #   Appsignal.config
    #
    # @example Setting the configuration
    #   Appsignal.config = Appsignal::Config.new(Dir.pwd, "production")
    #
    # @return [Config, nil]
    # @see Config
    attr_accessor :config
    # Accessor for toggle if the AppSignal C-extension is loaded.
    #
    # Can be `nil` if extension has not been loaded yet. See
    # {.extension_loaded?} for a boolean return value.
    #
    # @api private
    # @return [Boolean, nil]
    # @see Extension
    # @see extension_loaded?
    attr_accessor :extension_loaded
    # @!attribute [rw] logger
    #   Accessor for the AppSignal logger.
    #
    #   If no logger has been set, it will return a "in memory logger", using
    #   `in_memory_log`. Once AppSignal is started (using {.start}) the
    #   contents of the "in memory logger" is written to the new logger.
    #
    #   @note some classes may have options to set custom loggers. Their
    #     defaults are pointed to this attribute.
    #   @api private
    #   @return [Logger]
    #   @see start_logger
    attr_writer :logger

    # @api private
    def extensions
      @extensions ||= []
    end

    # @api private
    def initialize_extensions
      Appsignal.logger.debug("Initializing extensions")
      extensions.each do |extension|
        Appsignal.logger.debug("Initializing #{extension}")
        extension.initializer
      end
    end

    # @api private
    def testing?
      false
    end

    # Start the AppSignal integration.
    #
    # Starts AppSignal with the given configuration. If no configuration is set
    # yet it will try to automatically load the configuration using the
    # environment loaded from environment variables and the currently working
    # directory.
    #
    # This is not required for the automatic integrations AppSignal offers, but
    # this is required for all non-automatic integrations and pure Ruby
    # applications. For more information, see our [integrations
    # list](http://docs.appsignal.com/ruby/integrations/) and our [Integrating
    # AppSignal](http://docs.appsignal.com/ruby/instrumentation/integrating-appsignal.html)
    # guide.
    #
    # To start the logger see {.start_logger}.
    #
    # @example
    #   Appsignal.start
    #
    # @example with custom loaded configuration
    #   Appsignal.config = Appsignal::Config.new(Dir.pwd, "production")
    #   Appsignal.start
    #
    # @return [void]
    # @since 0.7.0
    def start
      unless extension_loaded?
        logger.info("Not starting appsignal, extension is not loaded")
        return
      end

      logger.debug("Starting appsignal")

      @config ||= Config.new(
        Dir.pwd,
        ENV["APPSIGNAL_APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"]
      )

      if config.valid?
        logger.level =
          if config[:debug]
            Logger::DEBUG
          else
            Logger::INFO
          end
        if config.active?
          logger.info "Starting AppSignal #{Appsignal::VERSION} "\
            "(#{$PROGRAM_NAME}, Ruby #{RUBY_VERSION}, #{RUBY_PLATFORM})"
          config.write_to_environment
          Appsignal::Extension.start
          Appsignal::Hooks.load_hooks
          Appsignal::EventFormatter.initialize_deprecated_formatters
          initialize_extensions

          if config[:enable_allocation_tracking] && !Appsignal::System.jruby?
            Appsignal::Extension.install_allocation_event_hook
          end

          GC::Profiler.enable if config[:enable_gc_instrumentation]

          Appsignal::Minutely.start if config[:enable_minutely_probes]
        else
          logger.info("Not starting, not active for #{config.env}")
        end
      else
        logger.error("Not starting, no valid config for this environment")
      end
    end

    # Stop AppSignal's agent.
    #
    # Stops the AppSignal agent. Call this before the end of your program to
    # make sure the agent is stopped as well.
    #
    # @example
    #   Appsignal.start
    #   # Run your application
    #   Appsignal.stop
    #
    # @param called_by [String] Name of the thing that requested the agent to
    #   be stopped. Will be used in the AppSignal log file.
    # @return [void]
    # @since 1.0.0
    def stop(called_by = nil)
      if called_by
        logger.debug("Stopping appsignal (#{called_by})")
      else
        logger.debug("Stopping appsignal")
      end
      Appsignal::Extension.stop
    end

    def forked
      return unless active?
      Appsignal.start_logger
      logger.debug("Forked process, resubscribing and restarting extension")
      Appsignal::Extension.start
    end

    def get_server_state(key)
      Appsignal::Extension.get_server_state(key)
    end

    # In memory logger used before any logger is started with {.start_logger}.
    #
    # The contents of this logger are flushed to the logger in {.start_logger}.
    #
    # @api private
    # @return [StringIO]
    def in_memory_log
      if defined?(@in_memory_log) && @in_memory_log
        @in_memory_log
      else
        @in_memory_log = StringIO.new
      end
    end

    def logger
      @logger ||= Appsignal::Logger.new(in_memory_log).tap do |l|
        l.level = Logger::INFO
        l.formatter = log_formatter("appsignal")
      end
    end

    # @api private
    def log_formatter(prefix = nil)
      pre = "#{prefix}: " if prefix
      proc do |severity, datetime, _progname, msg|
        "[#{datetime.strftime("%Y-%m-%dT%H:%M:%S")} (process) "\
          "##{Process.pid}][#{severity}] #{pre}#{msg}\n"
      end
    end

    # Start the AppSignal logger.
    #
    # Sets the log level and sets the logger. Uses a file-based logger or the
    # STDOUT-based logger. See the `:log` configuration option.
    #
    # @param path_arg [nil] Deprecated param. Use the `:log_path`
    #   configuration option instead.
    # @return [void]
    # @since 0.7.0
    def start_logger(path_arg = nil)
      if path_arg
        logger.info("Setting the path in start_logger has no effect anymore, set it in the config instead")
      end

      if config && config[:log] == "file" && config.log_file_path
        start_file_logger(config.log_file_path)
      else
        start_stdout_logger
      end

      logger.level =
        if config && config[:debug]
          Logger::DEBUG
        else
          Logger::INFO
        end

      logger << @in_memory_log.string if @in_memory_log
    end

    # Returns if the C-extension was loaded properly.
    #
    # @return [Boolean]
    # @see Extension
    # @since 1.0.0
    def extension_loaded?
      !!extension_loaded
    end

    # Returns the active state of the AppSignal integration.
    #
    # Conditions apply for AppSignal to be marked as active:
    #
    # - There is a config set on the {.config} attribute.
    # - The set config is active {Config.active?}.
    # - The AppSignal Extension is loaded {.extension_loaded?}.
    #
    # This logic is used within instrument helper such as {.instrument} so it's
    # not necessary to wrap {.instrument} calls with this method.
    #
    # @example Do this
    #   Appsignal.instrument(..) do
    #     # Do this
    #   end
    #
    # @example Don't do this
    #   if Appsignal.active?
    #     Appsignal.instrument(..) do
    #       # Don't do this
    #     end
    #   end
    #
    # @return [Boolean]
    # @since 0.2.7
    def active?
      config && config.active? && extension_loaded?
    end

    # @deprecated No replacement
    def is_ignored_error?(error) # rubocop:disable Naming/PredicateName
      deprecation_message "Appsignal.is_ignored_error? is deprecated " \
        "with no replacement and will be removed in version 3.0."
      Appsignal.config[:ignore_errors].include?(error.class.name)
    end
    alias :is_ignored_exception? :is_ignored_error?

    # @deprecated No replacement
    def is_ignored_action?(action) # rubocop:disable Naming/PredicateName
      deprecation_message "Appsignal.is_ignored_action? is deprecated " \
        "with no replacement and will be removed in version 3.0."
      Appsignal.config[:ignore_actions].include?(action)
    end

    private

    def start_stdout_logger
      @logger = Appsignal::Logger.new($stdout)
      logger.formatter = log_formatter("appsignal")
    end

    def start_file_logger(path)
      @logger = Appsignal::Logger.new(path)
      logger.formatter = log_formatter
    rescue SystemCallError => error
      start_stdout_logger
      logger.warn "Unable to start logger with log path '#{path}'."
      logger.warn error
    end
  end
end

require "appsignal/system"
require "appsignal/utils"
require "appsignal/extension"
require "appsignal/auth_check"
require "appsignal/config"
require "appsignal/event_formatter"
require "appsignal/hooks"
require "appsignal/marker"
require "appsignal/minutely"
require "appsignal/garbage_collection_profiler"
require "appsignal/integrations/railtie" if defined?(::Rails)
require "appsignal/integrations/resque"
require "appsignal/integrations/resque_active_job"
require "appsignal/transaction"
require "appsignal/version"
require "appsignal/rack/generic_instrumentation"
require "appsignal/rack/js_exception_catcher"
require "appsignal/js_exception_transaction"
require "appsignal/transmitter"