Registro

El registro y la supervisión funcionan en conjunto para ayudarte a comprender y optimizar el rendimiento de la aplicación, así como a diagnosticar errores y problemas relacionados con el sistema. Después de leer esta guía, consulta la Guía de supervisión a fin de obtener algunas prácticas recomendadas para supervisar tu aplicación, incluidas métricas específicas que se deben registrar con fines de supervisión.

Registro de bibliotecas cliente

Las bibliotecas cliente de la API de Google Ads vienen con registro incorporado. Para obtener detalles de registro específicos de la plataforma, consulta la documentación de registro dentro de la biblioteca cliente que elijas.

Accede a la nube

Hay muchas herramientas que puedes usar a fin de capturar registros y métricas de rendimiento para tu aplicación. Por ejemplo, puedes usar Google Cloud Logging para registrar métricas de rendimiento en tu proyecto de Google Cloud. Esto permite configurar paneles y alertas en Google Cloud Monitoring para usar las métricas registradas.

Cloud Logging ofrece bibliotecas cliente para todos los lenguajes de bibliotecas cliente de la API de Google Ads compatibles, excepto Perl. Por lo tanto, en la mayoría de los casos, es posible registrar con Cloud Logging directamente desde tu integración de biblioteca cliente. Para otros lenguajes, incluido Perl, Cloud Logging también ofrece una API de REST.

Hay algunas opciones para acceder a Cloud Logging o a otra herramienta desde una biblioteca cliente de la API de Google Ads. Cada opción tiene sus ventajas y desventajas de implementación, complejidad y rendimiento. Piensa con cuidado en estas compensaciones antes de decidir qué solución implementar.

Opción 1: Escribe registros locales en la nube desde un proceso en segundo plano

Los registros de la biblioteca cliente se pueden escribir en un archivo local en tu máquina si modificas la configuración de registro. Una vez que los registros se envían a un archivo local, puedes configurar un daemon para recopilar los registros y enviarlos a la nube.

Una limitación de este enfoque es que algunas métricas de rendimiento no se capturarían de forma predeterminada. Los registros de la biblioteca cliente incluyen detalles de los objetos de solicitud y respuesta, por lo que las métricas de latencia no se incluirían a menos que se realicen cambios adicionales para registrarlos.

Opción 2: Ejecuta tu aplicación en Compute Engine y, luego, instala el agente de operaciones

Si tu aplicación se ejecuta en Compute Engine, puedes instalar el agente de operaciones para enviar tus registros a Google Cloud Logging. El agente de operaciones se puede configurar para enviar los registros de tu aplicación a Cloud Logging, además de las métricas y registros que se envían de forma predeterminada.

Si tu aplicación ya se está ejecutando en un entorno de Google Cloud Platform (GCP) o si piensas moverla a GCP, esta es una excelente opción para considerar.

Opción 3: Implementa el registro en el código de tu aplicación

El registro directo desde el código de la aplicación se puede realizar de dos maneras:

  1. Incorporar cálculos de métricas y declaraciones de registro en cada ubicación aplicable en tu código Esta opción es más factible para las bases de código más pequeñas, en las que el alcance y los costos de mantenimiento de ese cambio serían mínimos.

  2. Implementar una interfaz de registro Si la lógica de la aplicación se puede abstraer para que diferentes partes de la aplicación hereden de la misma clase base, la lógica de registro se puede implementar en esa clase base. Por lo general, se prefiere esta opción en lugar de incorporar instrucciones de registro en todo el código de la aplicación, ya que es más fácil de mantener y escalar. Para bases de código más grandes, la capacidad de mantenimiento y escalabilidad de esta solución son aún más relevantes.

Una limitación de este enfoque es que los registros completos de solicitud y respuesta no están disponibles desde el código de la aplicación. Se puede acceder a los objetos de solicitud y respuesta completos desde interceptores de gRPC. Así, el registro incorporado de la biblioteca cliente obtiene registros de solicitudes y respuestas. En el caso de un error, puede haber información adicional disponible en el objeto de excepción, pero hay menos detalles disponibles para las respuestas correctas dentro de la lógica de la aplicación. Por ejemplo, en la mayoría de los casos, no se puede acceder al ID de solicitud para una solicitud correcta desde los objetos de respuesta de la API de Google Ads.

Opción 4: Implementa un interceptor de registro de gRPC personalizado

