PythonのLog出力をPrintではなくLoggingを使うようになった日

## はじめに

Python で開発を行う際、皆さんはログ出力にどのような方法を使っていますでしょうか？
ちょっとしたスクリプトや個人開発のときは print(...) で済ませがちですが、後からログを見返すエビデンスを取りたい場合や、本格的な運用を考えると、logging モジュールを使った方法がとても便利であるということに今更ながら気づいた者です。(普段はSalesforce開発がメインなのでPython開発者の方は優しく見てください)

この記事では、簡単なスクリプトでも「Print ではなく Logger を活用したい」と思うようになったきっかけと、設定方法・サンプルコードをまとめています。

## 1. Print から Logger に切り替えた理由
### 1.1. フォーマットが整う

print(...) で出力していたときは、日付やログレベルを付けるにも文字列を手打ちしていました。たとえば：

```python
print(f"{datetime.now()} - 処理開始")
```
これだと最初の出力が少ないときは良いのですが、コードが多くなってくると、出力内容に一貫性がないため、可読性が低くなってしまいます。一方、logging モジュールでは フォーマット指定 を行うことで、
「日時」「ログレベル」「モジュール名」「メッセージ」、「関数名」、「出力行」などを一定のスタイルで自動的に出力できることを知りました。（今までは、誰かが設定したLog設定をもとにlogger.info()などを使っていたため恩恵に気づけなかった、、、）

### 1.2. ログレベルによるフィルタリング
エラーなのか警告なのか、デバッグログなのか、print(...) ではすべて同じ扱いです。
logging なら、DEBUG / INFO / WARNING / ERROR / CRITICAL など重要度に応じた分類ができ、後から「INFO 以上のメッセージだけを保存」などの制御が容易です。

### 1.3. ファイル出力が簡単
後でエビデンスとして残したいなら、ファイルハンドラ (FileHandler) を追加すれば、わざわざ > mylog.txt などで標準出力のリダイレクトをしなくても「同時にコンソールとファイル両方へ出力する」といった設定が柔軟にできます。（結構ありがたいなと思いました）

## 2. ログ設定の基本： basicConfig を使う

Python の logging モジュールには、まず logging.basicConfig(...) が用意されています。
これを呼び出すだけで、全体のログレベル、ログフォーマット、出力先 を一括で設定できます。

### 2.1. setup_global_logging の例

```python
import logging

def setup_global_logging():
    """
    ここで basicConfig を使って大枠のロギングを設定する。
    - level: 全体のログレベル
    - format: ログのフォーマット
    - handler: デフォルトではコンソール (StreamHandler) に出力
    """
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s : %(message)s"
        # handlers=[...] を省略すると、デフォルトの StreamHandler が設定される
    )

```

### 設定例の解説

1. `level=logging.INFO`
     - ここで指定したレベルより重要度が下のログ（例: DEBUG）は、
    デフォルトでは表示やファイル出力が行われなくなります。
    - もっと詳細なログが欲しいときは level=logging.DEBUG と書きます。

2. `format="%(asctime)s [%(levelname)s] %(name)s : %(message)s"`
    - ログ1行1行で使う書式を指定しています。
    - %(asctime)s は日時、%(levelname)s はログレベル、%(name)s はロガーの名前、
    %(message)s は実際に呼び出し元が記載するメッセージを示します。(これだけであとはよしなに出力してくれるので超便利ですよね。これだけでももう十分)
    - 他にも下記のように出力コードが存在する関数名(`%(funcName)s`)や実行した行(`(lineno)d`)なども出力してくれます
    ```python
    import logging
    
    def setup_global_logging():
        logging.basicConfig(
            level=logging.INFO,
            format="%(asctime)s [%(levelname)s] %(name)s : %(funcName)s : %(lineno)d : %(message)s"
            # handlers=[...] を省略すると自動で StreamHandler が付く
        )

    # 使用例
    def main():
        setup_global_logging()
        logging.info("Start main function.")

    if __name__ == "__main__":
        main()
    
    # 出力例
    2025-01-25 11:24:52,224 [INFO] root : main : 55 : Start main function.
    ```

3. `(handlers=...)`
    - 省略時はコンソール（標準出力）へ出力する StreamHandler が1つ付与されます。
- 複数の Handler（例: ファイル出力 + コンソール出力）を同時に使いたい場合は、
ここにリストとして明示する方法もあります。(ハンドラーを実際に設定するコードなどは後述します)


