VBAでロガークラスを作ってみよう（#1）

（Qiitaに初めて投稿してみます。お作法間違い等ありましたらご容赦ください。）

## 1. ロガークラスを作りたい背景

VBAってPythonなどの最近の言語と比較すると、ロギングの機能がとっても貧弱ですよね。普通にVBAでログを書こうとすると、
```vb
Debug.Print(Format(Now(), "yyyy-mm-dd hh:mm;ss") & " - " & "ログを出力します")
```
というように、Debug.Printを直接的に使ったコードになってしまうと思います。はっきり言って使い勝手は最悪です。

ロガーとしては、

- 自動的に現在時刻を入れてくれる
- モジュール名や関数名を入れてくれる
- ログをレベル別に出力制御できる（``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``）

というあたりは最低限満足したいでしょうし、更には、

- 時刻はミリ秒単位まで出力できる
- ログのインスタンスを複数持ち、インスタンスごとに出力レベルを切り替える
- ログをコンソール（VBAはデバッグターミナル）とファイルに出力できる
- ログのフォーマットを指定できる

というあたりまで到達できると理想的です。本記事では上記を満足するようなロガークラスをVBA上で実装していきたいと思います。

- #1 : 現在時刻出力、モジュール名・関数名出力、レベル別出力制御ができる最低限のもの
- #2 : #1をベースに、時刻をミリ秒、複数インスタンス対応に拡張したもの
- #3 : #2をベースに、ファイル出力対応、ログのフォーマット指定に拡張したもの

と順を追って拡張していきたいと思います。本記事の範囲は#1です。

## 2. アウトプットのイメージ

ロガークラス#1の範囲は

- 現在時刻を秒単位まで出力できる
- モジュール名・関数名を出力できる
- ログをレベル別に出力制御できる（``DEBUG, INFO, WARNING, ERROR, CRITICAL``）
- フォーマットは固定（``現在時刻 - 出力レベル - 関数名 - メッセージ``）
- ファイル出力はできない

というところになります。

### 2.1. 外部からの利用イメージ

まず最初に、外部からロガークラスが利用されるときのサンプルコードを考えてみます。

```vb:MyModule.bas
' プライベート変数
Dim logger As New ExLogger

' モジュール内で共通のロガー初期化関数
Public Sub InitLogger()
    Set logger = GetExLogger()
    logger.ModuleName = "MyModule"
    logger.LogOutputLevel = LOG_DEBUG
End Sub



' 実際にロガーを使用するサンプル関数
Public Function MyFunction() As Long
    ' loggerの初期化
    InitLogger

    ' 関数の開始
    logger.LogInfo "MyFunction", "Function Start"

    ' メインコード
    someValue = 0
    If someValue = 0 Then
        logger.LogError "MyFunction", "Invalid someValue: " & someValue
        someValue = 1
    End If

    ' 関数のリターン
    HogeHogeFunc = someValue
    logger.LogDebug "MyFunction", "Return Value: " & someValue

    ' 関数の終了
    logger.LogInfo "MyFunction", "Function End"
End Function


' Main関数
Public Sub Main()
    ' loggerの初期化
    InitLogger
    
    ' 関数の開始
    logger.LogInfo "Main", "Main Start"
    
    ' MyFunctionのコール
    Dim hoge As Long
    hoge = MyFunction
    
    ' 関数の終了
    logger.LogInfo "Main", "Main End"
End Sub
```

このとき、出力されるログはこんなイメージです。

```text:Log (イメージ)
2024-05-20 09:42:49 - INFO - MyModule.Main - Main Start
2024-05-20 09:42:50 - INFO - MyModule.MyFunction - Function Start
2024-05-20 09:42:51 - ERROR - MyModule.MyFunction - Invalid someValue: 0
2024-05-20 09:42:52 - DEBUG - MyModule.MyFunction - Return Value: 1
2024-05-20 09:42:53 - INFO - MyModule.MyFunction - Function End
2024-05-20 09:42:54 - INFO - MyModule.Main - Main End
```

### 2.2. サンプルコードの解説

#### (1) モジュール名と関数名
VBAでは、自分のモジュール名とか自分の関数名を取得する手段がありません。ですので、何らかの方法でモジュール名や関数名をロガークラス側に教えてあげる必要があります。ここでは、

- ``InitLogger``関数でモジュール名を設定、このとき出力ログレベルも同時に設定する
- loggerを使用する関数内で関数名を都度記載する（冗長だが）

というやり方にしています。

#### (2) ログ出力関数（``LogDebug``, ``LogInfo``, ``LogError``）
ログを実際にコールする関数は、``logger.LogHogeHoge``という書式に名称統一します。Pythonでは、``logger.Info``のように``Info``, ``Debug``, ``Error``等を直接関数名として使用していると思いますが、VBAでは関数名の重複で``logger.Debug``が使用できないので関数名を変えないといけないという事情があります。

#### (3) logger.LogOutputLevelで出力レベルを調整
デバッグのときには全てのログを出したいけれども、デバッグが完了したらログはINFOレベル以上だけにする等、ログの出力レベルの調整をしたいときは、``logger.LogOutputLevel``で指定します。今回の``InitLogger``関数内では出力レベルを``LOG_DEBUG``にしているので全てのログが出力されます。

## 3. クラスを作ってみよう

それでは実際に作ってみましょう。今回、共通部分側として作成するものは

- クラスモジュール（ExLoggerクラス）
- 標準モジュール（ExCommonモジュール）

の2つがあります。クラスモジュールに殆どのコードが入るのですが、どうしてもクラスモジュール側では書けないものに関しては標準モジュール側に実装する必要があります。

### 3.1. クラス（ExLoggerクラス）の作成

