lib/elastic_apm/agent.rb



# frozen_string_literal: true

require 'elastic_apm/context_builder'
require 'elastic_apm/error_builder'
require 'elastic_apm/stacktrace_builder'
require 'elastic_apm/error'
require 'elastic_apm/transport/base'
require 'elastic_apm/spies'
require 'elastic_apm/metrics'

module ElasticAPM
  # rubocop:disable Metrics/ClassLength
  # @api private
  class Agent
    include Logging

    LOCK = Mutex.new

    # life cycle

    def self.instance # rubocop:disable Style/TrivialAccessors
      @instance
    end

    # rubocop:disable Metrics/MethodLength
    def self.start(config)
      return @instance if @instance

      config = Config.new(config) unless config.is_a?(Config)

      LOCK.synchronize do
        return @instance if @instance

        unless config.active?
          config.logger.debug format(
            "%sAgent disabled with `active: false'",
            Logging::PREFIX
          )
          return
        end

        @instance = new(config).start
      end
    end
    # rubocop:enable Metrics/MethodLength

    def self.stop
      LOCK.synchronize do
        return unless @instance

        @instance.stop
        @instance = nil
      end
    end

    def self.running?
      !!@instance
    end

    def initialize(config)
      @config = config

      @stacktrace_builder = StacktraceBuilder.new(config)
      @context_builder = ContextBuilder.new(config)
      @error_builder = ErrorBuilder.new(self)

      @transport = Transport::Base.new(config)
      @instrumenter = Instrumenter.new(
        config,
        stacktrace_builder: stacktrace_builder
      ) { |event| enqueue event }
      @metrics = Metrics.new(config) { |event| enqueue event }
    end

    attr_reader :config, :transport, :instrumenter,
      :stacktrace_builder, :context_builder, :error_builder, :metrics

    # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
    def start
      unless config.disable_start_message
        info '[%s] Starting agent, reporting to %s', VERSION, config.server_url
      end

      transport.start
      instrumenter.start
      metrics.start

      config.enabled_spies.each do |lib|
        debug "Requiring spy: #{lib}"
        require "elastic_apm/spies/#{lib}"
      end

      self
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

    def stop
      debug 'Stopping agent'

      metrics.stop
      instrumenter.stop
      transport.stop

      self
    end

    at_exit do
      stop
    end

    # transport

    def enqueue(obj)
      transport.submit obj
    end

    # instrumentation

    def current_transaction
      instrumenter.current_transaction
    end

    def current_span
      instrumenter.current_span
    end

    def start_transaction(
      name = nil,
      type = nil,
      context: nil,
      trace_context: nil
    )
      instrumenter.start_transaction(
        name,
        type,
        context: context,
        trace_context: trace_context
      )
    end

    def end_transaction(result = nil)
      instrumenter.end_transaction(result)
    end

    def start_span(
      name = nil,
      type = nil,
      backtrace: nil,
      context: nil,
      trace_context: nil
    )
      instrumenter.start_span(
        name,
        type,
        backtrace: backtrace,
        context: context,
        trace_context: trace_context
      )
    end

    def end_span
      instrumenter.end_span
    end

    def set_tag(key, value)
      instrumenter.set_tag(key, value)
    end

    def set_custom_context(context)
      instrumenter.set_custom_context(context)
    end

    def set_user(user)
      instrumenter.set_user(user)
    end

    def build_context(rack_env:, for_type:)
      @context_builder.build(rack_env: rack_env, for_type: for_type)
    end

    # errors

    def report(exception, context: nil, handled: true)
      return if config.filter_exception_types.include?(exception.class.to_s)

      error = @error_builder.build_exception(
        exception,
        context: context,
        handled: handled
      )
      enqueue error
    end

    def report_message(message, context: nil, backtrace: nil, **attrs)
      error = @error_builder.build_log(
        message,
        context: context,
        backtrace: backtrace,
        **attrs
      )
      enqueue error
    end

    # filters

    def add_filter(key, callback)
      transport.add_filter(key, callback)
    end
  end
  # rubocop:enable Metrics/ClassLength
end