All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.ads.googleads.lib.logging.LoggingInterceptor Maven / Gradle / Ivy

The newest version!
// Copyright 2019 Google LLC
//
// Licensed 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.

package com.google.ads.googleads.lib.logging;

import com.google.ads.googleads.lib.logging.Event.Summary;
import com.google.ads.googleads.lib.logging.scrub.LogScrubber;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.Metadata.Key;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

/** An interceptor which logs all RPCs made on a Channel. */
public class LoggingInterceptor implements ClientInterceptor {

  public static final Key REQUEST_ID_HEADER_KEY =
      Key.of("request-id", Metadata.ASCII_STRING_MARSHALLER);
  private static final ImmutableSet HEADERS_TO_SCRUB =
      ImmutableSet.of("developer-token", "authorization");
  private static final Logger thisClassLogger = LoggerFactory.getLogger(LoggingInterceptor.class);
  private final LogScrubber LOG_SCRUBBER = LogScrubber.getInstance();
  private final RequestLogger requestLogger;
  private final ImmutableMap requestHeaders;
  private final ImmutableMap scrubbedRequestHeaders;
  private final String endpoint;

  /** Creates with the RequestLogger sink, the constant header data, and the API endpoint. */
  public LoggingInterceptor(
      RequestLogger requestLogger, ImmutableMap headers, String endpoint) {
    this.requestLogger = requestLogger;
    this.requestHeaders = headers;
    this.scrubbedRequestHeaders = scrubHeaders(requestHeaders);
    this.endpoint = endpoint;
  }

  /** Logs the Google Ads API fields required for debugging. */
  @Override
  public  ClientCall interceptCall(
      final MethodDescriptor method, CallOptions callOptions, Channel next) {
    ClientCall wrappedCall = next.newCall(method, callOptions);
    return new SimpleForwardingClientCall(wrappedCall) {
      private volatile ReqT request;
      private volatile RespT response;
      private volatile Metadata responseHeaders;

      @Override
      public void start(Listener responseListener, Metadata headers) {
        super.start(
            new SimpleForwardingClientCallListener(responseListener) {
              @Override
              public void onMessage(RespT message) {
                super.onMessage(message);
                response = message;
              }

              @Override
              public void onHeaders(Metadata headers) {
                super.onHeaders(headers);
                responseHeaders = headers;
              }

              @Override
              public void onClose(Status status, Metadata trailers) {
                try {
                  Object scrubbedRequest = LOG_SCRUBBER.edit(request);
                  Object scrubbedResponse = LOG_SCRUBBER.edit(response);
                  logSummary(status, scrubbedRequest, method, responseHeaders, trailers);
                  logDetail(
                      status,
                      method,
                      requestHeaders,
                      endpoint,
                      scrubbedRequest,
                      responseHeaders,
                      trailers,
                      scrubbedResponse);
                } catch (Throwable ex) {
                  thisClassLogger.warn("Failed to log request.", ex);
                } finally {
                  request = null;
                  response = null;
                  responseHeaders = null;
                  // Allows the call to complete only once we're done writing logs, ensuring that
                  // logs are printed to stdout before the RPC completes.
                  super.onClose(status, trailers);
                }
              }
            },
            headers);
      }

      @Override
      public void sendMessage(ReqT message) {
        super.sendMessage(message);
        request = message;
      }
    };
  }

  private static ImmutableMap scrubHeaders(ImmutableMap headers) {
    Map scrubbed = new LinkedHashMap();
    if (headers != null) {
      scrubbed.putAll(headers);
      scrubbed.replaceAll((key, value) -> HEADERS_TO_SCRUB.contains(key) ? "REDACTED" : value);
    }
    return ImmutableMap.copyOf(scrubbed);
  }

  private static Level getDetailLevel(Status status) {
    return isSuccess(status) ? Level.DEBUG : Level.INFO;
  }

  private static Level getSummaryLevel(Status status) {
    return isSuccess(status) ? Level.INFO : Level.WARN;
  }

