Simple Logging Framework for VBScript Files

If you use VB Script files to automate recurring tasks, adding support for severity-based logging offers cheap insurance against future problems.

Simple Logging Framework for VBScript Files

I often use VB Script files to automate simple, recurring tasks.

I know there are "better" alternatives, but for a quick and dirty solution no other option satisfies all of the following criteria:

  • Runs in all versions of Windows with nothing to install (unlike Python)
  • No onerous security restrictions (unlike Powershell)
  • More capable than a simple Windows batch file (.bat)
  • Hasn't been updated in years (decades?) so it's rock solid dependable
  • Integrates well with Windows Task Scheduler to automate recurring tasks

It's greatest strength–simplicity–is also its biggest weakness.  

It does not enjoy the same kind of third-party framework support as other alternatives, such as Python or Powershell.  As a result, you'll need to build such support from scratch.  Today, I'm going to demonstrate how to do that when it comes to logging.

Let's build a simple logging framework for our VB Scripts.

The Logging.vbs File

I like to keep my logging functions in a separate file from my main script.  

In the next section, I will show you how to call this file.  But for now, let's jump straight to the code.  To begin, copy and paste the code below into a file named Logging.vbs.

The Code

Option Explicit

'Logging levels
Const CRITICAL = 1   'Log errors only
Const INFO = 2       'Log file copies and moves only
Const TRACE = 3      'Log verbose output for troubleshooting

Function GetScriptPath() 'As String
    Dim objShell: Set objShell = CreateObject("Wscript.Shell")
    Dim fpPath: fpPath = Wscript.ScriptFullName

    Dim objFSO: Set objFSO = CreateObject("Scripting.FileSystemObject")
    Dim objFile: Set objFile = objFSO.GetFile(fpPath)
    
    GetScriptPath = objFSO.GetParentFolderName(objFile) 
End Function

Sub WriteLog(LogLevel, ByVal Msg)
    Const FORAPPENDING = 8
    Dim fpLog: fpLog = GetScriptPath() & "\" & g_LogBase & ".log"
    Dim oFSO: Set oFSO = CreateObject("Scripting.FileSystemObject")
    Dim oFile
    ' Validate the file exists before attempting to write, create if it does not
    If oFSO.FileExists(fpLog) Then
        Set oFile = oFSO.OpenTextFile(fpLog, FORAPPENDING)
    Else
        Set oFile = oFSO.CreateTextFile(fpLog, True)
    End If
    ' Add timestamp to the entry
    Msg = "[" & FormatDateTime(Now()) & "]: " & Msg
    ' Check the logging level passed to append the correct suffix to the message
    Select Case LogLevel
        Case CRITICAL
            Msg = "ERROR " & Msg & "; [" & Err.Number & "] " & Err.Description
        Case INFO
            Msg = "Info  " & Msg
        Case TRACE
            Msg = "Debug " & Msg
        Case Else
            Msg = "ERROR: Logging level incorrectly set, defaulting to debugging."
            Msg = "Debug " & Msg
    End Select
    ' If logging message is less than or equal to the global log level write the message
    If LogLevel <= g_LogLevel Then
        oFile.WriteLine Msg
    End If
    ' Close the log
    oFile.Close
End Sub
This code is saved to a file named Logging.vbs.

How It Works

This VBScript code defines a function and a subroutine to write log messages to a file.

The GetScriptPath() function is used to get the path of the current VBScript file. It does this by creating a Wscript.Shell object and calling the Wscript.ScriptFullName property to get the full path of the current script, and then using the Scripting.FileSystemObject to get the parent folder path.

The WriteLog subroutine is used to write log messages to a log file. It takes two arguments: LogLevel and Msg. LogLevel determines the severity of the log message (e.g. critical, informational, or verbose). Msg is the message to be logged.

The subroutine uses theFORAPPENDING constant when opening the log file to specify that new log entries should be appended to the end of the file. It then creates a variable fpLog which is set to the full path of the log file, by appending the script path obtained from GetScriptPath() with the log file name defined in a variable LogBase.

The subroutine then uses the Scripting.FileSystemObject to check whether the log file exists, and opens it in append mode if it does, or creates a new file if it does not. It then appends a timestamp to the log message, and checks the log level passed to the subroutine. Based on the log level, it appends the correct prefix to the message (e.g. "ERROR" for critical log messages, "Info" for informational messages, "Debug" for verbose messages).

