0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LambdaとPythonでカスタムログレベルをJSON出力したい(+ログが2つ出る事象の対処)

Last updated at Posted at 2025-05-22

この記事の概要

AWS LambdaでPythonを実行する際に、ログをJSON出力し、カスタムログレベルを定義したいことがありました。

スクリーンショット 2025-05-22 23.51.40.png

Lambdaのロギング設定のログフォーマットをJSONに変更し、logging.addLevelNameでカスタムログレベルを追加すれば良いと思ったが上手くいかない事象に遭遇。その対処法と発生理由についてまとめます。

結論:対処法

下記のコードで実現できます。

ポイントは以下2つです。

  • Lambdaのログ出力設定をText→JSONに変更するだけではダメ、自分でJSON出力するFormatterを用意
    • カスタムログレベル名が出力されない事象が解決可能
  • logger.propagate = Falseで親のLoggerに流さない
    • ログが2つ出力される事象が解決可能
import logging
import json
import logging.handlers

# カスタムログレベルを指定
ALERT = 45
EMERGENCY = 55
logging.addLevelName(ALERT, "ALERT")
logging.addLevelName(EMERGENCY, "EMERGENCY")

from logging import root
print(root.handlers, 'root.handlers') # ローカルだと空だが、Lambda上だとLambdaLoggerHandle

# loggerを定義
logger = logging.getLogger(__name__)
logger.propagate = False # Root Loggerに流さない
logger.setLevel(logging.INFO)

# JSON出力するFormatterを用意
class JsonFormatter(logging.Formatter):
  def format(self, record):
    return json.dumps(record.__dict__) # recordの属性を全て出力

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)


def lambda_handler(event, context):
  logger.info("Lambda function started", extra={"event": "hoge"})
  logger.warning("This is a warning message", extra={"event": "hoge"})
  logger.error("This is an error message", extra={"event": "hoge"})
  logger.log(ALERT, "This is an alert message", extra={"event": "hoge"})
  logger.critical("This is a critical message", extra={"event": "hoge"})
  logger.log(EMERGENCY, "This is an emergency message", extra={"event": "hoge"})
  logger.fatal("This is a fatal message. Fatalの顔をしたCritical", extra={"event": "hoge"})

これにより、自分で定義したログレベルが出力出来ました。

スクリーンショット 2025-05-22 23.49.36.png

ちなみに、ログの出力設定は「Text」でも「JSON」でも、どちらでも良いです。

問題発生理由

カスタムログレベルを設定したかった経緯

上記から、Pythonで「ALERT」「EMERGENCY」のレベルを設定したかった。

発生していた課題

LambdaでJSONログフォーマットを指定できるため、ロギング設定をJSONに変更した。

スクリーンショット 2025-05-22 23.51.40.png

下記のように、カスタムログレベルを定義した。

import logging
import json
import logging.handlers

ALERT = 45
EMERGENCY = 55
logging.addLevelName(ALERT, "ALERT")
logging.addLevelName(EMERGENCY, "EMERGENCY")

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
  logger.info("Lambda function started", extra={"event": "hoge"})
  logger.warning("This is a warning message", extra={"event": "hoge"})
  logger.error("This is an error message", extra={"event": "hoge"})
  logger.log(ALERT, "This is an alert message", extra={"event": "hoge"})
  logger.critical("This is a critical message", extra={"event": "hoge"})
  logger.log(EMERGENCY, "This is an emergency message", extra={"event": "hoge"})
  logger.fatal("This is a fatal message. Fatalの顔をしたCritical", extra={"event": "hoge"})

すると、下記のような問題が起こりました。

  • ALERTに設定しているはずがERROR
  • EMERGENCYに設定しているはずがCRITICAL

スクリーンショット 2025-05-23 0.05.26.png

試しにフォーマットをTextに変更して再度実行してみると普通にカスタムログレベル名が出力されました。

スクリーンショット 2025-05-23 0.09.06.png

JSON出力の場合にカスタムログレベルが使えなかった理由

AWS LambdaではLambdaLoggerHandlerというハンドラがルートロガーに設定されています。

また、PythonのLoggingのドキュメントより、propageteフラグがfalseでない場合上位のロガーに関連付けられたハンドラが処理されます。

各ロガーは 0 個以上のハンドラを (Logger の addHandler() メソッド) で関連付けることができます。ロガーに直接関連付けられたハンドラに加えて、ロガーの上位にあるロガーすべてに関連付けられたハンドラ がメッセージを処理する際に呼び出されます (ただしロガーの propagate フラグが false 値にセットされている場合を除きます。その場合は、祖先ハンドラへの伝搬はそこで止まります)。

LambdaLoggerHandlerのフォーマット処理が怪しそうです。ソースコードを発見し、見てみました。

以下の記述から、フォーマットがJSONでない(Text)の場合はlevelnameをそのまま出力していることから、Textの場合はカスタムログレベルが出力されていることが分かります。次は、JsonFormatter()の部分を見てみます。