  private static String getRequestId(Metadata responseHeaders, Metadata responseTrailers) {
    if (responseHeaders != null && responseHeaders.containsKey(REQUEST_ID_HEADER_KEY)) {
      return responseHeaders.get(REQUEST_ID_HEADER_KEY);
    } else if (responseTrailers != null && responseTrailers.containsKey(REQUEST_ID_HEADER_KEY)) {
      return responseTrailers.get(REQUEST_ID_HEADER_KEY);
    } else {
      return null;
    }
  }

  private static String getCustomerId(Object request) {
    // Most requests have a customerId field.
    Optional getter =
        Stream.of(request.getClass().getMethods())
            .filter(method -> method.getName().equals("getCustomerId"))
            .findFirst();
    // However, some requests only have a resource name (e.g. CustomerService.get()).
    if (!getter.isPresent()) {
      getter =
          Stream.of(request.getClass().getMethods())
              .filter(method -> method.getName().equals("getResourceName"))
              .findFirst();
    }
    if (getter.isPresent()) {
      try {
        // If the customer ID is stored as a resource name we return the entire resource name rather
        // than attempting to extract the customer ID.
        return (String) getter.get().invoke(request);
      } catch (IllegalAccessException | InvocationTargetException e) {
        thisClassLogger.error("Unable to retrieve customer ID from " + request);
      }
    }
    return null;
  }

  private static  String getMethodName(MethodDescriptor method) {
    return method == null ? null : method.getFullMethodName();
  }

  private static boolean isSuccess(Status status) {
    return status != null && status.isOk();
  }

  /**
   * Logs an RPC call detailed message containing full request/response + headers. The level chosen
   * will depend on the response status (OK=DEBUG, FAILURE=INFO). Also checks if the logger is
   * enabled for the RPC status and logger configuration before computing message params.
   */
  private void logDetail(
      Status responseStatus,
      MethodDescriptor method,
      ImmutableMap requestHeaders,
      String endpoint,
      Object request,
      Metadata responseHeaders,
      Metadata responseTrailers,
      Object response) {
    Level level = getDetailLevel(responseStatus);
    if (requestLogger.isDetailEnabled(level)) {
      String methodName = getMethodName(method);
      boolean isSuccess = isSuccess(responseStatus);
      Event.Detail event =
          Event.Detail.builder()
              .setResponseStatus(responseStatus)
              .setSuccess(isSuccess)
              .setMethodName(methodName)
              .setRawRequestHeaders(requestHeaders)
              .setScrubbedRequestHeaders(scrubbedRequestHeaders)
              .setEndpoint(endpoint)
              .setRequest(request)
              .setResponseHeaderMetadata(responseHeaders)
              .setResponseTrailerMetadata(responseTrailers)
              .setResponse(response)
              .build();
      requestLogger.logDetail(level, event);
    }
  }

  /**
   * Logs an RPC call summary, containing method name, endpoint, customerId and requestId. The level
   * chosen will depend on the response status (OK=INFO, FAILURE=WARN). This will check if the
   * logger is enabled for the RPC status and logger configuration before computing message params.
   */
  private void logSummary(
      Status responseStatus,
      Object request,
      MethodDescriptor method,
      Metadata responseHeaders,
      Metadata responseTrailers) {
    Level level = getSummaryLevel(responseStatus);
    if (requestLogger.isSummaryEnabled(level)) {
      String customerId = getCustomerId(request);
      String requestId = getRequestId(responseHeaders, responseTrailers);
      String methodName = getMethodName(method);
      boolean isSuccess = isSuccess(responseStatus);
      Summary event =
          Summary.builder()
              .setResponseStatus(responseStatus)
              .setSuccess(isSuccess)
              .setMethodName(methodName)
              .setCustomerId(customerId)
              .setEndpoint(endpoint)
              .setRequestId(requestId)
              .build();
      requestLogger.logSummary(level, event);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy