PowerShell Write-Log: A Simple Logging Function Tutorial

Published:25 December 2024 - 5 min. read

If you’re writing PowerShell scripts that do anything meaningful, you need logging. Whether you’re deploying software, managing services, or automating tasks, having a record of what your script did (or didn’t do) is crucial. In this tutorial, you’ll learn how to create a simple but effective PowerShell logging function.

Prerequisites

If you’d like to follow along with this tutorial, be sure you have:

  • Windows 10 or Windows Server with PowerShell 5.1 or PowerShell 7+
  • A text editor (VSCode recommended)
  • Basic understanding of PowerShell functions

The Problem with Basic Logging

Let’s say you’re writing a script to silently install some software. The basic approach might look something like this:

Add-Content -Path "C:\\Scripts\\install.log" -Value "Starting install..."
Start-Process -FilePath 'installer.exe' -ArgumentList '/i /s' -Wait -NoNewWindow
Add-Content -Path "C:\\Scripts\\install.log" -Value "Finished install."

This works, but it has some issues:

  • No timestamps
  • Repetitive code
  • Inconsistent logging format
  • Hard-coded log path

Let’s fix these problems by building a proper logging function.

Building a Basic Write-Log Function

First, let’s create a simple function that adds timestamps to our log entries:

function Write-Log {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    $timeGenerated = Get-Date -Format HH:mm:ss
    Add-Content -Path "C:\\Scripts\\script.log" -Value "$timeGenerated - $Message"
}

Now you can use it like this:

Write-Log -Message "Starting install..."
Start-Process -FilePath 'installer.exe' -ArgumentList '/i /s' -Wait -NoNewWindow
Write-Log -Message "Finished install."

The log file (C:\Scripts\script.log) will contain entries that look like:

09:42:15 - Starting install...
09:43:22 - Finished install.

Much cleaner! But we can do better.

Adding More Functionality

Let’s enhance our logging function with some useful features:

  • Custom log paths
  • Different log levels (Info, Warning, Error)
  • Date in filename
  • Error handling

Here’s the improved version:

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$LogFilePath = "C:\\Scripts\\Logs",

        [Parameter()]
        [ValidateSet('Information','Warning','Error')]
        [string]$Level = "Information"
    )

    # Create the log directory if it doesn't exist
    if (!(Test-Path $LogFilePath)) {
        New-Item -Path $LogFilePath -ItemType Directory -Force | Out-Null
    }

    # Build the log file path with date
    $date = Get-Date -Format "MM-dd-yyyy"
    $logFile = Join-Path $LogFilePath "log-$date.txt"

    # Get the current timestamp
    $timeStamp = Get-Date -Format "HH:mm:ss"

    # Create the log entry
    $logEntry = "$timeStamp [$Level] - $Message"

    try {
        Add-Content -Path $logFile -Value $logEntry -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to write to log file: $_"
    }
}

This enhanced version gives you much more flexibility. Here’s how to use it:

# Basic information logging
Write-Log -Message "Starting software installation"

# Warning about a non-critical issue
Write-Log -Message "Config file not found, using defaults" -Level Warning

# Log an error
Write-Log -Message "Installation failed!" -Level Error

# Use a custom log path
Write-Log -Message "Custom path log" -LogFilePath "D:\\CustomLogs"

The resulting log file (log-03-12-2024.txt) will look like this:

10:15:22 [Information] - Starting software installation
10:15:23 [Warning] - Config file not found, using defaults
10:15:25 [Error] - Installation failed!

And in D:\CustomLogs\log-03-12-2024.txt:

10:15:26 [Information] - Custom path log

Notice how each entry includes the timestamp, log level in brackets, and the message. This structured format makes it easy to parse logs and quickly identify issues.

Real-World Example: Software Installation Script

Let’s put our logging function to work in a real script that installs software silently:

# First, dot-source the logging function
. .\\Write-Log.ps1

# Script variables
$installer = "C:\\Installers\\software.exe"
$logPath = "C:\\Scripts\\InstallLogs"

# Start logging
Write-Log -Message "Beginning installation process" -LogFilePath $logPath

# Check if installer exists
if (Test-Path $installer) {
    Write-Log -Message "Found installer at: $installer"

    try {
        # Attempt installation
        Write-Log -Message "Starting installation..."
        $process = Start-Process -FilePath $installer -ArgumentList '/i /s' -Wait -NoNewWindow -PassThru

        # Check the exit code
        if ($process.ExitCode -eq 0) {
            Write-Log -Message "Installation completed successfully"
        }
        else {
            Write-Log -Message "Installation failed with exit code: $($process.ExitCode)" -Level Error
        }
    }
    catch {
        Write-Log -Message "Installation failed with error: $_" -Level Error
    }
}
else {
    Write-Log -Message "Installer not found at: $installer" -Level Error
}

