로깅

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

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

Google Ads API 클라이언트 라이브러리에는 로깅이 기본 제공됩니다. 플랫폼별 로깅 세부정보는 원하는 클라이언트 라이브러리 내의 로깅 문서를 참조하세요.

언어 가이드
Java Java용 Logging 문서
.NET .NET용 Logging 문서
2,399필리핀 PHP용 Logging 문서
Python Python용 Logging 문서
Ruby Ruby용 Logging 문서
Perl Perl용 Logging 문서

로그 형식

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 Logging에 로깅하여 Google Cloud Monitoring 내에서 지연 시간 모니터링에 사용할 수 있도록 할 수 있습니다.

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