日志记录

日志记录和监控协同工作,帮助您了解和优化应用性能,以及诊断错误和系统相关问题。您应为所有 API 调用启用 摘要日志,并为失败的 API 调用启用 详细日志,以便在需要技术支持时提供 API 调用日志。

客户端库日志记录

Google Ads API 客户端库附带内置日志记录功能。如需了解特定于平台的日志记录详细信息,请参阅您选择的客户端库中的日志记录文档。

语言 指南
Java Java 的日志记录文档
.NET .NET 的日志记录文档
PHP PHP 的日志记录文档
Python Python 的日志记录文档
Ruby Ruby 的日志记录文档
Perl Perl 的日志记录文档

日志格式

Google Ads API 客户端库会为每个 API 调用生成详细日志和摘要 日志。详细日志包含 API 调用的所有详细信息,而摘要日志包含 API 调用的最少详细信息。 下面显示了每种日志的示例,这些日志经过截断和格式化,以便于阅读。

摘要日志

GoogleAds.SummaryRequestLogs Warning: 1 : [2023-09-15 19:58:39Z] -
Request made: Host: ,
Method: /google.ads.googleads.v26.services.GoogleAdsService/SearchStream,
ClientCustomerID: 5951878031, RequestID: hELhBPNlEDd8mWYcZu7b8g,
IsFault: True, FaultMessage: Status(StatusCode="InvalidArgument",
Detail="Request contains an invalid argument.")

详细日志

GoogleAds.DetailedRequestLogs Verbose: 1 : [2023-11-02 21:09:36Z] -
---------------BEGIN API CALL---------------

Request
-------

Method Name: /google.ads.googleads.v26.services.GoogleAdsService/SearchStream
Host:
Headers: {
  "x-goog-api-client": "gl-dotnet/5.0.0 gapic/17.0.1 gax/4.2.0 grpc/2.46.3",
  "login-customer-id": "1234567890",
  "x-goog-request-params": "customer_id=4567890123"
}

{
  "customerId": "4567890123",
  "query": "SELECT ad_group_criterion.type FROM ad_group_criterion WHERE ad_group.status IN (ENABLED, PAUSED) AND campaign.status IN (ENABLED, PAUSED)",
  "summaryRowSetting": "NO_SUMMARY_ROW"
}

Response
--------
Headers: {
  "date": "Thu, 02 Nov 2023 21:09:35 GMT",
  "alt-svc": "h3-29=\":443\"; ma=2592000"
}

{
  "results": [
    {
      "adGroupCriterion": {
        "resourceName": "customers/4567890123/adGroupCriteria/456789456789~123456123467",
        "type": "KEYWORD"
      }
    },
    {
      "adGroupCriterion": {
        "resourceName": "customers/4567890123/adGroupCriteria/456789456789~56789056788",
        "type": "KEYWORD"
      }
    }
  ],
  "fieldMask": "adGroupCriterion.type",
  "requestId": "VsJ4F00ew6s9heHvAJ-abw"
}
----------------END API CALL----------------

如果我不使用客户端库,该怎么办?

如果您不使用客户端库,请实现自己的日志记录功能,以捕获传出和传入 API 调用的详细信息。您应至少记录 request-id响应标头的值,然后可以根据需要与 技术支持团队共享该值。

记录到云端

您可以使用许多工具来捕获应用的日志和效果指标。例如,您可以使用 Google Cloud Logging 将性能指标记录到您的 Google Cloud Project。这样,您就可以设置 信息中心和提醒,以便在 Google Cloud Monitoring 中 利用记录的指标。

Cloud Logging 为所有受支持的 Google Ads API 客户端库语言(Perl 除外)提供客户端库,因此在大多数情况下,您可以直接从客户端库 集成中通过 Cloud Logging 进行日志记录。对于包括 Perl 在内的其他语言,Cloud Logging 还提供 REST API。

您可以选择多种方式从 Google Ads API 客户端库记录到 Cloud Logging 或其他工具。每种方式在实现时间、复杂性和性能方面都有各自的权衡。在决定要实现哪种解决方案之前,请仔细考虑这些权衡。

方式 1:从后台进程写入本地日志

您可以通过修改日志记录配置,将客户端库日志写入到本地机器上的本地文件。将日志输出到本地文件后,您可以设置守护程序来收集日志并将其发送到云端。

这种方式的一个限制是,默认情况下不会捕获某些效果指标。客户端库日志包含来自请求和响应对象的详细信息,因此除非进行其他更改以记录这些指标,否则延迟指标不会包含在内。

方式 2:使用 Compute Engine 和 Ops Agent

如果您的应用在 Compute Engine 上运行,则可以通过安装 Ops Agent 将日志发送到 Google Cloud Logging。除了默认发送的 指标和日志之外,您还可以将 Ops Agent 配置为将应用日志发送到 Cloud Logging。

如果您的应用已在 Google Cloud 环境中运行,或者您正在考虑将应用迁移到 Google Cloud,那么这是一个不错的选择。

方式 3:在应用代码中实现日志记录

