class Google::Ads::GoogleAds::Interceptors::LoggingInterceptor

Constants

HEADERS_TO_MASK
MASK_REPLACEMENT
SEARCH_REQUEST_MASK
SEARCH_RESPONSE_FIELDS_TO_MASK

Public Class Methods

new(logger) click to toggle source
Calls superclass method
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 45
def initialize(logger)
  # Don't propagate args, parens are necessary
  super()
  @logger = logger
end

Public Instance Methods

request_response(request:, call:, method:, metadata: {}) { || ... } click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 51
def request_response(request:, call:, method:, metadata: {})
  begin
    response = yield

    @logger.info { build_summary_message(request, call, method, false) }
    @logger.debug { build_request_message(metadata, request) }
    @logger.debug { build_success_response_message(response) }
    if response.respond_to?(:partial_failure_error) && response.partial_failure_error
      @logger.debug { build_partial_failure_message(response) }
    end
    response
  rescue Exception
    @logger.warn { build_summary_message(request, call, method, true) }
    @logger.info { build_request_message(metadata, request) }
    @logger.info { build_error_response_message }
    raise
  end
end
server_streamer(request:, call:, method:, metadata: {}) { || ... } click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 70
def server_streamer(request:, call:, method:, metadata: {})
  begin
    @logger.info { build_summary_message(request, call, method, false) }
    responses = yield
    Enumerator.new do |y|
      responses.each { |response|
        @logger.debug { build_request_message(metadata, request) }
        @logger.debug { build_success_response_message(response) }
        if response.respond_to?(:partial_failure_error) && response.partial_failure_error
          @logger.debug { build_partial_failure_message(response) }
        end
        y << response
      }
      @logger.debug {
        request_id = call
          .instance_variable_get(:@wrapped)
          .instance_variable_get(:@call)
          .trailing_metadata["request-id"]
        "Request ID for preceding streaming request: #{request_id}"
      }
    rescue Exception
      handle_error(request, call, method, metadata)
    end
  rescue Exception
    handle_error(request, call, method, metadata)
  end
end

Private Instance Methods

build_error_response_message() click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 117
def build_error_response_message
  exception = $!
  response_message = ""
  response_message << "Incoming response (errors): \n"
  response_message << error_details(exception).join
  response_message
end
build_partial_failure_message(response) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 107
def build_partial_failure_message(response)
  errors = PartialFailureErrorDecoder.decode(
    response.partial_failure_error
  )
  errors.reduce("Partial failure errors: ") do |accum, error|
    accum += error.to_json + "\n"
    accum
  end
end
build_request_message(metadata, request) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 157
def build_request_message(metadata, request)
  # calling #to_json on some protos (specifically those with non-UTF8
  # encodable byte values) causes a segfault, however #inspect works
  # so we check if the proto contains a bytevalue, and if it does
  # we #inspect instead of #to_json
  request_inspect = if use_bytes_inspect?(request)
                      request.inspect
                    else
                      sanitize_message(request).to_json
                    end
  "Outgoing request: Headers: #{sanitize_headers(metadata).to_json} " \
    "Payload: #{request_inspect}"
end
build_success_response_message(response) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 153
def build_success_response_message(response)
  "Incoming response: Payload: #{sanitize_message(response).to_json}"
end
build_summary_message(request, call, method, is_fault) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 301
def build_summary_message(request, call, method, is_fault)
  customer_id = "N/A"
  customer_id = request.customer_id if request.respond_to?(:customer_id)
  # CustomerService get requests have a different format.
  if request.respond_to?(:resource_name)
    customer_id = request.resource_name.split('/').last
  end

  is_fault_string = if is_fault
                      "yes"
                    else
                      "no"
                    end

  wrapped_var = call.instance_variable_get(:@wrapped)

  trailing_metadata = wrapped_var
    .instance_variable_get(:@call)
    .trailing_metadata

  request_id = "N/A"
  request_id = trailing_metadata["request-id"] if trailing_metadata

  [
    "CID: #{customer_id}",
    "Host: #{wrapped_var.peer}",
    "Method: #{method}",
    "IsFault: #{is_fault_string}",
    "Request ID: #{request_id}",
  ].join(", ")
end
clone_to_json(message) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 179
def clone_to_json(message)
  JSON.parse(message.to_json)
end
contains_bytes_field?(descriptor) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 344
def contains_bytes_field?(descriptor)
  return false if descriptor.nil?
  return false if @cycle_finder.is_cycle?(descriptor)

  @cycle_finder.add_object(descriptor)

  descriptor.map { |x| x.type == :bytes || (x.type == :message && contains_bytes_field?(x.subtype)) }.any?
end
error_details(exception) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 125
def error_details(exception)
  last_user_line = CallerFilter.first_non_google_ads_line
  exception_details = [
    "  ",
    exception.class,
    "(",
    exception.message,
    "): "
  ]

  case exception
  when Google::Ads::GoogleAds::Errors::GoogleAdsError
    exception.failure.errors.each do |error|
      exception_details << error.message
    end
  when GRPC::InvalidArgument
    exception_details << exception.details
  end

  exception_details << [
    "\n",
    "  called from: ",
    last_user_line
  ]

  exception_details
