【テクニカル・上級編】【上級者向け】ページ追加・削除イベントをフックし、変更履歴を自動的にテキストログへ追記する監査ログシステム – Visio VBA解析バイブル

スポンサーリンク
スポンサーリンク

Visioにおける監査ログの極意:ClassモジュールとWindows APIを用いたページ操作の完全追跡

エンタープライズ環境において、Visioは単なる「作図ツール」ではない。それはシステムの構成定義、ネットワークトポロジ、あるいは複雑な業務フローを記述する「生きた設計図」である。

設計図の整合性が失われる瞬間、それはページが削除されたとき、あるいは意図しない変更が加えられたときだ。しかし、多くの開発者は「誰がいつ何をしたか」というトレーサビリティ(追跡可能性)の確保を軽視している。標準的なVBAの機能だけでは、ユーザーの意図的な操作を捕捉し、永続的なログとして残すことは不可能に近い。

本稿では、Classモジュールによるイベントハンドリング、Windows APIを用いた厳格なユーザー識別、そしてメモリ管理を最適化した「監査ログシステム」の構築手法を解説する。これは単なるコードの提示ではない。システムの整合性を守るための、アーキテクチャ設計の記録である。

1. アーキテクチャの設計思想

一般的な「ログ出力」は、特定のプロシージャが実行された際に書き込むものだ。しかし、監査ログの本質は「イベントへの介入」にある。ユーザーがVisioのUI(リボンやメニュー)から直接ページを追加・削除した場合、通常のコードベースのログは機能しない。

我々が実装すべきは、アプリケーションのライフサイクルに深く食い込み、`Document`オブジェクトのイベントを監視する「イベントリスナー・パターン」である。

コンポーネント構成

1. `clsAuditor` (Class Module): `Document`オブジェクトを `WithEvents` で保持し、ページ操作イベントをインターセプトする。
2. `modAPI` (Standard Module): `advapi32.dll` を呼び出し、環境変数に依存しない厳格なユーザー名取得を行う。
3. `ThisDocument`: アプリケーション起動時にリスナーをインスタンス化し、オブジェクトの生存期間を管理する。

2. 実装:厳格なユーザー識別とAPIの呼び出し

`Environ(“Username”)` を使うのは素人の仕事だ。環境変数はユーザーによって容易に偽装できる。真の監査ログには、OSレベルのトークンからユーザー名を特定するWindows APIが必要となる。

' --- modAPI: Windows API定義モジュール ---
Option Explicit

If VBA7 Then

' 64bit環境用宣言 Public Declare PtrSafe Function GetUserNameW Lib "advapi32.dll" (ByVal lpBuffer As LongPtr, ByRef nSize As Long) As Long

Else

' 32bit環境用宣言 Public Declare Function GetUserNameW Lib "advapi32.dll" (ByVal lpBuffer As Long, ByRef nSize As Long) As Long

End If

''' <summary> ''' Windows APIを用いて、セッション中の真正なユーザー名を取得する ''' </summary> Public Function GetRealUserName() As String Dim buffer As String * 255 Dim size As Long size = 256 ' API呼び出し If GetUserNameWStr(buffer, size) <> 0 Then ' Unicode文字列をVBA文字列に変換(Null終端まで) GetRealUserName = Left$(buffer, InStr(buffer, Chr$(0)) - 1) Else GetRealUserName = "Unknown_User" End If End Function ' 内部的なUnicode処理用ヘルパー Private Function GetUserNameWStr(ByVal buffer As String, ByRef size As Long) As Long ' 実装の簡略化のため、ここでは概念を示しているが、 ' 本来はStrPtrを用いた高度なメモリ操作が望ましい GetUserNameW(StrPtr(buffer), size) GetUserNameWStr = 0 ' Success End Function

3. 実装:Classモジュールによるイベント・フック

ここが心臓部だ。`Document` オブジェクトの `PageAdded` および `PageDeleted` イベントを捕捉する。

' --- clsAuditor: イベントリスナー・クラスモジュール ---
Option Explicit

' Documentオブジェクトへの参照をイベント付きで保持
Private WithEvents TargetDoc As Visio.Document
Private LogFilePath As String

''' <summary>
''' コンストラクタ:リスナーの初期化
''' </summary>
''' <param name="doc">監視対象のDocumentオブジェクト</param>
''' <param name="logPath">ログの出力先パス</param>
Public Sub Initialize(ByVal doc As Visio.Document, ByVal logPath As String)
    Set TargetDoc = doc
    LogFilePath = logPath
End Sub

''' <summary>
''' ページが追加された際のイベントハンドラ
''' </summary>
Private Sub TargetDoc_PageAdded(ByVal Page As Visio.Page)
    WriteLog "PAGE_ADD", "Page Name: " & Page.Name
End Sub

''' <summary>
''' ページが削除された際のイベントハンドラ
''' </summary>
Private Sub TargetDoc_PageDeleted(ByVal Page As Visio.Page)
    ' 注意: 削除イベント時、Pageオブジェクトの状態は不安定な場合がある
    ' 可能な限り、削除される直前の情報を保持する設計が望ましい
    WriteLog "PAGE_DELETE", "Page Name: " & Page.Name
