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:
-
Always log the start and end of your script – This helps track script execution time and completion status.
-
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
-
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
-
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!