lib/elastic_apm/spies/net_http.rb



# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

# frozen_string_literal: true

module ElasticAPM
  # @api private
  module Spies
    # @api private
    class NetHTTPSpy
      DISABLE_KEY = :__elastic_apm_net_http_disabled
      TYPE = 'external'
      SUBTYPE = 'http'

      class << self
        def disabled=(disabled)
          Thread.current[DISABLE_KEY] = disabled
        end

        def disabled?
          Thread.current[DISABLE_KEY] ||= false
        end

        def disable_in
          self.disabled = true

          begin
            yield
          ensure
            self.disabled = false
          end
        end
      end

      # @api private
      module Ext
        # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
        def request(req, body = nil, &block)
          unless (transaction = ElasticAPM.current_transaction)
            return super(req, body, &block)
          end

          if ElasticAPM::Spies::NetHTTPSpy.disabled?
            return super(req, body, &block)
          end

          host = req['host']&.split(':')&.first || address || 'localhost'
          method = req.method.to_s.upcase

          uri_or_path = URI(req.path)

          # Support the case where a whole url is passed as a path to a nil host
          uri =
            if uri_or_path.host
              uri_or_path
            else
              path, query = req.path.split('?')
              url = use_ssl? ? +'https://' : +'http://'
              url << host
              url << ":#{port}" if port
              url << path
              url << "?#{query}" if query
              URI(url)
            end

          context =
            ElasticAPM::Span::Context.new(
              http: { url: uri, method: method },
              destination: ElasticAPM::Span::Context::Destination.from_uri(uri, type: SUBTYPE)
            )

          ElasticAPM.with_span(
            "#{method} #{host}",
            TYPE,
            subtype: SUBTYPE,
            context: context
          ) do |span|
            trace_context = span&.trace_context || transaction.trace_context
            trace_context.apply_headers { |key, value| req[key] = value }

            result = super(req, body, &block)

            if (http = span&.context&.http)
              http.status_code = result.code
            end

            span&.outcome = Span::Outcome.from_http_status(result.code)
            result
          end
        end
        # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
      end

      def install
        Net::HTTP.prepend(Ext)
      end
    end

    register 'Net::HTTP', 'net/http', NetHTTPSpy.new
  end
end