End Sub

''' <summary>
''' ログの書き込みプロセス
''' </summary>
Private Sub WriteLog(ByVal action As String, ByVal details As String)
    On Error GoTo ErrHandler
    Dim fso As Object
    Dim ts As Object
    Dim userName As String
    Dim timestamp As String
    
    ' ユーザー情報の取得 (API経由)
    userName = GetRealUserName()
    timestamp = Format(Now, "yyyy-mm-dd HH:mm:ss")
    
    ' FileSystemObjectの利用(パフォーマンスと堅牢性のバランス)
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set ts = fso.OpenTextFile(LogFilePath, 8, True) ' 8 = ForAppending
    
    ' ログフォーマット: [Timestamp] [User] [Action] [Details]
    ts.WriteLine StringFormat("[%s] [%s] [%s] %s", timestamp, userName, action, details)
    
    ' オブジェクトの明示的な解放(メモリリーク防止)
    ts.Close
    Set ts = Nothing
    Set fso = Nothing
    Exit Sub

ErrHandler:
    ' 監査ログの失敗がメインプロセスを停止させてはならない
    ' ただし、エラー自体をイベントビューア等に投げるべきである
    Debug.Print "CRITICAL ERROR: Audit Log Failed - " & Err.Description
    If Not ts Is Nothing Then ts.Close
End Sub

' 文字列フォーマット用ヘルパー(簡易版)
Private Function StringFormat(ByVal fmt As String, ParamArray args() As Variant) As String
    Dim i As Long
    Dim res As String
    res = fmt
    For i = LBound(args) To UBound(args)
        res = Replace(res, StringFormat("[%s]", i), CStr(args(i))) ' 本来はより厳密な置換が必要
        ' 簡易実装として、単純な置換ロジックを想定
    Next i
    ' 本来は sprintf のような実装を自作する
    StringFormat = res 
End Function

*(※注: `StringFormat` は実装の簡略化のため概念を示しています。実運用では `Replace` を用いた再帰的な置換ロジックを実装してください)*

4. インスタンスのライフサイクル管理

Classモジュールは、そのインスタンスが破棄されるとイベントの監視も停止する。`ThisDocument` モジュールにおいて、グローバル変数としてインスタンスを保持し、ドキュメントの生存期間と同期させる必要がある。

' --- ThisDocument: ドキュメント起動時の制御 ---
Option Explicit

' インスタンスをモジュールレベルで保持することで、メモリ上に維持する
Private Auditor As clsAuditor

Private Sub Document_Open()
    Dim logPath As String
    logPath = ThisDocument.Path & "\audit_log.txt"
    
    ' リスナーの初期化
    Set Auditor = New clsAuditor
    Auditor.Initialize ThisDocument, logPath
    
    Debug.Print "Audit System Initialized. Logging to: " & logPath
End Sub

Private Sub Document_Close()
    ' 明示的なクリーンアップ
    If Not Auditor Is Nothing Then
        Set Auditor = Nothing
    End If
End Sub

5. 極限の知見:パフォーマンスと堅牢性への考察

シニアエンジニアとして、この実装をさらに高みへ引き上げるための知見を共有しておく。

オブジェクトのライフサイクルとメモリ管理

VBAにおける `Set obj = Nothing` は、単なる作法ではない。特に `WithEvents` を使用する場合、循環参照(Circular Reference)が発生すると、オブジェクトはメモリ上に残り続け、Visioのプロセス自体が終了できなくなる。`Document_Close` で確実に `Nothing` を代入することは、システム全体の安定性に直結する。

I/Oのボトルネック

上記のコードでは `FileSystemObject` を使用している。頻繁なページ操作が行われる環境では、ファイルを開閉するコストが無視できなくなる。超高頻度の操作を想定する場合、ログをメモリ上の `Collection` にバッファリングし、一定時間ごと、あるいは `Document_BeforeClose` 時に一括書き出しを行う「バッチ・ライティング」への転換を検討せよ。

セキュリティの限界

このシステムは「誰が何をしたか」を記録するが、「ログを消去する行為」を止めることはできない。真に堅牢な監査システムを構築するならば、ログの出力先をローカルファイルではなく、書き込み権限を限定したネットワーク上の共有ディレクトリ、あるいは外部のログ管理サーバー(Syslog等)のAPIへ直接POSTする設計にすべきだ。

結論

Visio VBAを用いた監査ログシステムの実装は、単なるプログラミングの範疇を超え、システムの「信頼性」を定義するアーキテクチャ設計そのものである。Classモジュールによるイベントの抽象化、Windows APIによるアイデンティティの確立、そして厳格なメモリ管理。これらを組み合わせることで、初めて「プロフェッショナルな業務ツール」としての地位を確立できるのである。

タイトルとURLをコピーしました