gRPC admite interceptores unarios y de transmisión que pueden acceder a los objetos de solicitud y respuesta a medida que pasan entre el cliente y el servidor. Las bibliotecas cliente de la API de Google Ads usan interceptores de gRPC para ofrecer compatibilidad de registro integrada. De manera similar, puedes implementar un interceptor de gRPC personalizado a fin de acceder a los objetos de solicitud y respuesta, extraer información para fines de registro y supervisión, y escribir esos datos en la ubicación que elijas.

A diferencia de algunas de las otras soluciones que se presentan aquí, la implementación de un interceptor de gRPC personalizado te brinda la flexibilidad para capturar objetos de solicitud y respuesta en cada solicitud e implementa una lógica adicional para capturar detalles de la solicitud. Por ejemplo, puedes calcular el tiempo transcurrido de una solicitud mediante la implementación de la lógica de tiempo de rendimiento dentro del interceptor personalizado y, luego, registra la métrica en Google Cloud Logging a fin de que esté disponible para la supervisión de latencia dentro de Google Cloud Monitoring.

Interceptor personalizado de Google Cloud Logging en Python

Para demostrar esta solución, escribimos un ejemplo de un interceptor de registro personalizado en Python. El interceptor personalizado se crea y se pasa al cliente de servicio. Luego, accede a los objetos de solicitud y respuesta que pasan por cada llamada al método de servicio, procesa los datos de esos objetos y los envía a Google Cloud Logging.

Además de los datos que provienen de los objetos de solicitud y respuesta, en el ejemplo, se implementa una lógica adicional para capturar el tiempo transcurrido de la solicitud y otros metadatos que serían útiles con fines de supervisión, por ejemplo, si la solicitud tuvo éxito o no. Para obtener más información sobre cómo esta información puede ser útil, en general para la supervisión y, en particular, cuando se combinan Google Cloud Logging y Google Cloud Monitoring, consulta la Guía de supervisión.

# Copyright 2022 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
#
#     https://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.
"""A custom gRPC Interceptor that logs requests and responses to Cloud Logging.

The custom interceptor object is passed into the get_service method of the
GoogleAdsClient. It intercepts requests and responses, parses them into a
human readable structure and logs them using the logging service instantiated
within the class (in this case, a Cloud Logging client).
"""

import logging
import time

from google.cloud import logging
from grpc import UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor

from google.ads.googleads.interceptors import LoggingInterceptor, mask_message


