Cloud Functions のログをカスタマイズして行番号やログレベルを付与する

Cloud Functions を使っていて、デバッグ時に行番号やファイル名が取得できなくてちょっと困りました。
色々調べましたが、googleのloggingライブラリをラッパーすることで対応しました。

# 結果
こんな感じになりました。
時刻も日本時間に修正され、行番号も表示されています。
ログレベルの途中制御も可能になっています。
これでデバッグが楽になりそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389667/29c7028f-06d4-afaf-d1e0-6bdd8dfb9baf.png)


# 準備
- google-cloud-logging を導入しておくこと。
- Cloud Functions のデプロイ時に、requirements.txt にも記入しておくこと。

```bash
$ pip install google-cloud-logging
```

```text:requirements.txt
google.cloud.logging==3.2.1
```


# ソースコード

```bash:
.
├── main.py
├── gclogger.py
└── requirements.txt
```

```python:main.py
import base64
from gclogger import gclogger,DEBUG,INFO,ERROR,WARNING,CRITICAL

def hello_pubsub(event, context):
    """Triggered from a message on a Cloud Pub/Sub topic.
    Args:
         event (dict): Event payload.
         context (google.cloud.functions.Context): Metadata for the event.
    """
    try:
      pubsub_message = base64.b64decode(event['data']).decode('utf-8')
      print(pubsub_message)
      
      # 1st, you call createLogger function
      
      gclogger.createLogger()
      gclogger.debug('debug message',gclogger.location())
      gclogger.info("info message",gclogger.location())
      gclogger.warning('warning message',gclogger.location())
      gclogger.error('error message',gclogger.location())
      gclogger.critical('critical message',gclogger.location())
      gclogger.exception('exception message',gclogger.location())
      print('log level: ERROR')
      
      # if you want to change log level. call setLevel()
      gclogger.setLevel(ERROR)
      gclogger.debug('debug message',gclogger.location())
      gclogger.info("info message",gclogger.location())
      gclogger.warning('warning message',gclogger.location())
      gclogger.error('error message',gclogger.location())
      gclogger.critical('critical message',gclogger.location())
      gclogger.exception('exception message',gclogger.location())
      gclogger.setLevel(INFO)
      
      print('log level: INFO')
      gclogger.debug('debug message',gclogger.location())
      gclogger.info("info message",gclogger.location())
      gclogger.warning('warning message',gclogger.location())
      gclogger.error('error message',gclogger.location())
      gclogger.critical('critical message',gclogger.location())
      gclogger.exception('exception message',gclogger.location())

    except Exception as e:
      print(e)

```