您可以采用以下两种方式之一直接从应用代码进行日志记录:

  1. 在代码中的每个适用位置加入指标计算和日志语句。对于较小的代码库,这种方式更可行,因为此类更改的范围和维护成本将是最低的。

  2. 实现日志记录接口。如果可以对应用逻辑进行抽象,以便应用的不同部分继承自同一个基类,则可以在该基类中实现日志记录逻辑。与在整个应用代码中加入日志语句相比,这种方式通常更受欢迎,因为它更易于维护和扩缩。对于较大的代码库,此解决方案的可维护性和可扩缩性就显得更加重要。

这种方式的一个限制是,无法从应用代码获取完整的请求和响应日志。可以通过 gRPC 拦截器访问完整的请求和响应对象;内置客户端库日志记录就是通过这种方式获取请求和响应日志的。如果发生错误,异常对象中可能会提供其他信息,但应用逻辑中成功响应的详细信息较少。例如,在大多数情况下,无法从 Google Ads API 响应对象访问成功请求的请求 ID。

方式 4:自定义 gRPC 日志记录拦截器

gRPC 支持一元和流式 拦截器,这些拦截器可以在 请求和响应对象在客户端和 服务器之间传递时访问它们。Google Ads API 客户端库使用 gRPC 拦截器来提供内置日志记录支持。同样,您可以实现自定义 gRPC 拦截器来访问请求和响应对象,提取信息以用于日志记录和监控,并将这些数据写入您选择的位置。

与此处介绍的其他一些解决方案不同,实现自定义 gRPC 拦截器可让您灵活地捕获每个请求的请求和响应对象,并实现其他逻辑来捕获请求的详细信息。 例如,您可以在自定义拦截器本身中实现性能计时逻辑来计算请求的耗时,然后将该指标记录到 Google Cloud Logging,以便在 Google Cloud Monitoring 中进行延迟监控。

Python 中的自定义 Cloud Logging 拦截器

为了演示此解决方案,我们编写了一个 Python 中的自定义日志记录拦截器示例。创建自定义拦截器并将其传递给服务客户端。然后,它会访问每个服务方法调用中传递的请求和响应对象,处理这些对象中的数据,并将数据发送到 Google Cloud Logging。

除了来自请求和响应对象的数据之外,该示例还实现了一些额外的逻辑来捕获请求的耗时,以及一些其他对监控有用的元数据,例如请求是否成功。如需详细了解这些 信息在监控(一般情况下)以及在结合使用 Google Cloud Logging 和 Google Cloud Monitoring 时(具体情况下)的用途,请参阅监控 指南。

# 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 time
from typing import Any, Callable, Dict, Optional

from google.cloud import logging as google_cloud_logging
from grpc._interceptor import _ClientCallDetails

from google.ads.googleads.interceptors import LoggingInterceptor


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: str):
        """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: google_cloud_logging.Client = google_cloud_logging.Client()
        self.logger: google_cloud_logging.Logger = logging_client.logger("cloud_logging")
        self.rpc_start: float
        self.rpc_end: float

    def log_successful_request(
        self,
        method: str,
        customer_id: Optional[str],
        metadata_json: str,
        request_id: str,
        request: Any,  # google.ads.googleads.vX.services.types.SearchGoogleAdsRequest or SearchGoogleAdsStreamRequest
        trailing_metadata_json: str,
        response: Any,  # grpc.Call or grpc.Future
    ) -> None:
        """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: Any = 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: float = (self.rpc_end - self.rpc_start) * 1000

        debug_log: Dict[str, Any] = {
            "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: Dict[str, Any] = {
            "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: str,
        customer_id: Optional[str],
        metadata_json: str,
        request_id: str,
        request: Any,  # google.ads.googleads.vX.services.types.SearchGoogleAdsRequest or SearchGoogleAdsStreamRequest
        trailing_metadata_json: str,
        response: Any,  # grpc.Call or grpc.Future
    ) -> None:
        """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: Any = self._get_error_from_response(response)
        exception_str: str = self._parse_exception_to_str(exception)
        fault_message: str = self._get_fault_message(exception)

        info_log: Dict[str, Any] = {
            "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: Dict[str, Any] = {
            "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: Callable[[_ClientCallDetails, Any], Any], # Any is request type
        client_call_details: _ClientCallDetails,
        request: Any,  # google.ads.googleads.vX.services.types.SearchGoogleAdsRequest
    ) -> Any:  # grpc.Call or grpc.Future
        """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: Any) -> None: # response_future is grpc.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: Any = continuation(client_call_details, request) # response is grpc.Call or grpc.Future

        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: Callable[[_ClientCallDetails, Any], Any], # Any is request type
        client_call_details: _ClientCallDetails,
        request: Any,  # google.ads.googleads.vX.services.types.SearchGoogleAdsStreamRequest
    ) -> Any:  # grpc.Call or grpc.Future
        """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: Any) -> None: # response_future is grpc.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: Any = continuation(client_call_details, request) # response is grpc.Call or grpc.Future

        # 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