Finally, it checks whether the log level is less than or equal to a global log level (g_LogLevel). If it is, the log message is written to the log file using the WriteLine method, and the log file is closed. If not, the log message is not written to the file.

The Main Script File

This is the file with all of the actual business logic.

There are a few things that must be included in your main script file.  

  • Global constant g_LogBase: the base name of your log file
  • Global constant g_LogLevel: the logging level you want (usually 3 in development and 1 or 2 in production)
  • Subroutine IncludeRelative: used to "import" external VBScript files

Here's a minimal example:

Option Explicit

'Define constants for Logging.vbs
Const g_LogBase = "MinimalSample"   ' <--- Set your log file's base name
Const g_LogLevel = 3    'Critical: 1; Info: 2; Trace: 3

IncludeRelative("Logging.vbs") ' Path to file with VBS Logging functions

'-----------------------------------------------------------------------
'--== YOUR CODE GOES HERE ==--'
WriteLog TRACE, "This is a message for use in debugging"
WriteLog INFO, "This is a message for use in production"

On Error Resume Next
Dim Dummy: Dummy = 1/0
WriteLog CRITICAL, "This message gets error Num and Description auto-added"
On Error Goto 0
'-----------------------------------------------------------------------


Sub IncludeRelative(sInstFile)
    Dim objShell: Set objShell = CreateObject("Wscript.Shell")
    Dim fpPath: fpPath = Wscript.ScriptFullName
    Dim oFSO: Set oFSO = CreateObject("Scripting.FileSystemObject")
    Dim oFile: Set oFile = oFSO.GetFile(fpPath)
    Dim foScript: foScript = oFSO.GetParentFolderName(oFile)
    Dim fpInclude: fpInclude = foScript & "\" & sInstFile
    
    On Error Resume Next
    If oFSO.FileExists(fpInclude) Then
        Dim f: Set f = oFSO.OpenTextFile(fpInclude)
        Dim s: s = f.ReadAll
        f.Close
        ExecuteGlobal s
    End If
    On Error Goto 0
    Set f = Nothing
    Set oFSO = Nothing
    
End Sub
Save the above code to a file named Minimal.vbs. IMPORTANT: Make sure it is saved in the same folder as the Logging.vbs file from above.

How It Works

This VBScript file is an example of how to use the logging functions defined in the "Logging.vbs" file shown earlier. It defines constants to set the log file's base name and the logging level.

The IncludeRelative subroutine is used to load the "Logging.vbs" file, which contains the logging functions used in this script. It uses the ExecuteGlobal function to execute the code from "Logging.vbs" in the context of this script.

The code between the comment lines "--== YOUR CODE GOES HERE ==--" is an example of how to use the logging functions. It calls WriteLog function with different logging levels and messages. The first message is written with the TRACE logging level, which is the most verbose logging level. The second message is written with the INFO logging level, which is used to log file copies and moves. The third message is written with the CRITICAL logging level and includes an intentional error to demonstrate how the Err object's values can be automatically added to the log message.

Overall, this script demonstrates how to use the logging functions defined in "Logging.vbs" to log messages with different logging levels and how to include the logging functions in another script using the IncludeRelative subroutine.

Sample Output

Here's what the created log file looks like when we run the above script:

Typical Usage

Here's a more typical scenario:

Option Explicit

'Define constants for Logging.vbs
Const g_LogBase = "MySampleScript"
Const g_LogLevel = 3    'Critical: 1; Info: 2; Trace: 3

'Exit codes
Const EC_FileCreateFailed = 100
Const EC_Success = 0

IncludeRelative("Logging.vbs") ' Path to file with VBS Logging functions

Main

Sub Main()
    WriteLog TRACE, " --== STARTING Sample SCRIPT ==--"
    
    'Initialize variables
    WriteLog TRACE, "Initializing variables"

    'Simulate creation of new file (fails 25% of the time)
    Randomize  'Required to avoid getting 0.7055475 every time you run Rnd()
    Dim RandomResult: RandomResult = Rnd()
    WriteLog TRACE, "Random result: " & RandomResult
    
    Dim FileCreated
    If RandomResult > 0.25 Then
        FileCreated = True
    Else
        FileCreated = False
    End If

    'Log success at the INFO level, log failure at the CRITICAL level
    If FileCreated Then
        WriteLog INFO, "Created file"
    Else
        WriteLog CRITICAL, "File creation failed!"
        WScript.Quit EC_FileCreateFailed
    End If

    WriteLog TRACE, " --== Sample SCRIPT COMPLETE ==--" & vbNewLine
    WScript.Quit EC_Success