```python:gclogger.py
from logging import DEBUG,INFO,CRITICAL,ERROR,WARNING


class gclogger():
    """
    This module for GCP Logging.
    
    Usage:
    
    from gclogger import gclogger,DEBUG,INFO,ERROR,WARNING,CRITICAL

    # 1st, you call createLogger function
    
    gclogger.createLogger()
    gclogger.debug('debug message',gclogger.location())
    gclogger.info("info message",gclogger.location())
    gclogger.warning('warning message',gclogger.location())
    gclogger.error('error message',gclogger.location())
    gclogger.critical('critical message',gclogger.location())
    gclogger.exception('exception message',gclogger.location())
    print('log level: ERROR')
    
    # if you want to change log level. call setLevel()
    gclogger.setLevel(ERROR)
    gclogger.debug('debug message',gclogger.location())
    gclogger.info("info message",gclogger.location())
    gclogger.warning('warning message',gclogger.location())
    gclogger.error('error message',gclogger.location())
    gclogger.critical('critical message',gclogger.location())
    gclogger.exception('exception message',gclogger.location())
    gclogger.setLevel(INFO)
    
    print('log level: INFO')
    gclogger.debug('debug message',gclogger.location())
    gclogger.info("info message",gclogger.location())
    gclogger.warning('warning message',gclogger.location())
    gclogger.error('error message',gclogger.location())
    gclogger.critical('critical message',gclogger.location())
    gclogger.exception('exception message',gclogger.location())
    """
    __logger = None
    
    
    @staticmethod
    def createLogger(level:int=DEBUG):
        """
        Parameters
        ----------
        level : int, optional. please read setLevel() docs
            DESCRIPTION. The default is DEBUG.

        Returns
        -------
        None.

        """
        from logging import getLogger
        from google.cloud.logging import Client

        if gclogger.__logger is None:            
            Client().setup_logging()
            gclogger.__logger = getLogger()
            gclogger.__logger.setLevel(DEBUG)
        return(None)

    @staticmethod
    def location(depth=0):
        """
        get function_name,line_no,file_name

        Parameters
        ----------
        depth : TYPE, optional
            DESCRIPTION. The default is 0.

        Returns
        -------
        string  "[FUNC_NAME] xxx [LINE_NO] xxx [FILE_NAME] xxx"

        """
        import inspect
        import os

        frame = inspect.currentframe().f_back
        filename = os.path.basename(frame.f_code.co_filename)
        code_name = frame.f_code.co_name
        lineno = frame.f_lineno
        return(f'\t[FUNC_NAME] {code_name}\t[LINE_NO] {lineno}\t[FILE_NAME] {filename}')     

    @staticmethod
    def getnow(TimeZone:str="Asia/Tokyo", strformat:str="%Y-%m-%d %H:%M:%S.%f %Z"):
        """
        Get the current time considering the time zone and return a string according to the specified format

        Parameters
        ----------
        TimeZone : string
            DESCRIPTION. set timezone
            Usage. "Asia/Tokyo", "America/Los_Angeles", ...
            
        strformat : string
            DESCRIPTION. set time format
            Usage. "%Y/%m/%d" etc

        Returns
        -------
        strformat

        """
        from datetime import datetime
        from zoneinfo import ZoneInfo
        now = datetime.now(ZoneInfo(TimeZone))
        return(now.strftime(strformat))
    
    @staticmethod
    def setLevel(level:int):
        """
        Parameters
        ----------
        level : DEBUG(10),INFO(20),WARNING(30),ERROR(40),CRITICAL(50)
            DESCRIPTION. set log level.
            Usage. setLevel(DEBUG) # setLevel(10)

        Returns
        -------
        None.

        """
        if not level in (DEBUG,INFO,WARNING,ERROR,CRITICAL):
            gclogger.__logger.warning('[WARNING]\tlevel is in DEBUG/INFO/WARNING/ERROR/CRITICAL .')
        else:
            gclogger.__logger.setLevel(level)
        return(None)

    @staticmethod
    def debug(message,location:str=''):
        if gclogger.__logger is not None:
            gn = gclogger.getnow()
            gclogger.__logger.debug(f'{gn}\t[DEBUG]\t{message}{location}')
        else:
            gclogger.createLogger()
        return(None)


    @staticmethod
    def info(message,location:str=''):
        if gclogger.__logger is not None:
            gn = gclogger.getnow()
            gclogger.__logger.info(f'{gn}\t[INFO]\t{message}{location}')
        else:
            gclogger.createLogger()
        return(None)

            
    @staticmethod
    def warning(message,location:str=''):
        if gclogger.__logger is not None:
            gn = gclogger.getnow()
            gclogger.__logger.warning(f'{gn}\t[WARNING]\t{message}{location}')
        else:
            gclogger.createLogger()
        return(None)


    @staticmethod
    def error(message,location:str=''):
        if gclogger.__logger is not None:
            gn = gclogger.getnow()
            gclogger.__logger.error(f'{gn}\t[ERROR]\t{message}{location}')
        else:
            gclogger.createLogger()
        return(None)

            
    @staticmethod
    def critical(message,location:str=''):
        if gclogger.__logger is not None:
            gn = gclogger.getnow()
            gclogger.__logger.critical(f'{gn}\t[CRITICAL]\t{message}{location}')
        else:
            gclogger.createLogger()
        return(None)

    @staticmethod
    def exception(message,location:str=''):
        """
        err() + Error Class Information
        
        Parameters
        ----------
        message : TYPE
            DESCRIPTION.
        location : string
            DESCRIPTION. Do location() in the function you call
                         ex:  gclogger.exception("Oh my God",gclogger.location())
        Returns
        -------
        None.
        """
        if gclogger.__logger is not None:
            gn = gclogger.getnow()
            gclogger.__logger.exception(f'{gn}\t[ERROR]\t{message}{location}\t[EXCEPTION_MSG]')
        else:
            gclogger.createLogger()
        return(None)
```



# 参考
[pythonで実行中のファイル、関数、行番号を取得する](https://qiita.com/ymko/items/b46d32b98f013f06d85)
[(Python)Cloud FunctionsからCloud Loggingへのログ連携方法の比較](https://zenn.dev/koshilife/articles/d2c752b3a0d9bb)
[Cloud Functions ログの作成、表示、処理](https://cloud.google.com/functions/docs/monitoring/logging#functions-log-helloworld-python)