class CloudLoggingInterceptor(LoggingInterceptor):
    """An interceptor that logs rpc request and response details to Google Cloud Logging.

    This class inherits logic from the LoggingInterceptor, which simplifies the
    implementation here. Some logic is required here in order to make the
    underlying logic work -- comments make note of this where applicable.
    NOTE: Inheriting from the LoggingInterceptor class could yield unexpected side
    effects. For example, if the LoggingInterceptor class is updated, this class would
    inherit the updated logic, which could affect its functionality. One option to avoid
    this is to inherit from the Interceptor class instead, and selectively copy whatever
    logic is needed from the LoggingInterceptor class."""

    def _init__(self, api_version):
        """Initializer for the CloudLoggingInterceptor.

        Args:
            api_version: a str of the API version of the request.
        """
        super().__init__(logger=None, api_version=api_version)
        # Instantiate the Cloud Logging client.
        logging_client = logging.Client()
        self.logger = logging_client.logger("cloud_logging")

    def log_successful_request(
        self,
        method,
        customer_id,
        metadata_json,
        request_id,
        request,
        trailing_metadata_json,
        response,
    ):
        """Handles logging of a successful request.

        Args:
            method: The method of the request.
            customer_id: The customer ID associated with the request.
            metadata_json: A JSON str of initial_metadata.
            request_id: A unique ID for the request provided in the response.
            request: An instance of a request proto message.
            trailing_metadata_json: A JSON str of trailing_metadata.
            response: A grpc.Call/grpc.Future instance.
        """
        # Retrieve and mask the RPC result from the response future.
        # This method is available from the LoggingInterceptor class.
        # Ensure self._cache is set in order for this to work.
        # The response result could contain up to 10,000 rows of data,
        # so consider truncating this value before logging it, to save
        # on data storage costs and maintain readability.
        result = self.retrieve_and_mask_result(response)

        # elapsed_ms is the approximate elapsed time of the RPC, in milliseconds.
        # There are different ways to define and measure elapsed time, so use
        # whatever approach makes sense for your monitoring purposes.
        # rpc_start and rpc_end are set in the intercept_unary_* methods below.
        elapsed_ms = (self.rpc_end - self.rpc_start) * 1000

        debug_log = {
            "method": method,
            "host": metadata_json,
            "request_id": request_id,
            "request": str(request),
            "headers": trailing_metadata_json,
            "response": str(result),
            "is_fault": False,
            "elapsed_ms": elapsed_ms,
        }
        self.logger.log_struct(debug_log, severity="DEBUG")

        info_log = {
            "customer_id": customer_id,
            "method": method,
            "request_id": request_id,
            "is_fault": False,
            # Available from the Interceptor class.
            "api_version": self._api_version,
        }
        self.logger.log_struct(info_log, severity="INFO")

    def log_failed_request(
        self,
        method,
        customer_id,
        metadata_json,
        request_id,
        request,
        trailing_metadata_json,
        response,
    ):
        """Handles logging of a failed request.

        Args:
            method: The method of the request.
            customer_id: The customer ID associated with the request.
            metadata_json: A JSON str of initial_metadata.
            request_id: A unique ID for the request provided in the response.
            request: An instance of a request proto message.
            trailing_metadata_json: A JSON str of trailing_metadata.
            response: A JSON str of the response message.
        """
        exception = self._get_error_from_response(response)
        exception_str = self._parse_exception_to_str(exception)
        fault_message = self._get_fault_message(exception)

        info_log = {
            "method": method,
            "endpoint": self.endpoint,
            "host": metadata_json,
            "request_id": request_id,
            "request": str(request),
            "headers": trailing_metadata_json,
            "exception": exception_str,
            "is_fault": True,
        }
        self.logger.log_struct(info_log, severity="INFO")

        error_log = {
            "method": method,
            "endpoint": self.endpoint,
            "request_id": request_id,
            "customer_id": customer_id,
            "is_fault": True,
            "fault_message": fault_message,
        }
        self.logger.log_struct(error_log, severity="ERROR")

    def intercept_unary_unary(self, continuation, client_call_details, request):
        """Intercepts and logs API interactions.

        Overrides abstract method defined in grpc.UnaryUnaryClientInterceptor.

        Args:
            continuation: a function to continue the request process.
            client_call_details: a grpc._interceptor._ClientCallDetails
                instance containing request metadata.
            request: a SearchGoogleAdsRequest or SearchGoogleAdsStreamRequest
                message class instance.

        Returns:
            A grpc.Call/grpc.Future instance representing a service response.
        """
        # Set the rpc_end value to current time when RPC completes.
        def update_rpc_end(response_future):
            self.rpc_end = time.perf_counter()

        # Capture precise clock time to later calculate approximate elapsed
        # time of the RPC.
        self.rpc_start = time.perf_counter()

        # The below call is REQUIRED.
        response = continuation(client_call_details, request)

        response.add_done_callback(update_rpc_end)

        self.log_request(client_call_details, request, response)

        # The below return is REQUIRED.
        return response

    def intercept_unary_stream(
        self, continuation, client_call_details, request
    ):
        """Intercepts and logs API interactions for Unary-Stream requests.

        Overrides abstract method defined in grpc.UnaryStreamClientInterceptor.

        Args:
            continuation: a function to continue the request process.
            client_call_details: a grpc._interceptor._ClientCallDetails
                instance containing request metadata.
            request: a SearchGoogleAdsRequest or SearchGoogleAdsStreamRequest
                message class instance.

        Returns:
            A grpc.Call/grpc.Future instance representing a service response.
        """

        def on_rpc_complete(response_future):
            self.rpc_end = time.perf_counter()
            self.log_request(client_call_details, request, response_future)

        # Capture precise clock time to later calculate approximate elapsed
        # time of the RPC.
        self.rpc_start = time.perf_counter()

        # The below call is REQUIRED.
        response = continuation(client_call_details, request)

        # Set self._cache to the cache on the response wrapper in order to
        # access the streaming logs. This is REQUIRED in order to log streaming
        # requests.
        self._cache = response.get_cache()

        response.add_done_callback(on_rpc_complete)

        # The below return is REQUIRED.
        return response