ExLoggerクラスは、ロガー本体が記述されるクラスモジュールになります。

- プライベート変数2個（ログ出力レベルを設定する``pLogOutputLevel``、モジュール名を設定する``pModuleName``）
- プロパティ関数4個（上記2変数のGETとLET）
- 初期化関数2個（必ずコールされる``Class_Initialize``と、パラメータ付きで初期化する``Initialize``）
- ログ出力関数5個（``LogDebug``, ``LogInfo``, ``LogWarning``, ``LogError``, ``LogCritical``）
- 上記ログ出力関数から呼ばれる共通プライベート関数1個(``LogMessage``）

という構成になります。

```vb:ExLogger.cls
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Class ExLogger
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit
Option Base 1


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Private
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private pLogOutputLevel As LogLevel
Private pModuleName As String


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Property
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Property Get LogOutputLevel() As LogLevel
    LogOutputLevel = pLogOutputLevel
End Property

Property Let LogOutputLevel(ByVal value As LogLevel)
    pLogOutputLevel = value
End Property

Property Get ModuleName() As String
    ModuleName = pModuleName
End Property

Property Let ModuleName(ByVal value As String)
    pModuleName = value
End Property


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Initialize
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub Class_Initialize()
    LogOutputLevel = LOG_DEBUG
    ModuleName = "(No Name)"
End Sub

Public Sub Initialize(Optional ByVal iLogOutputLevel = LOG_DEBUG, Optional ByVal iModuleName = "(No Name)")
    LogOutputLevel = iLogOutputLevel
    ModuleName = iModuleName
End Sub


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Log Functions
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Sub LogDebug(ByVal funcName As String, Optional ByVal message As String)
    If pLogOutputLevel <= LOG_DEBUG Then Debug.Print LogMessage("DEBUG", funcName, message)
End Sub

Public Sub LogInfo(ByVal funcName As String, Optional ByVal message As String)
    If pLogOutputLevel <= LOG_INFO Then Debug.Print LogMessage("INFO", funcName, message)
End Sub

Public Sub LogWarning(ByVal funcName As String, Optional ByVal message As String)
    If pLogOutputLevel <= LOG_WARNING Then Debug.Print LogMessage("WARNING", funcName, message)
End Sub

Public Sub LogError(ByVal funcName As String, Optional ByVal message As String)
    If pLogOutputLevel <= LOG_ERROR Then Debug.Print LogMessage("ERROR", funcName, message)
End Sub

Public Sub LogCritical(ByVal funcName As String, Optional ByVal message As String)
    If pLogOutputLevel <= LOG_CRITICAL Then Debug.Print LogMessage("CRITICAL", funcName, message)
End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Internal Functions
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private Function LogMessage(ByVal levelString As String, ByVal funcName As String, Optional ByVal message As String) As String
    LogMessage = Format(Now(), "yyyy-mm-dd hh:mm:ss") & " - " & levelString & " - " & pModuleName & "." & funcName & " - " & message
End Function
```

### 3.2. 標準モジュール（ExCommonモジュール）の作成

クラスモジュールだけではサポートできない範囲をカバーするため、標準モジュールを1️つ用意します。本記事では``ExCommon``としておきます。具体的には、

- LOG_DEBUGなどの定数を列挙型（Enum）で定義
- シングルトンクラスにするためのプライベート変数とGetExLogger関数を定義

という2つのことをExCommonモジュールで実装しています。

```vb:ExCommon.bas
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Module ExCommon
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit
Option Base 1


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Constant
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Enum LogLevel
    LOG_DEBUG = 1
    LOG_INFO
    LOG_WARNING
    LOG_ERROR
    LOG_CRITICAL
End Enum


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Variable
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Private singletonExLogger As ExLogger


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Public Function GetExLogger() As ExLogger
    If singletonExLogger Is Nothing Then
        Set singletonExLogger = New ExLogger
    End If
    Set GetExLogger = singletonExLogger
End Function
```

### 3.3. サンプルコードを動かしてみましょう

``Main``を動作させると以下のような出力がデバッグターミナルに出てきます。

```text:ログ（LOG_DEBUG）
2024-05-20 13:04:49 - INFO - MyModule.Main - Main Start
2024-05-20 13:04:49 - INFO - MyModule.MyFunction - Function Start
2024-05-20 13:04:49 - ERROR - MyModule.MyFunction - Invalid someValue: 0
2024-05-20 13:04:49 - DEBUG - MyModule.MyFunction - Return Value: 1
2024-05-20 13:04:49 - INFO - MyModule.MyFunction - Function End
2024-05-20 13:04:49 - INFO - MyModule.Main - Main End
```

次に、``MyModule``の``InitLogger``のデバッグレベルの値を``LOG_INFO``に変更して再度実行してみます。そうするとDEBUGログ出力が消え、5行のログとなります。

```text:ログ（LOG_INFO）
2024-05-20 13:31:54 - INFO - MyModule.Main - Main Start
2024-05-20 13:31:54 - INFO - MyModule.MyFunction - Function Start
2024-05-20 13:31:54 - ERROR - MyModule.MyFunction - Invalid someValue: 0
2024-05-20 13:31:54 - INFO - MyModule.MyFunction - Function End
2024-05-20 13:31:54 - INFO - MyModule.Main - Main End
```

最後に``MyModule``の``InitLogger``のデバッグレベルの値を``LOG_ERROR``に変更して再度実行してみます。そうするとINFOログ出力が消え、1行のログとなります。

```text:ログ（LOG_ERROR）
2024-05-20 13:34:04 - ERROR - MyModule.MyFunction - Invalid someValue: 0
```

これで、「VBAでロガークラスを作ってみよう（#1）」は終了です。反響がありそうでしたら#2、#3と発展させていこうと思います。