# frozen_string_literal: true

require "concurrent/utility/processor_counter"

require "sentry/utils/exception_cause_chain"
require "sentry/utils/custom_inspection"
require "sentry/utils/env_helper"
require "sentry/dsn"
require "sentry/release_detector"
require "sentry/transport/configuration"
require "sentry/cron/configuration"
require "sentry/linecache"
require "sentry/interfaces/stacktrace_builder"
require "sentry/logger"
require "sentry/structured_logger"
require "sentry/log_event_buffer"
require "sentry/metric_event_buffer"

module Sentry
  class Configuration
    include CustomInspection
    include LoggingHelper
    include ArgumentCheckingHelper

    # Directories to be recognized as part of your app. e.g. if you
    # have an `engines` dir at the root of your project, you may want
    # to set this to something like /(app|config|engines|lib)/
    #
    # The default is value is /(bin|exe|app|config|lib|test|spec)/
    #
    # @return [Regexp, nil]
    attr_accessor :app_dirs_pattern

    # to send events in a non-blocking way, sentry-ruby has its own background worker
    # by default, the worker holds a thread pool that has [the number of processors] threads
    # but you can configure it with this configuration option
    # E.g.: config.background_worker_threads = 5
    #
    # if you want to send events synchronously, set the value to 0
    # E.g.: config.background_worker_threads = 0
    # @return [Integer]
    attr_accessor :background_worker_threads

    # The maximum queue size for the background worker.
    # Jobs will be rejected above this limit.
    #
    # Default is {BackgroundWorker::DEFAULT_MAX_QUEUE}.
    # @return [Integer]
    attr_accessor :background_worker_max_queue

    # a proc/lambda that takes an array of stack traces
    # it'll be used to silence (reduce) backtrace of the exception
    #
    # @example
    #   config.backtrace_cleanup_callback = lambda do |backtrace|
    #     Rails.backtrace_cleaner.clean(backtrace)
    #   end
    #
    # @return [Proc, nil]
    attr_accessor :backtrace_cleanup_callback

    # Optional Proc, called before adding the breadcrumb to the current scope
    # @example
    #   config.before = lambda do |breadcrumb, hint|
    #     breadcrumb.message = 'a'
    #     breadcrumb
    #   end
    # @return [Proc]
    attr_reader :before_breadcrumb

    # Optional Proc, called before sending an error event to the server
    # @example
    #   config.before_send = lambda do |event, hint|
    #     # skip ZeroDivisionError exceptions
    #     if hint[:exception].is_a?(ZeroDivisionError)
    #       nil
    #     else
    #       event
    #     end
    #   end
    # @return [Proc]
    attr_reader :before_send

    # Optional Proc, called before sending a transaction event to the server
    # @example
    #   config.before_send_transaction = lambda do |event, hint|
    #     # skip unimportant transactions or strip sensitive data
    #     if event.transaction == "/healthcheck/route"
    #       nil
    #     else
    #       event
    #     end
    #   end
    # @return [Proc]
    attr_reader :before_send_transaction

    # Optional Proc, called before sending a check-in event to the server
    # @example
    #   config.before_send_check_in = lambda do |event, hint|
    #     if event.monitor_slug == "unimportant_job"
    #       nil
    #     else
    #       event
    #     end
    #   end
    # @return [Proc]
    attr_reader :before_send_check_in

    # Optional Proc, called before sending an event to the server
    # @example
    #   config.before_send_log = lambda do |log|
    #     log.attributes["sentry"] = true
    #     log
    #   end
    # @return [Proc]
    attr_accessor :before_send_log

    # An array of breadcrumbs loggers to be used. Available options are:
    # - :sentry_logger
    # - :http_logger
    # - :redis_logger
    #
    # And if you also use sentry-rails:
    # - :active_support_logger
    #
    # @return [Array<Symbol>]
    attr_reader :breadcrumbs_logger

    # Max number of breadcrumbs a breadcrumb buffer can hold
    # @return [Integer]
    attr_accessor :max_breadcrumbs

    # Number of lines of code context to capture, or nil for none
    # @return [Integer, nil]
    attr_accessor :context_lines

    # RACK_ENV by default.
    # @return [String]
    attr_reader :environment

    # Whether the SDK should run in the debugging mode. Default is false.
    # If set to true, SDK errors will be logged with backtrace
    # @return [Boolean]
    attr_accessor :debug

    # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
    # @return [String]
    attr_reader :dsn

    # Whitelist of enabled_environments that will send notifications to Sentry. Array of Strings.
    # @return [Array<String>, nil]
    attr_accessor :enabled_environments

    # Logger 'progname's to exclude from breadcrumbs
    # @return [Array<String>]
    attr_accessor :exclude_loggers

    # Array of exception classes that should never be sent. See IGNORE_DEFAULT.
    # You should probably append to this rather than overwrite it.
    # @return [Array<String>]
    attr_accessor :excluded_exceptions

    # Boolean to check nested exceptions when deciding if to exclude. Defaults to true
    # @return [Boolean]
    attr_accessor :inspect_exception_causes_for_exclusion
    alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion

    # Whether to capture local variables from the raised exception's frame. Default is false.
    # @return [Boolean]
    attr_accessor :include_local_variables

    # Whether to capture events and traces into Spotlight. Default is false.
    # If you set this to true, Sentry will send events and traces to the local
    # Sidecar proxy at http://localhost:8969/stream.
    # If you want to use a different Sidecar proxy address, set this to String
    # with the proxy URL.
    # @return [Boolean, String]
    attr_accessor :spotlight

    # You may provide your own LineCache for matching paths with source files.
    # This may be useful if you need to get source code from places other than the disk.
    # @see LineCache
    # @return [LineCache]
    attr_accessor :linecache

    # Logger used by Sentry. In Rails, this is the Rails logger, otherwise
    # Sentry provides its own Sentry::Logger.
    # @return [Logger]
    attr_accessor :sdk_logger

    # File path for DebugTransport to log events to. If not set, defaults to a temporary file.
    # This is useful for debugging and testing purposes.
    # @return [String, nil]
    attr_accessor :sdk_debug_transport_log_file

    # Project directory root for in_app detection. Could be Rails root, etc.
    # Set automatically for Rails.
    # @return [String]
    attr_accessor :project_root

    # Whether to strip the load path while constructing the backtrace frame filename.
    # Defaults to true.
    # @return [Boolean]
    attr_accessor :strip_backtrace_load_path

    # Insert sentry-trace to outgoing requests' headers
    # @return [Boolean]
    attr_accessor :propagate_traces

    # Array of rack env parameters to be included in the event sent to sentry.
    # @return [Array<String>]
    attr_accessor :rack_env_whitelist

    # Release tag to be passed with every event sent to Sentry.
    # We automatically try to set this to a git SHA or Capistrano release.
    # @return [String]
    attr_reader :release

    # The sampling factor to apply to events. A value of 0.0 will not send
    # any events, and a value of 1.0 will send 100% of events.
    # @return [Float]
    attr_accessor :sample_rate

    # Include module versions in reports - boolean.
    # @return [Boolean]
    attr_accessor :send_modules

    # When send_default_pii's value is false (default), sensitive information like
    # - user ip
    # - user cookie
    # - request body
    # - query string
    # will not be sent to Sentry.
    # @return [Boolean]
    attr_accessor :send_default_pii

    # Allow to skip Sentry emails within rake tasks
    # @return [Boolean]
    attr_accessor :skip_rake_integration

    # IP ranges for trusted proxies that will be skipped when calculating IP address.
    attr_accessor :trusted_proxies

    # @return [String]
    attr_accessor :server_name

    # Transport related configuration.
    # @return [Transport::Configuration]
    attr_reader :transport

    # Cron related configuration.
    # @return [Cron::Configuration]
    attr_reader :cron

    # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
    # @return [Float, nil]
    attr_reader :traces_sample_rate

    # Take a Proc that controls the sample rate for every tracing event, e.g.
    # @example
    #   config.traces_sampler =  lambda do |tracing_context|
    #     # tracing_context[:transaction_context] contains the information about the transaction
    #     # tracing_context[:parent_sampled] contains the transaction's parent's sample decision
    #     true # return value can be a boolean or a float between 0.0 and 1.0
    #   end
    # @return [Proc]
    attr_accessor :traces_sampler

    # Enable Structured Logging
    # @return [Boolean]
    attr_accessor :enable_logs

    # Structured logging configuration.
    # @return [StructuredLoggingConfiguration]
    attr_reader :structured_logging

    # Send diagnostic client reports about dropped events, true by default
    # tries to attach to an existing envelope max once every 30s
    # @return [Boolean]
    attr_accessor :send_client_reports

    # Track sessions in request/response cycles automatically
    # @return [Boolean]
    attr_accessor :auto_session_tracking

    # Whether to downsample transactions automatically because of backpressure.
    # Starts a new monitor thread to check health of the SDK every 10 seconds.
    # Default is false
    # @return [Boolean]
    attr_accessor :enable_backpressure_handling

    # Allowlist of outgoing request targets to which sentry-trace and baggage headers are attached.
    # Default is all (/.*/)
    # @return [Array<String, Regexp>]
    attr_accessor :trace_propagation_targets

    # Collection of HTTP status codes or ranges of codes to ignore when tracing incoming requests.
    # If a transaction's http.response.status_code matches one of these values,
    # the transaction will be dropped and marked as not sampled.
    # Defaults to TRACE_IGNORE_STATUS_CODES_DEFAULT.
    #
    # @example
    #   # ignore 404 and 502 <= status_code <= 511
    #   config.trace_ignore_status_codes = [404, (502..511)]
    #
    # @return [Array<Integer>, Array<Range>]
    attr_reader :trace_ignore_status_codes

    # The instrumenter to use, :sentry or :otel
    # @return [Symbol]
    attr_reader :instrumenter

    # The profiler class
    # @return [Class]
    attr_reader :profiler_class

    # Take a float between 0.0 and 1.0 as the sample rate for capturing profiles.
    # Note that this rate is relative to traces_sample_rate / traces_sampler,
    # i.e. the profile is sampled by this rate after the transaction is sampled.
    # @return [Float, nil]
    attr_reader :profiles_sample_rate

    # Interval in microseconds at which to take samples.
    # The default is 1e6 / 101, or 101Hz.
    # Note that the 101 is intentional to avoid lockstep sampling.
    #
    # @example
    #   config.profiles_sample_interval = 1e5 / 101
    # @return [Float]
    attr_accessor :profiles_sample_interval

    # Array of patches to apply.
    # Default is {DEFAULT_PATCHES}
    # @return [Array<Symbol>]
    attr_accessor :enabled_patches

    # Maximum number of log events to buffer before sending
    # @return [Integer]
    attr_accessor :max_log_events

    # Enable metrics collection, defaults to true
    # @return [Boolean]
    attr_accessor :enable_metrics

    # Maximum number of metric events to buffer before sending
    # @return [Integer]
    attr_accessor :max_metric_events

    # Optional Proc, called before sending a metric
    # @example
    #   config.before_send_metric = lambda do |metric|
    #     # return nil to drop the metric
    #     metric
    #   end
    # @return [Proc, nil]
    attr_reader :before_send_metric

    # Optional Proc, called to filter log messages before sending to Sentry
    # @example
    #   config.std_lib_logger_filter = lambda do |logger, message, level|
    #     # Only send error and fatal logs to Sentry
    #     [:error, :fatal].include?(level)
    #   end
    # @return [Proc, nil]
    attr_reader :std_lib_logger_filter

    # these are not config options
    # @!visibility private
    attr_reader :errors, :gem_specs

    # These exceptions could enter Puma's `lowlevel_error_handler` callback and the SDK's Puma integration
    # But they are mostly considered as noise and should be ignored by default
    # Please see https://github.com/getsentry/sentry-ruby/pull/2026 for more information
    PUMA_IGNORE_DEFAULT = [
      "Puma::MiniSSL::SSLError",
      "Puma::HttpParserError",
      "Puma::HttpParserError501"
    ].freeze

    # Most of these errors generate 4XX responses. In general, Sentry clients
    # only automatically report 5xx responses.
    IGNORE_DEFAULT = [
      "Mongoid::Errors::DocumentNotFound",
      "Rack::QueryParser::InvalidParameterError",
      "Rack::QueryParser::ParameterTypeError",
      "Sinatra::NotFound"
    ].freeze

    RACK_ENV_WHITELIST_DEFAULT = %w[
      REMOTE_ADDR
      SERVER_NAME
      SERVER_PORT
    ].freeze

    TRACE_IGNORE_STATUS_CODES_DEFAULT = [(301..303), (305..399), (401..404)]

    HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\
    "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`"

    LOG_PREFIX = "** [Sentry] "
    MODULE_SEPARATOR = "::"
    SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder]

    INSTRUMENTERS = [:sentry, :otel]

    PROPAGATION_TARGETS_MATCH_ALL = /.*/

    DEFAULT_PATCHES = %i[redis puma http].freeze

    APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/

    # 101 Hz in microseconds
    DEFAULT_PROFILES_SAMPLE_INTERVAL = 1e6 / 101

    class << self
      # Post initialization callbacks are called at the end of initialization process
      # allowing extending the configuration of sentry-ruby by multiple extensions
      def post_initialization_callbacks
        @post_initialization_callbacks ||= []
      end

      # allow extensions to add their hooks to the Configuration class
      def add_post_initialization_callback(&block)
        callbacks[:initialize][:after] << block
      end

      def before(event, &block)
        callbacks[event.to_sym][:before] << block
      end

      def after(event, &block)
        callbacks[event.to_sym][:after] << block
      end

      # @!visibility private
      def callbacks
        @callbacks ||= {
          initialize: { before: [], after: [] },
          configured: { before: [], after: [] }
        }
      end

      def validations
        @validations ||= {}
      end

      def validate(attribute, optional: false, type: nil)
        validations[attribute] = {
          optional: optional,
          type: type,
          proc: build_validation_proc(optional, type)
        }
      end

      private

      def build_validation_proc(optional, type)
        case type
        when :numeric
          ->(value) do
            if optional && value.nil?
              true
            else
              unless value.is_a?(Numeric)
                message = "must be a Numeric"
                message += " or nil" if optional

                { error: message, value: value }
              else
                true
              end
            end
          end
        else
          ->(value) { true }
        end
      end
    end

    validate :traces_sample_rate, optional: true, type: :numeric
    validate :profiles_sample_rate, optional: true, type: :numeric

    def initialize
      run_callbacks(:before, :initialize)

      self.app_dirs_pattern = APP_DIRS_PATTERN
      self.debug = Sentry::Utils::EnvHelper.env_to_bool(ENV["SENTRY_DEBUG"])
      self.background_worker_threads = (processor_count / 2.0).ceil
      self.background_worker_max_queue = BackgroundWorker::DEFAULT_MAX_QUEUE
      self.backtrace_cleanup_callback = nil
      self.strip_backtrace_load_path = true
      self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
      self.breadcrumbs_logger = []
      self.context_lines = 3
      self.include_local_variables = false
      self.environment = environment_from_env
      self.enabled_environments = nil
      self.exclude_loggers = []
      self.excluded_exceptions = IGNORE_DEFAULT + PUMA_IGNORE_DEFAULT
      self.inspect_exception_causes_for_exclusion = true
      self.linecache = ::Sentry::LineCache.new
      self.sdk_logger = ::Sentry::Logger.new(STDOUT)
      self.project_root = Dir.pwd
      self.propagate_traces = true

      self.sample_rate = 1.0
      self.send_modules = true
      self.send_default_pii = false
      self.skip_rake_integration = false
      self.send_client_reports = true
      self.auto_session_tracking = true
      self.enable_backpressure_handling = false
      self.trusted_proxies = []
      self.dsn = ENV["SENTRY_DSN"]

      spotlight_env = ENV["SENTRY_SPOTLIGHT"]
      spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
      self.spotlight = spotlight_bool.nil? ? (spotlight_env || false) : spotlight_bool
      self.server_name = server_name_from_env
      self.instrumenter = :sentry
      self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL]
      self.trace_ignore_status_codes = TRACE_IGNORE_STATUS_CODES_DEFAULT
      self.enabled_patches = DEFAULT_PATCHES.dup

      self.before_send = nil
      self.before_send_transaction = nil
      self.before_send_check_in = nil
      self.before_send_log = nil
      self.before_send_metric = nil
      self.std_lib_logger_filter = nil
      self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
      self.traces_sampler = nil
      self.enable_logs = false
      self.enable_metrics = true

      self.profiler_class = Sentry::Profiler
      self.profiles_sample_interval = DEFAULT_PROFILES_SAMPLE_INTERVAL

      @transport = Transport::Configuration.new
      @cron = Cron::Configuration.new
      @structured_logging = StructuredLoggingConfiguration.new
      @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)

      self.max_log_events = LogEventBuffer::DEFAULT_MAX_EVENTS
      self.max_metric_events = MetricEventBuffer::DEFAULT_MAX_METRICS

      run_callbacks(:after, :initialize)

      yield(self) if block_given?

      run_callbacks(:after, :configured)
    end

    def validate
      if profiler_class == Sentry::Profiler && profiles_sample_rate && !Sentry.dependency_installed?(:StackProf)
        log_warn("Please add the 'stackprof' gem to your Gemfile to use the StackProf profiler with Sentry.")
      end

      if profiler_class == Sentry::Vernier::Profiler && profiles_sample_rate && !Sentry.dependency_installed?(:Vernier)
        log_warn("Please add the 'vernier' gem to your Gemfile to use the Vernier profiler with Sentry.")
      end

      self.class.validations.each do |attribute, validation|
        value = public_send(attribute)

        next if (result = validation[:proc].call(value)) === true

        raise ArgumentError, result[:error]
      end
    end

    def dsn=(value)
      @dsn = init_dsn(value)
    end

    alias server= dsn=

    def release=(value)
      check_argument_type!(value, String, NilClass)

      @release = value
    end

    def breadcrumbs_logger=(logger)
      loggers =
        if logger.is_a?(Array)
          logger
        else
          Array(logger)
        end

      require "sentry/breadcrumb/sentry_logger" if loggers.include?(:sentry_logger)

      @breadcrumbs_logger = logger
    end

    def before_send=(value)
      check_callable!("before_send", value)

      @before_send = value
    end

    def before_send_transaction=(value)
      check_callable!("before_send_transaction", value)

      @before_send_transaction = value
    end

    def before_send_check_in=(value)
      check_callable!("before_send_check_in", value)

      @before_send_check_in = value
    end

    def before_send_metric=(value)
      check_callable!("before_send_metric", value)

      @before_send_metric = value
    end

    def before_breadcrumb=(value)
      check_callable!("before_breadcrumb", value)

      @before_breadcrumb = value
    end

    def std_lib_logger_filter=(value)
      check_callable!("std_lib_logger_filter", value)

      @std_lib_logger_filter = value
    end

    def environment=(environment)
      @environment = environment.to_s
    end

    def instrumenter=(instrumenter)
      @instrumenter = INSTRUMENTERS.include?(instrumenter) ? instrumenter : :sentry
    end

    def trace_ignore_status_codes=(codes)
      unless codes.is_a?(Array) && codes.all? { |code| valid_status_code_entry?(code) }
        raise ArgumentError, "trace_ignore_status_codes must be an Array of integers or ranges between (100-599) where begin <= end"
      end

      @trace_ignore_status_codes = codes
    end

    def traces_sample_rate=(traces_sample_rate)
      @traces_sample_rate = traces_sample_rate
    end

    def profiles_sample_rate=(profiles_sample_rate)
      @profiles_sample_rate = profiles_sample_rate
    end

    def profiler_class=(profiler_class)
      if profiler_class == Sentry::Vernier::Profiler
        begin
          require "vernier"
        rescue LoadError
        end
      end

      @profiler_class = profiler_class
    end

    def sending_allowed?
      spotlight || sending_to_dsn_allowed?
    end

    def sending_to_dsn_allowed?
      @errors = []

      valid? && capture_in_environment?
    end

    def sample_allowed?
      return true if sample_rate == 1.0

      Random.rand < sample_rate
    end

    def session_tracking?
      auto_session_tracking && enabled_in_current_env?
    end

    def exception_class_allowed?(exc)
      if exc.is_a?(Sentry::Error)
        # Try to prevent error reporting loops
        log_debug("Refusing to capture Sentry error: #{exc.inspect}")
        false
      elsif excluded_exception?(exc)
        log_debug("User excluded error: #{exc.inspect}")
        false
      else
        true
      end
    end

    def enabled_in_current_env?
      enabled_environments.nil? || enabled_environments.include?(environment)
    end

    def valid_sample_rate?(sample_rate)
      return false unless sample_rate.is_a?(Numeric)
      sample_rate >= 0.0 && sample_rate <= 1.0
    end

    def tracing_enabled?
      valid_sampler = !!((valid_sample_rate?(@traces_sample_rate)) || @traces_sampler)

      valid_sampler && sending_allowed?
    end

    def profiling_enabled?
      valid_sampler = !!(valid_sample_rate?(@profiles_sample_rate))

      tracing_enabled? && valid_sampler && sending_allowed?
    end

    # @return [String, nil]
    def csp_report_uri
      if dsn && dsn.valid?
        uri = dsn.csp_report_uri
        uri += "&sentry_release=#{CGI.escape(release)}" if release && !release.empty?
        uri += "&sentry_environment=#{CGI.escape(environment)}" if environment && !environment.empty?
        uri
      end
    end

    # @api private
    def stacktrace_builder
      @stacktrace_builder ||= StacktraceBuilder.new(
        project_root: @project_root.to_s,
        app_dirs_pattern: @app_dirs_pattern,
        linecache: @linecache,
        context_lines: @context_lines,
        backtrace_cleanup_callback: @backtrace_cleanup_callback,
        strip_backtrace_load_path: @strip_backtrace_load_path
      )
    end

    # @api private
    def detect_release
      return unless sending_allowed?

      @release ||= ReleaseDetector.detect_release(project_root: project_root, running_on_heroku: running_on_heroku?)

      if running_on_heroku? && release.nil?
        log_warn(HEROKU_DYNO_METADATA_MESSAGE)
      end
    rescue => e
      log_error("Error detecting release", e, debug: debug)
    end

    # @api private
    def error_messages
      @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first
      @errors.join(", ")
    end

    private

    def init_dsn(dsn_string)
      return if dsn_string.nil? || dsn_string.empty?

      DSN.new(dsn_string)
    end

    def excluded_exception?(incoming_exception)
      excluded_exception_classes.any? do |excluded_exception|
        matches_exception?(excluded_exception, incoming_exception)
      end
    end

    def excluded_exception_classes
      @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) }
    end

    def get_exception_class(x)
      x.is_a?(Module) ? x : safe_const_get(x)
    end

    def matches_exception?(excluded_exception_class, incoming_exception)
      if inspect_exception_causes_for_exclusion?
        Sentry::Utils::ExceptionCauseChain.exception_to_array(incoming_exception).any? { |cause| excluded_exception_class === cause }
      else
        excluded_exception_class === incoming_exception
      end
    end

    def safe_const_get(x)
      x = x.to_s unless x.is_a?(String)
      Object.const_get(x)
    rescue NameError # There's no way to safely ask if a constant exist for an unknown string
      nil
    end

    def capture_in_environment?
      return true if enabled_in_current_env?

      @errors << "Not configured to send/capture in environment '#{environment}'"
      false
    end

    def valid?
      if @dsn&.valid?
        true
      else
        @errors << "DSN not set or not valid"
        false
      end
    end

    def environment_from_env
      ENV["SENTRY_CURRENT_ENV"] || ENV["SENTRY_ENVIRONMENT"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
    end

    def server_name_from_env
      if running_on_heroku?
        ENV["DYNO"]
      else
        # Try to resolve the hostname to an FQDN, but fall back to whatever
        # the load name is.
        Socket.gethostname || Socket.gethostbyname(hostname).first rescue server_name
      end
    end

    def running_on_heroku?
      File.directory?("/etc/heroku") && !ENV["CI"]
    end

    def run_callbacks(hook, event)
      self.class.callbacks[event][hook].each do |hook|
        instance_eval(&hook)
      end
    end

    def processor_count
      available_processor_count = Concurrent.available_processor_count if Concurrent.respond_to?(:available_processor_count)
      available_processor_count || Concurrent.processor_count
    end

    def valid_http_status_code?(code)
      code.is_a?(Integer) && code >= 100 && code <= 599
    end

    def valid_status_code_entry?(entry)
      case entry
      when Integer
        valid_http_status_code?(entry)
      when Range
        valid_http_status_code?(entry.begin) &&
          valid_http_status_code?(entry.end) &&
          entry.begin <= entry.end
      else
        false
      end
    end
  end

  class StructuredLoggingConfiguration
    # File path for DebugStructuredLogger to log events to
    # @return [String, Pathname, nil]
    attr_accessor :file_path

    # The class to use as a structured logger.
    # @return [Class]
    attr_accessor :logger_class

    def initialize
      @file_path = nil
      @logger_class = Sentry::StructuredLogger
    end
  end
end
