lib/elastic_apm/http.rb



# frozen_string_literal: true

require 'net/http'
require 'zlib'

require 'elastic_apm/service_info'
require 'elastic_apm/system_info'
require 'elastic_apm/process_info'
require 'elastic_apm/filters'

module ElasticAPM
  # @api private
  class Http
    include Log

    USER_AGENT = "elastic-apm/ruby #{VERSION}".freeze
    ACCEPT = 'application/json'.freeze
    CONTENT_TYPE = 'application/json'.freeze

    def initialize(config, adapter: HttpAdapter)
      @config = config
      @adapter = adapter.new(config)
      @base_payload = {
        service: ServiceInfo.build(config),
        process: ProcessInfo.build(config),
        system: SystemInfo.build(config)
      }
      @filters = Filters.new(config)
    end

    attr_reader :filters

    def post(path, payload = {})
      payload.merge! @base_payload
      filters.apply(payload)

      request = prepare_request path, payload.to_json
      response = @adapter.perform request

      return response if response.is_a?(Net::HTTPSuccess)

      error 'POST returned an unsuccessful status code (%d)', response.code
      error "apm-server's response: %s", response.body

      response
    end

    private

    def prepare_request(path, data)
      @adapter.post url_for(path) do |req|
        req['Accept'] = ACCEPT
        req['Content-Type'] = CONTENT_TYPE
        req['User-Agent'] = USER_AGENT

        if (token = @config.secret_token)
          req['Authorization'] = "Bearer #{token}"
        end

        prepare_request_body! req, data
      end
    end

    def prepare_request_body!(req, data)
      if @config.http_compression &&
         data.bytesize > @config.compression_minimum_size
        deflated = Zlib.deflate data, @config.compression_level

        req['Content-Encoding'] = 'deflate'
        req['Content-Length'] = deflated.bytesize.to_s
        req.body = deflated
      else
        req['Content-Length'] = data.bytesize.to_s
        req.body = data
      end
    end

    def url_for(path)
      "#{@config.server_url}#{path}"
    end
  end

  # @api private
  class HttpAdapter
    def initialize(conf)
      @config = conf
    end

    def post(path)
      req = Net::HTTP::Post.new path
      yield req if block_given?
      req
    end

    def perform(req)
      http.start do |http|
        http.request req
      end
    end

    private

    def http
      return @http if @http

      http = Net::HTTP.new server_uri.host, server_uri.port
      http.use_ssl = @config.use_ssl?
      http.verify_mode = verify_mode
      http.read_timeout = @config.http_read_timeout
      http.open_timeout = @config.http_open_timeout

      if @config.debug_http
        http.set_debug_output(@config.logger)
      end

      @http = http
    end

    def server_uri
      @server_uri ||= URI(@config.server_url)
    end

    def verify_mode
      if @config.use_ssl? && @config.verify_server_cert?
        OpenSSL::SSL::VERIFY_PEER
      else
        OpenSSL::SSL::VERIFY_NONE
      end
    end
  end
end