AWS Lambdaのロガー設定

# はじめに
俺俺ロガーをつくりました。

## 参考
- [AWS Lambda Logs to One JSON Line](https://stackoverflow.com/questions/50233013/aws-lambda-logs-to-one-json-line)
- [Lambda + Python3.7のログをJSON形式で出力してみる（行ごとに分割されない形で）](https://dev.classmethod.jp/articles/lambda-python-log-output-json/#toc-5)
- [AWS Lambda/PythonでJSON形式でログを出すベストプラクティス](https://qiita.com/tonluqclml/items/780370a4575781eb19df)


## コード
ぶっちゃけ参考サイトのロガー設定を良いとこドリップしただけです。改良点は以下です。

- エラーレベルや、ロガーを仕込んだ関数、位置が分かるようにしている。
- `logger('parameter %s', x)`のような形で、第一引数にメッセージ、第二引数以降に変数という、元の`logger`の入力の仕方が可能。
- 例外時は`logger.exception(e)`とすれば`traceback`にスタックトレースが出力される。
- 日本語も出力可能。
- **aws_request_idも出力できる。**デコレータを使用してLoggerAdapter経由でaws_request_idを出力するよう設定しています。

```python:utils.py
import logging
import json
import os
import traceback


class FormatterJSON(logging.Formatter):
    def format(self, record):
        record.message = record.getMessage()
        if self.usesTime():
            record.asctime = self.formatTime(record, self.datefmt)
        j = {
            'levelname': record.levelname,
            'levelno': record.levelno,
            'time': '%(asctime)s.%(msecs)dZ' % dict(asctime=record.asctime, msecs=record.msecs),
            'aws_request_id': getattr(record, 'aws_request_id', '00000000-0000-0000-0000-000000000000'),
            'message': record.message,
            'module': record.module,
            'filename': record.filename,
            'funcName': record.funcName,
            'lineno': record.lineno,
            'traceback': {},
            'extra_data': record.__dict__.get('data', {}),
        }
        if record.exc_info:
            exception_data = traceback.format_exc().splitlines()
            j['traceback'] = exception_data

        return json.dumps(j, ensure_ascii=False)


class LoggerAdapter(logging.LoggerAdapter):
    def __init__(self, logger):
        super().__init__(logger, extra={})
        
    def inject_aws_context(self, lambda_handler):
        def wrapper(event, context):
            self.extra['aws_request_id'] = context.aws_request_id
            return lambda_handler(event, context)
        return wrapper
        
        
def get_logger():
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(FormatterJSON(
        '[%(levelname)s]\t%(asctime)s.%(msecs)dZ\t%(levelno)s\t%(message)s\t%(aws_request_id)s\n','%Y-%m-%dT%H:%M:%S'
    ))
    logger = logging.getLogger(__name__)
    logger.addHandler(stream_handler)
    logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO'))
    logger = LoggerAdapter(logger)
    return logger
```

正常時のログ。

```python:lambda_function.py
import json
import os
import traceback

from utils import get_logger

logger = get_logger()

@logger.inject_aws_context
def lambda_handler(event, context):
    value = dict(a='abc', b='def', c=123)
    logger.info('Start hello Func', extra=dict(data=value))
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

```json
{
    "levelname": "INFO",
    "levelno": 20,
    "time": "2021-03-16T15:12:14.836Z",
    "aws_request_id": "9780f8b7-3d76-4e74-9b9b-d23e0a542119",
    "message": "Start hello Func",
    "module": "lambda_function",
    "filename": "lambda_function.py",
    "funcName": "lambda_handler",
    "lineno": 12,
    "traceback": {},
    "extra_data": {}
}
```
例外のログ。

```python:lambda_function.py
import json
import os
import traceback

from utils import get_logger

logger = get_logger()

@logger.inject_aws_context
def lambda_handler(event, context):
    value = dict(a='abc', b='def', c=123)
    
    try:
        print(1/0)
    except ZeroDivisionError as error:
        logger.exception(error)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

```json
{
    "levelname": "ERROR",
    "levelno": 40,
    "time": "2021-03-16T15:16:20.662Z",
    "aws_request_id": "ca30328e-ca87-470a-bbfc-896facc4e6d8",
    "message": "division by zero",
    "module": "lambda_function",
    "filename": "lambda_function.py",
    "funcName": "lambda_handler",
    "lineno": 17,
    "traceback": [
        "Traceback (most recent call last):",
        "  File \"/var/task/lambda_function.py\", line 15, in lambda_handler",
        "    print(1/0)",
        "ZeroDivisionError: division by zero"
    ],
    "extra_data": {}
}
```

## ここだけの話
Lambda Powertools を使えばいいのでは？
https://dev.classmethod.jp/articles/lambda-powertools/


## 多分ダメなやつ
以下は以前考えていたものなんですが、root ロガーを汚してしまうので、他にloggingを使用しているモジュールがあると壊れる可能性があります。

- `logging.basicConfig()`でデフォルトハンドラー（ストリームハンドラー）を設定することでロガーの設定の記述量を少なくした。Lambdaではストリームハンドラーだけあれば良いので、これで良いと判断した。

```python
import logging
import json
import os
import traceback

class FormatterJSON(logging.Formatter):
    def format(self, record):
        record.message = record.getMessage()
        if self.usesTime():
            record.asctime = self.formatTime(record, self.datefmt)
        j = {
            'levelname': record.levelname,
            'levelno': record.levelno,
            'time': '%(asctime)s.%(msecs)dZ' % dict(asctime=record.asctime, msecs=record.msecs),
            'aws_request_id': getattr(record, 'aws_request_id', '00000000-0000-0000-0000-000000000000'),
            'message': record.message,
            'module': record.module,
            'filename': record.filename,
            'funcName': record.funcName,
            'lineno': record.lineno,
            'traceback': {},
            'extra_data': record.__dict__.get('data', {}),
        }
        if record.exc_info:
            exception_data = traceback.format_exc().splitlines()
            j['traceback'] = exception_data
        
        return json.dumps(j, ensure_ascii=False)

logging.basicConfig()
logging.getLogger().handlers[0].setFormatter(FormatterJSON(
    '[%(levelname)s]\t%(asctime)s.%(msecs)dZ\t%(levelno)s\t%(message)s\n','%Y-%m-%dT%H:%M:%S'
))
logger = logging.getLogger(__name__)
logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO'))
```

正常ログの例。

```python
def lambda_handler(event, context):
    value = dict(a='abc', b='def', c=123)
    logger.info('Start hello Func', extra=dict(data=value))
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

```json
{
    "levelname": "INFO",
    "levelno": 20,
    "time": "2021-02-09T05:42:58.503Z",
    "aws_request_id": "c199c00d-a2ae-4081-9fd2-7b513e7c55ba",
    "message": "Start hello Func",
    "module": "lambda_function",
    "filename": "lambda_function.py",
    "funcName": "lambda_handler",
    "lineno": 40,
    "traceback": {},
    "extra_data": {
        "a": "abc",
        "b": "def",
        "c": 123
    }
}
```

例外ログの例。

```python
def lambda_handler(event, context):
    value = dict(a='abc', b='def', c=123)

    try:
        print(1/0)
    except ZeroDivisionError as error:
        logger.exception(error)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

```

```json
{
    "levelname": "ERROR",
    "levelno": 40,
    "time": "2021-02-09T05:46:43.80Z",
    "aws_request_id": "e8c0ce56-7dd8-4a39-ac4f-12afbc0ebb32",
    "message": "division by zero",
    "module": "lambda_function",
    "filename": "lambda_function.py",
    "funcName": "lambda_handler",
    "lineno": 44,
    "traceback": [
        "Traceback (most recent call last):",
        "  File \"/var/task/lambda_function.py\", line 42, in lambda_handler",
        "    print(1/0)",
        "ZeroDivisionError: division by zero"
    ],
    "extra_data": {}
}
```