end
handle_error(request, call, method, metadata) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 100
def handle_error(request, call, method, metadata)
  @logger.warn { build_summary_message(request, call, method, true) }
  @logger.info { build_request_message(metadata, request) }
  @logger.info { build_error_response_message }
  raise
end
interesting_error_classes() click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 353
def interesting_error_classes
  @interesting_error_classes ||= Google::Ads::GoogleAds::Errors.namespaces.map do |namespace|
    namespace.const_get(:GoogleAdsFailure)
  end
end
response_error_from_detail(detail) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 333
def response_error_from_detail(detail)
  detail.errors.map.with_index { |error, i|
    "Error #{i + 1}: #{error.to_json}"
  }.join("\n")
end
sanitize_customer_user_access(message) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 252
def sanitize_customer_user_access(message)
  if message.include?("emailAddress")
    message["emailAddress"] = MASK_REPLACEMENT
  end
  if message.include?("inviterUserEmailAddress")
    message["inviterUserEmailAddress"] = MASK_REPLACEMENT
  end
  message
end
sanitize_customer_user_access_invitation(message) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 262
def sanitize_customer_user_access_invitation(message)
  if message.include?("emailAddress")
    message["emailAddress"] = MASK_REPLACEMENT
  end
  message
end
sanitize_feeds_request(message) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 269
def sanitize_feeds_request(message)
  if message.include?("operations")
    message["operations"].each do |operation|
      if operation.include?("create")
        operation = operation["create"]
      elsif operation.include?("update")
        operation = operation["update"]
      else
        # Only create and update can contain sensitive fields.
        next
      end
      if operation.include?("placesLocationFeedData") &&
          operation["placesLocationFeedData"].include?("emailAddress")
        operation["placesLocationFeedData"]["emailAddress"] = MASK_REPLACEMENT
      end
    end
  end
  message
end
sanitize_field(object, path) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 289
def sanitize_field(object, path)
  split_path = path.split(".")
  target_field = split_path.last
  split_path.inject(object) do |obj, field|
    if obj.include?(target_field)
      obj[target_field] = MASK_REPLACEMENT
      break
    end
    obj[field]
  end
end
sanitize_headers(metadata) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 171
def sanitize_headers(metadata)
  metadata = metadata.clone
  HEADERS_TO_MASK.each do |header|
    metadata[header] = MASK_REPLACEMENT
  end
  metadata
end
sanitize_message(message) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 183
def sanitize_message(message)
  message_class = message.class.to_s.split("::").last
  if %w[SearchGoogleAdsStreamResponse SearchGoogleAdsResponse].include?(
      message_class)
    # Sanitize all known sensitive fields across all search responses.
    message = clone_to_json(message)
    message["fieldMask"].split(",").each do |path|
      if SEARCH_RESPONSE_FIELDS_TO_MASK.include?(path.split(".").last)
        message["results"]&.each do |result|
          sanitize_field(result, path)
        end
      end
    end
    message
  elsif %w[SearchGoogleAdsRequest SearchGoogleAdsStreamRequest].include?(
      message_class)
    if SEARCH_REQUEST_MASK === message.query
      message = clone_to_json(message)
      message["query"] = MASK_REPLACEMENT
    end
    message
  elsif "CustomerUserAccess" == message_class
    # Sanitize sensitive fields specific to CustomerUserAccess get requests.
    message = clone_to_json(message)
    sanitize_customer_user_access(message)
  elsif "MutateCustomerUserAccessRequest" == message_class
    # Sanitize sensitive fields when mutating a CustomerUserAccess.
    message = clone_to_json(message)
    if message.include?("operation") && message["operation"].include?("update")
      message["operation"]["update"] =
        sanitize_customer_user_access(message["operation"]["update"])
    end
    message
  elsif "CustomerUserAccessInvitation" == message_class
    # Sanitize sensitive fields specific to CustomerUserAccessInvitation get requests.
    message = clone_to_json(message)
    sanitize_customer_user_access_invitation(message)
  elsif "MutateCustomerUserAccessInvitationRequest" == message_class
    # Sanitize sensitive fields when mutating a CustomerUserAccessInvitation.
    message = clone_to_json(message)
    if message.include?("operation") && message["operation"].include?("create")
      message["operation"]["create"] =
        sanitize_customer_user_access_invitation(message["operation"]["create"])
    end
    message
  elsif "Feed" == message_class
    # Sanitize sensitive fields specific to Feed get requests.
    message = clone_to_json(message)
    if message.include?("placesLocationFeedData") &&
        message["placesLocationFeedData"].include?("emailAddress")
      message["placesLocationFeedData"]["emailAddress"] = MASK_REPLACEMENT
    end
    message
  elsif "MutateFeedsRequest" == message_class
    # Sanitize sensitive fields when mutating a Feed.
    message = clone_to_json(message)
    sanitize_feeds_request(message)
  elsif "CreateCustomerClientRequest" == message_class
    # Sanitize sensitive fields when creating a CustomerClient.
    message = clone_to_json(message)
    if message.include?("emailAddress")
      message["emailAddress"] = MASK_REPLACEMENT
    end
    message
  else
    message
  end
end
use_bytes_inspect?(request) click to toggle source
# File lib/google/ads/google_ads/interceptors/logging_interceptor.rb, line 339
def use_bytes_inspect?(request)
  @cycle_finder = CycleFinder.new
  contains_bytes_field?(request.class.descriptor)
end