End Sub

Sub IncludeRelative(sInstFile)
    Dim objShell: Set objShell = CreateObject("Wscript.Shell")
    Dim fpPath: fpPath = Wscript.ScriptFullName
    Dim oFSO: Set oFSO = CreateObject("Scripting.FileSystemObject")
    Dim oFile: Set oFile = oFSO.GetFile(fpPath)
    Dim foScript: foScript = oFSO.GetParentFolderName(oFile)
    Dim fpInclude: fpInclude = foScript & "\" & sInstFile
    
    On Error Resume Next
    If oFSO.FileExists(fpInclude) Then
        Dim f: Set f = oFSO.OpenTextFile(fpInclude)
        Dim s: s = f.ReadAll
        f.Close
        ExecuteGlobal s
    End If
    On Error Goto 0
    Set f = Nothing
    Set oFSO = Nothing
    
End Sub
Save the above code to a file named Sample.vbs. IMPORTANT: Make sure it is saved in the same folder as the Logging.vbs file from above.

How It Works

This VBScript file defines a script named "Sample" that simulates the creation of a new file and logs the result using the logging functions defined in the "Logging.vbs" file.   As with the Minimal.vbs sample, the script begins by setting some constants for the logging functions, including the base name for the log file and the logging level.

As with the minimal sample, the script then includes the contents of the "Logging.vbs" file using a custom function named IncludeRelative.

The Main subroutine begins by logging a message at the TRACE level indicating that the script is starting. It then initializes some variables and generates a random number between 0 and 1 using the Rnd() function. If this random number is greater than 0.25, the script sets a variable named FileCreated to True, indicating that the simulated file creation was successful. Otherwise, it sets FileCreated to False.

The script then logs either an INFO or a CRITICAL message depending on the value of FileCreated. If FileCreated is True, the script logs an INFO message indicating that the file was successfully created. If FileCreated is False, the script logs a CRITICAL message indicating that the file creation failed, and then exits with an error code of 100 using the Quit() function.

Finally, the Main subroutine logs a message at the TRACE level indicating that the script is complete and exits with a success code of 0.

Sample Output

I ran this VB Script several times, setting g_LogLevel to different values.  I color-coded the output to correspond with the log level at the time the entries were appended:

  • 1 (CRITICAL): Pink highlight
  • 2 (INFO): Yellow highlight
  • 3 (TRACE): Green highlight

If you look closely, you'll see I used the logging messages to troubleshoot the sample script itself.  When I first used the Rnd() function, I forgot to randomize it.  Every call to Rnd() returned 0.7055475.  Once I added the Randomize call, the next iteration (starting on line 26) returned 0.132992–which finally simulated a file creation failure.

For the final red section, I had to execute the script about 30 times to produce the eight ERROR lines that you see in the screenshot–the Info lines did not print when the logging level was set to Critical.

Conclusion

By their nature, VB scripts tend to be quick and dirty.  But that doesn't mean we need to make things unnecessarily hard on ourselves.

I don't write VB scripts often, but when I do, it's usually because I'm trying to automate a recurring task. The thing with these sorts of recurring tasks is that, once you set them up, they often run for months or even years without requiring any maintenance.  

Until one day they break.

If you didn't bother to set up logging when you wrote the script, you'll be in for a nasty surprise.  Even with a simple script it can take over an hour to figure out how it's supposed to work.  And that's before you've had a chance to start thinking about why it suddenly stopped working.

If you took the time to set up logging, here's how the process will go instead:

  1. Check the log files to see exactly when the task stopped
  2. Look for any ERROR lines that might indicate the source of the problem
  3. Change the logging level to 3 to enable TRACE messages
  4. Execute the script and let the trace messages guide you
  5. Easily add additional TRACE messages to further pinpoint the problem
  6. Solve the problem
  7. Bask in adulation

Acknowledgements
  • Portions of this article's body generated with the help of ChatGPT

Cover image created with Microsoft Designer

BUG FIX [2023-06-30]: Changed LogBase to g_LogBase in WriteLog() function to match declaration in Logging.vbs (h/t bc deewhy in the comments below).

All original code samples by Mike Wolfe are licensed under CC BY 4.0