require 'securerandom' require 'bigdecimal' require 'multi_json' require 'aws-xray-sdk/exceptions' require 'aws-xray-sdk/model/cause' require 'aws-xray-sdk/model/annotations' require 'aws-xray-sdk/model/metadata' module XRay # This module contains common properties and methods # used by segment and subsegment class. module Entity attr_reader :name, :exception, :cause, :namespace, :http_request, :http_response attr_accessor :parent, :throttle, :error, :fault, :sampled, :aws, :start_time, :end_time HTTP_REQUEST_KEY = %I[url method user_agent client_ip x_forwarded_for].freeze HTTP_RESPONSE_KEY = %I[status content_length].freeze # Generates a random 8-digit hex number as the entity id and returns it. def id @id ||= begin SecureRandom.hex(8) end end def closed? @closed ||= false end # @param [Float] end_time End time on epoch. def close(end_time: nil) raise EntityClosedError if closed? @end_time = end_time || Time.now.to_f @closed = true end # @return [Array] The children subsegments of this entity. def subsegments @subsegments ||= [] end # @param [Subsegment] subsegment Append the provided subsegment to children subsegments. def add_subsegment(subsegment:) raise EntityClosedError if closed? subsegment.sampled = sampled subsegment.parent = self subsegments << subsegment nil end # @param [Subsegment] subsegment Remove the provided subsegment from children subsegments. # @return [Subsegment] The deleted subsegment if the deletion is successful. def remove_subsegment(subsegment:) subsegments.delete(subsegment) subsegment end def annotations @annotations ||= Annotations.new(self) end def metadata(namespace: :default) @metadata ||= Metadata.new(self) @metadata.sub_meta(namespace) end # Set error/fault/throttle flags based on http status code. # This method is idempotent. # @param [Integer] status def apply_status_code(status:) raise EntityClosedError if closed? case status.to_i when 429 @throttle = true @error = true @fault = false when 400..499 @error = true @throttle = false @fault = false when 500..599 @fault = true @error = false @throttle = false end @http_response ||= {} @http_response[:status] = status.to_i end # @param [Hash] request Supported keys are `:url`, `:user_agent`, `:client_ip`, # `:x_forwarded_for`, `:method`. Value can be one of # String or Integer or Boolean types depend on the key. def merge_http_request(request:) raise EntityClosedError if closed? request.delete_if { |k| !HTTP_REQUEST_KEY.include?(k) } @http_request ||= {} @http_request.merge!(request) end # @param [Hash] response Supported keys are `:status`, `:content_length`. # Value can be one of String or Integer types depend on the key. def merge_http_response(response:) raise EntityClosedError if closed? response.delete_if { |k| !HTTP_RESPONSE_KEY.include?(k) } @http_response ||= {} @http_response.merge!(response) apply_status_code status: response[:status] if response.key?(:status) end # @param [Exception] exception The exception object to capture. # @param remote A boolean flag indicates whether the exception is # returned from the downstream service. def add_exception(exception:, remote: false) raise EntityClosedError if closed? @fault = true @exception = exception if cause_id = find_root_cause(exception) @cause = Cause.new id: cause_id else @cause = Cause.new exception: exception, remote: remote end end # @return [String] Cause id is the id of the subsegment where # the exception originally comes from. def cause_id return @cause.id if @cause end # @return [Hash] The hash that contains all attributes that will # be later serialized and sent out. def to_h h = { name: name, id: id, start_time: start_time } if closed? h[:end_time] = end_time else h[:in_progress] = true end h[:subsegments] = subsegments.map(&:to_h) unless subsegments.empty? h[:aws] = aws if aws if http_request || http_response h[:http] = {} h[:http][:request] = http_request if http_request h[:http][:response] = http_response if http_response end if (a = annotations.to_h) && !a.empty? h[:annotations] = a end # make sure @metadata is defined before evaluating it, to prevent warning `variable @metadata not initialized` if (defined?(@metadata) && m = @metadata) && !m.to_h.empty? h[:metadata] = m.to_h end h[:parent_id] = parent.id if parent # make sure the value in hash can only be boolean true h[:fault] = !!fault if fault h[:error] = !!error if error h[:throttle] = !!throttle if throttle h[:cause] = cause.to_h if cause h end def to_json @to_json ||= begin MultiJson.dump(to_h) end end private def find_root_cause(e) subsegment = subsegments.find { |i| i.exception.hash == e.hash } return nil unless subsegment if cause_id = subsegment.cause_id cause_id else subsegment.id end end end end