(Qiitaに初めて投稿してみます。お作法間違い等ありましたらご容赦ください。)
1. ロガークラスを作りたい背景
VBAってPythonなどの最近の言語と比較すると、ロギングの機能がとっても貧弱ですよね。普通にVBAでログを書こうとすると、
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. 外部からの利用イメージ
まず最初に、外部からロガークラスが利用されるときのサンプルコードを考えてみます。
' プライベート変数
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
このとき、出力されるログはこんなイメージです。
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
)
という構成になります。
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' 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モジュールで実装しています。
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' 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
を動作させると以下のような出力がデバッグターミナルに出てきます。
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行のログとなります。
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行のログとなります。
2024-05-20 13:34:04 - ERROR - MyModule.MyFunction - Invalid someValue: 0
これで、「VBAでロガークラスを作ってみよう(#1)」は終了です。反響がありそうでしたら#2、#3と発展させていこうと思います。