if log_format == LogFormat.JSON:
    logger_handler.setFormatter(JsonFormatter())
else:
    logger_handler.setFormatter(
        logging.Formatter(
            "[%(levelname)s]\t%(asctime)s.%(msecs)03dZ\t%(aws_request_id)s\t%(message)s\n",
            "%Y-%m-%dT%H:%M:%S",
        )
    )

JsonFormatter()のformat処理を見ると、JSONの場合はlevelnoを_format_log_levelで変換しています。変換ロジックを見ると、0, 10, 20, 30, 40, 50のいずれかに変換されていることが分かりました。

45の場合は40、55の場合は50となるため、ALERT→ERROR、EMERGENCY→CRITICALにログレベルが勝手に変換されてしまっていました。

def format(self, record: logging.LogRecord) -> str:
    record.levelno = _format_log_level(record)
    record.levelname = logging.getLevelName(record.levelno)
    record._frame_type = _JSON_FRAME_TYPES.get(
        record.levelno, _JSON_FRAME_TYPES[logging.NOTSET]
    )
    ...

##########

def _format_log_level(record: logging.LogRecord) -> int:
    return min(50, max(0, record.levelno)) // 10 * 10

まとめると以下の理由でカスタムログレベルをJSON出力出来ていませんでした。

  • Lambdaでloggingを使うとデフォルトでLambdaLoggerHandlerが使用されている
  • LambdaLoggerHandlerはログ出力時にログレベルを0, 10, 20, 30, 40, 50のいずれかに変換する

カスタムログレベルをJSON出力する方法

ここまでで、デフォルトのLambdaLoggerHandlerを使うとカスタムログレベルを出力出来ないことが分かりました。

そのため、自分で定義したハンドラを使えばカスタムログレベルを出力出来そうだと言えます。そこで、下記のコードのようにJSON形式で出力するハンドラを追加します。

import logging
import json
import logging.handlers

ALERT = 45
EMERGENCY = 55
logging.addLevelName(ALERT, "ALERT")
logging.addLevelName(EMERGENCY, "EMERGENCY")

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

### ここから追加:JSON形式で出力するハンドラを追加
class JsonFormatter(logging.Formatter):
  def format(self, record):
    return json.dumps(record.__dict__) # recordの属性を全て出力

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
### ここまで追加

def lambda_handler(event, context):
  logger.info("Lambda function started", extra={"event": "hoge"})
  logger.warning("This is a warning message", extra={"event": "hoge"})
  logger.error("This is an error message", extra={"event": "hoge"})
  logger.log(ALERT, "This is an alert message", extra={"event": "hoge"})
  logger.critical("This is a critical message", extra={"event": "hoge"})
  logger.log(EMERGENCY, "This is an emergency message", extra={"event": "hoge"})
  logger.fatal("This is a fatal message. Fatalの顔をしたCritical", extra={"event": "hoge"})

その結果、カスタムログレベルが出力されるようになりました。しかし、ログが2つずつ出力されるという別の問題が発生します。

スクリーンショット 2025-05-23 0.32.10.png

これは、先ほども説明したようにルートロガーに設定されたLambdaLoggerHandlerの処理も走ってしまうため、ログが2つずつ出力されてしまっています。

各ロガーは 0 個以上のハンドラを (Logger の addHandler() メソッド) で関連付けることができます。ロガーに直接関連付けられたハンドラに加えて、ロガーの上位にあるロガーすべてに関連付けられたハンドラ がメッセージを処理する際に呼び出されます (ただしロガーの propagate フラグが false 値にセットされている場合を除きます。その場合は、祖先ハンドラへの伝搬はそこで止まります)。

そのため、定義したロガーのpropagate属性をFalseにしルートロガーへの伝搬を止めるようにします。

import logging
import json
import logging.handlers

ALERT = 45
EMERGENCY = 55
logging.addLevelName(ALERT, "ALERT")
logging.addLevelName(EMERGENCY, "EMERGENCY")

logger = logging.getLogger(__name__)
### ここから追加
logger.propagate = False # Root Loggerに流さない
### 追加ここまで
logger.setLevel(logging.INFO)

class JsonFormatter(logging.Formatter):
  def format(self, record):
    return json.dumps(record.__dict__) # recordの属性を全て出力

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)


def lambda_handler(event, context):
  logger.info("Lambda function started", extra={"event": "hoge"})
  logger.warning("This is a warning message", extra={"event": "hoge"})
  logger.error("This is an error message", extra={"event": "hoge"})
  logger.log(ALERT, "This is an alert message", extra={"event": "hoge"})
  logger.critical("This is a critical message", extra={"event": "hoge"})
  logger.log(EMERGENCY, "This is an emergency message", extra={"event": "hoge"})
  logger.fatal("This is a fatal message. Fatalの顔をしたCritical", extra={"event": "hoge"})

これにより、カスタムログレベルを出力しながらログが2つずつ出力される事象の解消もできました。

スクリーンショット 2025-05-23 0.36.50.png

関連リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?