## 3. ファイル出力を関数ごとに分けたい場合
### 3.1. なぜ関数単位でログを分ける？
例えば「A情報の取得処理(fetch_A) は fetch_A.log に残す」、「B情報の取得処理(fetch_B) は fetch_B.log に残す」 といった運用をしたい場合、
ロガー (Logger) を名前付きで作成し、そのロガーに個別の FileHandler を追加します。
つまり、関数ごとにログ出力先ファイルを分けたい時などに便利です。

### 3.2. サンプル： get_function_logger
以下の例では、関数名（func_name）をもとに logs/xxx.log を作ってログを追記するようにしています。

```python
import logging
import os

def get_function_logger(func_name: str, log_dir="logs"):
    """
    func_name (例: "fetch_A", "fetch_B" など) 用に
    個別の FileHandler をセットした logger を返す。
    """
    # func_name をロガー名として取得 (例: logger "fetch_A")
    logger = logging.getLogger(func_name)

    # FileHandler の重複追加を避けるため、既存ハンドラをチェック
    for h in logger.handlers:
        if isinstance(h, logging.FileHandler):
            # すでに FileHandler があるなら使いまわす
            return logger

    # なければ、FileHandlerを新規作成
    os.makedirs(log_dir, exist_ok=True)
    file_path = os.path.join(log_dir, f"{func_name}.log")
    file_handler = logging.FileHandler(file_path, mode="a", encoding="utf-8")

    # ログのフォーマットは自由にカスタマイズ可能
    # ここでbasicConfigのフォーマットを上書きすることが可能。
    # baseConfigのフォーマットを使用したい場合は設定不要)
    file_format = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s : %(funcName)s : %(lineno)d : %(message)s")
    file_handler.setFormatter(file_format)

    logger.addHandler(file_handler)
    return logger

```

### 仕組み
- logging.getLogger(func_name) で「名前付きロガー」を取得。
- ここに FileHandler を追加して、指定ディレクトリ (logs/) の特定ファイルへ出力。
何度呼んでもハンドラが増えすぎないように、既存ハンドラをチェックしているのがポイントです。

## 3.3. 利用例

```python
def fetch_A():
    logger = get_function_logger("fetch_A")
    logger.info("Fetching A data...")
    # 処理実装

def fetch_B():
    logger = get_function_logger("fetch_B")
    logger.info("Fetching B data...")
    # 処理実装

```

これで「fetch_A」という名前のロガーは logs/fetch_A.log に、「fetch_B」という名前のロガーは logs/fetch_B.log に書き込まれます。書き込まれます。

## 4. 出力例
以下は fetch_A() を呼び出したときの例です。
ポイントは、コンソール出力 (StreamHandler)とlogs/fetch_A.log (FileHandler)で出力のフォーマットが異なるところです。

### 4.1. コンソール出力 (StreamHandler)
```shell
# baseConfigのフォーマット設定
2025-01-22 12:34:56,789 [INFO] fetch_A : Fetching A data...
```

### 4.2. logs/fetch_A.log (FileHandler)
```shell
# baseConfigのフォーマット設定を上書きした設定
2025-01-22 12:34:56,789 [INFO] fetch_A : fetch_A : 42 : Fetching A data...
```

## 5. まとめ

1. print(...) ではなく logging を使うメリット
    - 一貫したログフォーマット（日時、レベル、名前など）
    - ログレベルによる制御
    - 同時に複数出力 (コンソールとファイル) が簡単

2. `basicConfig(...)` を用いてログの一括設定
    - level, format, handlers を指定すると、デフォルトのルートロガーに対して一括設定が容易にできる

3. 関数 / モジュールごとにファイルを分けたい場合
    - getLogger("module_or_function_name") + FileHandler を追加
    - ロガー階層構造でグローバルの設定を継承しつつ、独自ハンドラを追加できる

今後、ちょっとしたスクリプトでも logging を利用すると、後からログを確認したり共有したりするときにグッと楽になります。（しかも設定がめちゃくちゃ簡単）
また、構文が直感的でわかりやすく宣言的なため、カスタマイズがすごくしやすいです。（カスタマイズにこだわって時間を溶かして本末転倒になりそうな勢い）
特に業務やデータ移行スクリプトなどで「エビデンスをしっかり残したい」場合は、ぜひ logging モジュールを活用してみてください。