로깅

로깅과 모니터링은 함께 작동하여 애플리케이션 성능을 파악하고 최적화할 뿐만 아니라 오류 및 시스템 관련 문제를 진단하는 데 도움이 됩니다. 기술 지원이 필요할 때 API 호출 로그를 제공할 수 있도록 모든 API 호출에 요약 로그를 사용 설정하고 실패한 API 호출에는 세부정보 로그를 사용 설정해야 합니다.

클라이언트 라이브러리 로깅

Google Ads API 클라이언트 라이브러리에는 로깅이 내장되어 있습니다. 플랫폼별 로깅 세부정보는 선택한 클라이언트 라이브러리의 로깅 문서를 참고하세요.

언어 가이드
자바 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.v14.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.v14.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 gccl/3.0.1 pb/3.21.5",
  "developer-token": "REDACTED",
  "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 프로젝트에 성능 측정항목을 로깅할 수 있습니다. 이렇게 하면 Google Cloud Monitoring에서 대시보드와 알림을 설정하여 로깅된 측정항목을 활용할 수 있습니다.

Cloud Logging은 Perl을 제외한 모든 지원되는 Google Ads API 클라이언트 라이브러리 언어에 클라이언트 라이브러리를 제공하므로 대부분의 경우 클라이언트 라이브러리 통합에서 Cloud Logging으로 직접 로깅할 수 있습니다. Perl을 비롯한 다른 언어의 경우 Cloud Logging에서 REST API도 제공합니다.

Google Ads API 클라이언트 라이브러리에서 Cloud Logging 또는 다른 도구에 로깅하는 방법에는 몇 가지가 있습니다. 각 옵션에는 구현 시간, 복잡성, 성능에 관한 장단점이 있습니다. 구현할 솔루션을 결정하기 전에 이러한 장단점을 신중하게 고려하세요.

옵션 1: 백그라운드 프로세스에서 로컬 로그를 클라우드에 작성

로깅 구성을 수정하여 클라이언트 라이브러리 로그를 머신의 로컬 파일에 쓸 수 있습니다. 로그가 로컬 파일로 출력되면 로그를 수집하여 클라우드로 전송하는 데몬을 설정할 수 있습니다.

이 접근 방식의 한 가지 제한사항은 일부 실적 측정항목이 기본적으로 캡처되지 않는다는 점입니다. 클라이언트 라이브러리 로그에는 요청 및 응답 객체의 세부정보가 포함되므로 이를 로깅하도록 추가 변경사항을 적용하지 않는 한 지연 시간 측정항목은 포함되지 않습니다.

옵션 2: Compute Engine에서 애플리케이션을 실행하고 운영 에이전트 설치

애플리케이션이 Compute Engine에서 실행 중인 경우 운영 에이전트를 설치하여 로그를 Google Cloud Logging으로 전송할 수 있습니다. 운영 에이전트는 기본적으로 전송되는 측정항목 및 로그 외에도 애플리케이션 로그를 Cloud Logging으로 전송하도록 구성할 수 있습니다.

애플리케이션이 이미 Google Cloud 환경에서 실행 중이거나 애플리케이션을 Google Cloud로 이전하려는 경우 이 옵션을 고려해 보세요.

옵션 3: 애플리케이션 코드에서 로깅 구현

애플리케이션 코드에서 직접 로깅하는 방법은 다음 두 가지 중 하나입니다.

  1. 코드의 모든 관련 위치에 측정항목 계산 및 로그 문을 통합합니다. 이 옵션은 이러한 변경사항의 범위와 유지보수 비용이 최소인 소규모 코드베이스에 더 적합합니다.

  2. 로깅 인터페이스 구현 애플리케이션의 여러 부분이 동일한 기본 클래스에서 상속받도록 애플리케이션 로직을 추상화할 수 있는 경우 로깅 로직을 해당 기본 클래스에 구현할 수 있습니다. 이 옵션은 유지관리 및 확장이 더 쉽기 때문에 애플리케이션 코드 전체에 로그 문을 통합하는 것보다 일반적으로 선호됩니다. 코드베이스가 클수록 이 솔루션의 유지 관리성과 확장성이 더욱 중요합니다.

이 접근 방식의 한 가지 제한사항은 애플리케이션 코드에서 전체 요청 및 응답 로그를 사용할 수 없다는 점입니다. 전체 요청 및 응답 객체는 gRPC 인터셉터에서 액세스할 수 있습니다. 이 방식으로 내장 클라이언트 라이브러리 로깅은 요청 및 응답 로그를 가져옵니다. 오류가 발생하면 예외 객체에서 추가 정보를 사용할 수 있지만 애플리케이션 로직 내에서 성공적인 응답에 사용할 수 있는 세부정보는 적습니다. 예를 들어 대부분의 경우 성공한 요청의 요청 ID는 Google Ads API 응답 객체에서 액세스할 수 없습니다.

옵션 4: 맞춤 gRPC 로깅 인터셉터 구현

gRPC는 단항 및 스트리밍 인터셉터를 지원합니다. 이 인터셉터는 요청 및 응답 객체가 클라이언트와 서버 간에 전달될 때 액세스할 수 있습니다. Google Ads API 클라이언트 라이브러리는 gRPC 인터셉터를 사용하여 기본 제공 로깅 지원을 제공합니다. 마찬가지로 맞춤 gRPC 인터셉터를 구현하여 요청 및 응답 객체에 액세스하고, 로깅 및 모니터링 목적으로 정보를 추출하고, 선택한 위치에 해당 데이터를 쓸 수 있습니다.

여기에 나와 있는 다른 솔루션과 달리 맞춤 gRPC 인터셉터를 구현하면 모든 요청에서 요청 및 응답 객체를 유연하게 캡처하고 요청 세부정보를 캡처하는 추가 로직을 구현할 수 있습니다. 예를 들어 맞춤 인터셉터 자체 내에 성능 타이밍 로직을 구현하여 요청의 경과 시간을 계산한 다음, Google Cloud Monitoring 내에서 지연 시간 모니터링에 사용할 수 있도록 측정항목을 Google Cloud Logging에 로깅할 수 있습니다.

Python의 맞춤 Google 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 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