Write-Log -Message "Installation script completed"

The resulting log file will look something like this:

09:15:22 [Information] - Beginning installation process
09:15:22 [Information] - Found installer at: C:\\Installers\\software.exe
09:15:22 [Information] - Starting installation...
09:16:45 [Information] - Installation completed successfully
09:16:45 [Information] - Installation script completed

Useful Tips

Here are some best practices when using this logging function:

  1. Always log the start and end of your script – This helps track script execution time and completion status.

  2. Use appropriate log levels – Don’t mark everything as an error; use the right level for the situation:

    • Information: Normal operations
    • Warning: Non-critical issues
    • Error: Critical problems that need attention
  3. Include relevant details – Log enough information to understand what happened:

    # Bad
    Write-Log "Failed to connect"
    
    # Good
    Write-Log "Failed to connect to server 'SQL01' - timeout after 30 seconds" -Level Error
    
  4. Clean up old logs – Consider adding log rotation to prevent filling up disk space:

    # Delete logs older than 30 days
    Get-ChildItem -Path $LogFilePath -Filter "*.txt" |
        Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
        Remove-Item
    

Conclusion

A good logging function is essential for any serious PowerShell script. With the Write-Log function we’ve built, you now have a flexible and reusable way to add proper logging to all your scripts. Remember to adapt the function to your specific needs – you might want to add features like:

Log Rotation

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [Parameter()]
        [int]$MaxLogFiles = 30  # Keep last 30 days of logs
    )

    # Remove old log files
    Get-ChildItem -Path $LogFilePath -Filter "*.txt" |
        Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$MaxLogFiles) } |
        Remove-Item -Force

    # Continue with normal logging...
}

Different Output Formats (CSV, JSON)

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [Parameter()]
        [ValidateSet('TXT','CSV','JSON')]
        [string]$Format = 'TXT'
    )

    $logEntry = [PSCustomObject]@{
        Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        Level = $Level
        Message = $Message
    }

    switch ($Format) {
        'CSV'  { $logEntry | Export-Csv -Path "$LogFilePath\\log.csv" -Append -NoTypeInformation }
        'JSON' { $logEntry | ConvertTo-Json | Add-Content -Path "$LogFilePath\\log.json" }
        'TXT'  { "$($logEntry.Timestamp) [$($logEntry.Level)] - $($logEntry.Message)" |
                 Add-Content -Path "$LogFilePath\\log.txt" }
    }
}

Network Path Support

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [Parameter()]
        [string]$NetworkPath = "\\\\server\\logs"
    )

    # Test network path connectivity
    if (!(Test-Path $NetworkPath)) {
        # Fallback to local logging if network is unavailable
        $NetworkPath = "C:\\Scripts\\Logs"
        Write-Warning "Network path unavailable. Using local path: $NetworkPath"
    }

    # Continue with normal logging...
}

Email Notifications for Errors

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Message,

        [Parameter()]
        [string]$SmtpServer = "smtp.company.com",

        [Parameter()]
        [string[]]$NotifyOnError = "[email protected]"
    )

    # Normal logging first...

    # Send email if this is an error
    if ($Level -eq 'Error' -and $NotifyOnError) {
        $emailParams = @{
            From = "[email protected]"
            To = $NotifyOnError
            Subject = "PowerShell Script Error"
            Body = "Error occurred at $timeStamp`n`nMessage: $Message"
            SmtpServer = $SmtpServer
        }

        try {
            Send-MailMessage @emailParams
        }
        catch {
            Write-Warning "Failed to send error notification: $_"
        }
    }
}

The key is to start with a solid foundation and build up from there based on your specific needs. These examples should give you a good starting point for extending the basic logging function with more advanced features.

For example, you might combine several of these features into a single, comprehensive logging solution:

Write-Log -Message "Critical error in payment processing" `
          -Level Error `
          -Format CSV `
          -NetworkPath "\\\\server\\logs" `
          -NotifyOnError "[email protected]","[email protected]" `
          -MaxLogFiles 90

This would:

  • Log the error in CSV format
  • Store it on a network share
  • Email multiple recipients
  • Maintain 90 days of log history

Remember to test thoroughly, especially when implementing network paths or email notifications, as these external dependencies can affect your script’s reliability. Happy scripting!

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!