Logging
Note
Learn how to implement effective logging in PowerShell scripts - from simple console output to production-ready log files.
Overview
Logging is critical for troubleshooting, auditing, and monitoring your PowerShell scripts. Good logging helps you understand what your script did, when it did it, and what went wrong.
Why logging matters:
- Troubleshooting - Understand failures after they happen
- Auditing - Track who did what and when
- Monitoring - Detect issues in scheduled scripts
- Debugging - Understand script flow during development
- Compliance - Meet regulatory requirements
This guide covers everything from simple console messages to production-ready logging systems.
Understanding PowerShell Output Streams
PowerShell has six output streams, each for a different purpose:
| Stream | Cmdlet | Purpose | Captured by $? | In Variables |
|---|---|---|---|---|
| Success (1) | Write-Output | Normal output | ✅ Yes | ✅ Yes |
| Error (2) | Write-Error | Error messages | ❌ No | ✅ Yes ($Error) |
| Warning (3) | Write-Warning | Warning messages | ✅ Yes | ❌ No |
| Verbose (4) | Write-Verbose | Detailed info | ✅ Yes | ❌ No |
| Debug (5) | Write-Debug | Debug info | ✅ Yes | ❌ No |
| Information (6) | Write-Information | Informational | ✅ Yes | ✅ Yes (PS 5.0+) |
| Host (N/A) | Write-Host | Display only | ✅ Yes | ❌ No |
Quick Comparison
# Write-Output - Goes to pipeline (can be captured)
Write-Output "Processing data..."
$result = Get-Data # Can capture output
# Write-Host - Display only (cannot be captured)
Write-Host "Processing data..." -ForegroundColor Green
$result = Get-Data # Cannot capture Write-Host output
# Write-Verbose - Optional detailed messages
Write-Verbose "Looking up user in database..." # Only shows with -Verbose
# Write-Warning - Warning messages
Write-Warning "File size exceeds 1GB"
# Write-Error - Error messages
Write-Error "Failed to connect to server"
# Write-Information - Informational messages (PS 5.0+)
Write-Information "Backup completed: 150 files" -InformationAction Continue
Write-Host vs Write-Output vs Others
Write-Host (Display Only)
Use for: Progress messages, colored output, user-facing text
# Good uses of Write-Host
Write-Host "=== Starting Backup Process ===" -ForegroundColor Cyan
Write-Host "Progress: 25%" -ForegroundColor Yellow
Write-Host "SUCCESS: Backup complete!" -ForegroundColor Green
# Why it's useful
Write-Host "Processing..." -NoNewline
Start-Sleep -Seconds 2
Write-Host " Done!" -ForegroundColor Green
⚠️ Important: Write-Host output cannot be captured, redirected, or piped!
# This DOESN'T work
function Get-Data {
Write-Host "Here's your data" # Goes to console only
}
$result = Get-Data # $result is EMPTY!
# This DOES work
function Get-Data {
Write-Output "Here's your data" # Goes to pipeline
}
$result = Get-Data # $result contains the string
Write-Output (Pipeline Output)
Use for: Data you want to return, output that can be piped or captured
function Get-SystemInfo {
# Returns object - can be captured/piped
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
OS = (Get-CimInstance Win32_OperatingSystem).Caption
Uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
}
}
# Can capture output
$info = Get-SystemInfo
# Can pipe output
Get-SystemInfo | Export-Csv system.csv
Best practice: Don't use Write-Output explicitly - just output the object:
# Unnecessary
function Get-Name {
Write-Output "John"
}
# Better (implicit output)
function Get-Name {
"John"
}
Write-Verbose (Optional Details)
Use for: Detailed information that's normally hidden
function Get-UserData {
[CmdletBinding()] # Required for -Verbose to work
param([string]$Username)
Write-Verbose "Connecting to Active Directory..."
Write-Verbose "Looking up user: $Username"
$user = Get-ADUser -Identity $Username
Write-Verbose "Found user: $($user.Name)"
Write-Verbose "Email: $($user.EmailAddress)"
return $user
}
# Normal call (verbose messages hidden)
Get-UserData -Username "jdoe"
# With -Verbose (shows all messages)
Get-UserData -Username "jdoe" -Verbose
Write-Warning (Warnings)
Use for: Non-fatal issues that need attention
function Backup-Files {
param([string]$Path)
$files = Get-ChildItem -Path $Path
if ($files.Count -gt 10000) {
Write-Warning "Large backup detected: $($files.Count) files. This may take a while."
}
# Continue with backup...
}
Write-Error (Errors)
Use for: Error messages (doesn't stop execution unless ErrorActionPreference is Stop)
function Get-Data {
param([string]$Path)
if (-not (Test-Path $Path)) {
Write-Error "File not found: $Path"
return
}
# Continue processing...
}
Write-Information (Informational)
Use for: Informational messages that can be captured (PowerShell 5.0+)
function Process-Data {
[CmdletBinding()]
param()
Write-Information "Starting data processing..." -InformationAction Continue
# Process data...
Write-Information "Processed 150 records" -InformationAction Continue
}
# Can capture information stream
$info = Process-Data 6>&1 # Redirect information stream
Simple Logging Patterns
Basic File Logging
# Simple log entry
$logFile = "C:\Logs\script.log"
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $logFile -Value "[$timestamp] Script started"
# With error handling
try {
# Your code here
Add-Content -Path $logFile -Value "[$timestamp] Processing complete"
}
catch {
Add-Content -Path $logFile -Value "[$timestamp] ERROR: $_"
}
Console + File Logging
$logFile = "C:\Logs\backup.log"
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] $Message"
# Write to console
Write-Host $logEntry
# Write to file
Add-Content -Path $logFile -Value $logEntry
}
# Usage
Write-Log "Starting backup process"
Write-Log "Backing up 150 files"
Write-Log "Backup complete"
Log Levels
$logFile = "C:\Logs\script.log"
function Write-Log {
param(
[string]$Message,
[ValidateSet("INFO", "WARNING", "ERROR")]
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Color-coded console output
$color = switch ($Level) {
"INFO" { "White" }
"WARNING" { "Yellow" }
"ERROR" { "Red" }
}
Write-Host $logEntry -ForegroundColor $color
Add-Content -Path $logFile -Value $logEntry
}
# Usage
Write-Log "Script started" -Level INFO
Write-Log "Low disk space detected" -Level WARNING
Write-Log "Failed to connect to database" -Level ERROR
Creating a Reusable Logging Function
Production-Ready Write-Log Function
function Write-Log {
<#
.SYNOPSIS
Writes log messages to console and file with timestamps and levels
.PARAMETER Message
The message to log
.PARAMETER Level
Log level: INFO, WARNING, ERROR, DEBUG
.PARAMETER LogFile
Path to log file. If not specified, uses default location.
.PARAMETER NoConsole
Skip console output (file only)
.EXAMPLE
Write-Log "Process started"
.EXAMPLE
Write-Log "Disk space low" -Level WARNING
.EXAMPLE
Write-Log "Connection failed" -Level ERROR -LogFile "C:\Logs\custom.log"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]$Message,
[ValidateSet("INFO", "WARNING", "ERROR", "DEBUG")]
[string]$Level = "INFO",
[string]$LogFile = "C:\Logs\PowerShell_$(Get-Date -Format 'yyyyMMdd').log",
[switch]$NoConsole
)
process {
# Create log directory if it doesn't exist
$logDir = Split-Path $LogFile
if (-not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
# Format log entry
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Write to file
try {
Add-Content -Path $LogFile -Value $logEntry -ErrorAction Stop
}
catch {
Write-Warning "Failed to write to log file: $_"
}
# Write to console (unless NoConsole specified)
if (-not $NoConsole) {
$color = switch ($Level) {
"INFO" { "White" }
"WARNING" { "Yellow" }
"ERROR" { "Red" }
"DEBUG" { "Gray" }
}
Write-Host $logEntry -ForegroundColor $color
}
}
}
Using the Logging Function in Scripts
# At the start of your script
$logFile = "C:\Logs\MyScript_$(Get-Date -Format 'yyyyMMdd').log"
Write-Log "======================================" -LogFile $logFile
Write-Log "Script: MyScript.ps1" -LogFile $logFile
Write-Log "Started: $(Get-Date)" -LogFile $logFile
Write-Log "User: $env:USERNAME" -LogFile $logFile
Write-Log "Computer: $env:COMPUTERNAME" -LogFile $logFile
Write-Log "======================================" -LogFile $logFile
try {
Write-Log "Processing data..." -LogFile $logFile
# Your code here
Write-Log "Processing complete" -LogFile $logFile
}
catch {
Write-Log "Script failed: $_" -Level ERROR -LogFile $logFile
Write-Log $_.ScriptStackTrace -Level DEBUG -LogFile $logFile
exit 1
}
finally {
Write-Log "Script ended" -LogFile $logFile
}
Advanced Logging Patterns
Structured Logging (JSON Format)
function Write-StructuredLog {
param(
[string]$Message,
[string]$Level = "INFO",
[hashtable]$Properties = @{}
)
$logEntry = [PSCustomObject]@{
Timestamp = Get-Date -Format "o" # ISO 8601 format
Level = $Level
Message = $Message
Computer = $env:COMPUTERNAME
User = $env:USERNAME
ProcessId = $PID
Properties = $Properties
}
$json = $logEntry | ConvertTo-Json -Compress
Add-Content -Path "C:\Logs\structured.json" -Value $json
}
# Usage
Write-StructuredLog -Message "User login" -Properties @{
Username = "jdoe"
IPAddress = "192.168.1.100"
Success = $true
}
Write-StructuredLog -Message "File processed" -Properties @{
FileName = "data.csv"
RecordCount = 1500
Duration = "00:02:15"
}
Log Rotation
function Start-LogRotation {
param(
[string]$LogPath,
[int]$MaxSizeMB = 10,
[int]$KeepCount = 5
)
if (-not (Test-Path $LogPath)) {
return
}
$logFile = Get-Item $LogPath
$sizeMB = [math]::Round($logFile.Length / 1MB, 2)
if ($sizeMB -ge $MaxSizeMB) {
Write-Verbose "Log file is ${sizeMB}MB, rotating..."
# Create archive name with timestamp
$archiveName = "{0}_{1}{2}" -f
[IO.Path]::GetFileNameWithoutExtension($LogPath),
(Get-Date -Format "yyyyMMdd_HHmmss"),
[IO.Path]::GetExtension($LogPath)
$archivePath = Join-Path (Split-Path $LogPath) "Archive"
if (-not (Test-Path $archivePath)) {
New-Item -Path $archivePath -ItemType Directory | Out-Null
}
# Move current log to archive
Move-Item -Path $LogPath -Destination (Join-Path $archivePath $archiveName)
# Clean up old archives
Get-ChildItem -Path $archivePath |
Sort-Object CreationTime -Descending |
Select-Object -Skip $KeepCount |
Remove-Item -Force
Write-Verbose "Log rotated successfully"
}
}
# Use before logging
Start-LogRotation -LogPath "C:\Logs\script.log" -MaxSizeMB 10 -KeepCount 5
Write-Log "Starting script"
Transcript Logging
# Automatic transcript of entire session
$transcriptPath = "C:\Logs\Transcript_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
try {
Start-Transcript -Path $transcriptPath
# Your script here
Write-Host "This goes to transcript"
Get-Process | Select-Object -First 5
Stop-Transcript
}
catch {
Stop-Transcript
throw
}
Log File Management
Automatic Cleanup Function
function Remove-OldLogs {
<#
.SYNOPSIS
Removes log files older than specified days
.EXAMPLE
Remove-OldLogs -LogPath "C:\Logs" -DaysToKeep 30
#>
param(
[string]$LogPath = "C:\Logs",
[int]$DaysToKeep = 30,
[string]$Filter = "*.log"
)
$cutoffDate = (Get-Date).AddDays(-$DaysToKeep)
Get-ChildItem -Path $LogPath -Filter $Filter -Recurse |
Where-Object { $_.LastWriteTime -lt $cutoffDate } |
ForEach-Object {
Write-Verbose "Removing old log: $($_.Name)"
Remove-Item -Path $_.FullName -Force
}
}
# Usage
Remove-OldLogs -LogPath "C:\Logs" -DaysToKeep 30 -Verbose
Compress Old Logs
function Compress-OldLogs {
param(
[string]$LogPath = "C:\Logs",
[int]$DaysOld = 7
)
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
$oldLogs = Get-ChildItem -Path $LogPath -Filter "*.log" |
Where-Object { $_.LastWriteTime -lt $cutoffDate }
foreach ($log in $oldLogs) {
$zipName = "$($log.BaseName)_$($log.LastWriteTime.ToString('yyyyMMdd')).zip"
$zipPath = Join-Path $LogPath $zipName
if (-not (Test-Path $zipPath)) {
Compress-Archive -Path $log.FullName -DestinationPath $zipPath
Remove-Item -Path $log.FullName
Write-Verbose "Compressed: $($log.Name) -> $zipName"
}
}
}
Complete Logging Example
Here's a production-ready script with comprehensive logging:
<#
.SYNOPSIS
Production script with comprehensive logging
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$SourcePath,
[string]$LogPath = "C:\Logs"
)
# --- Configuration ---
$ErrorActionPreference = "Stop"
$scriptName = [IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
$logFile = Join-Path $LogPath "${scriptName}_$(Get-Date -Format 'yyyyMMdd').log"
# --- Logging Function ---
function Write-Log {
param(
[Parameter(Mandatory=$true)]
[string]$Message,
[ValidateSet("INFO", "WARNING", "ERROR", "DEBUG")]
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Ensure log directory exists
$logDir = Split-Path $logFile
if (-not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
# Write to file
Add-Content -Path $logFile -Value $logEntry
# Write to console with color
$color = switch ($Level) {
"INFO" { "White" }
"WARNING" { "Yellow" }
"ERROR" { "Red" }
"DEBUG" { "Gray" }
}
Write-Host $logEntry -ForegroundColor $color
}
# --- Main Script ---
try {
# Log startup
Write-Log "========================================" -Level INFO
Write-Log "Script Started: $scriptName" -Level INFO
Write-Log "User: $env:USERNAME" -Level INFO
Write-Log "Computer: $env:COMPUTERNAME" -Level INFO
Write-Log "PowerShell: $($PSVersionTable.PSVersion)" -Level INFO
Write-Log "Parameters:" -Level INFO
Write-Log " SourcePath: $SourcePath" -Level INFO
Write-Log " LogPath: $LogPath" -Level INFO
Write-Log "========================================" -Level INFO
# Validate prerequisites
Write-Log "Validating prerequisites..." -Level INFO
if (-not (Test-Path $SourcePath)) {
throw "Source path not found: $SourcePath"
}
Write-Log "Source path validated" -Level INFO
# Main processing
Write-Log "Starting main processing..." -Level INFO
$files = Get-ChildItem -Path $SourcePath -File
Write-Log "Found $($files.Count) files to process" -Level INFO
$processedCount = 0
$errorCount = 0
foreach ($file in $files) {
try {
Write-Log "Processing: $($file.Name)" -Level DEBUG
# Your processing logic here
# For example:
# Process-File -Path $file.FullName
$processedCount++
}
catch {
$errorCount++
Write-Log "Failed to process $($file.Name): $_" -Level ERROR
Write-Log $_.ScriptStackTrace -Level DEBUG
}
}
# Summary
Write-Log "========================================" -Level INFO
Write-Log "Processing Summary:" -Level INFO
Write-Log " Total Files: $($files.Count)" -Level INFO
Write-Log " Processed: $processedCount" -Level INFO
Write-Log " Errors: $errorCount" -Level INFO
if ($errorCount -gt 0) {
Write-Log "Script completed with errors" -Level WARNING
exit 1
}
else {
Write-Log "Script completed successfully" -Level INFO
exit 0
}
}
catch {
Write-Log "FATAL ERROR: $_" -Level ERROR
Write-Log $_.ScriptStackTrace -Level DEBUG
Write-Log "Script terminated abnormally" -Level ERROR
exit 1
}
finally {
Write-Log "Script ended: $(Get-Date)" -Level INFO
Write-Log "Log file: $logFile" -Level INFO
}
Tips & Tricks
Use Log Levels Consistently
# INFO - Normal operations
Write-Log "Backup started" -Level INFO
Write-Log "Processed 150 files" -Level INFO
# WARNING - Issues that don't stop execution
Write-Log "Disk space low: 5GB remaining" -Level WARNING
Write-Log "File already exists, skipping" -Level WARNING
# ERROR - Errors that need attention
Write-Log "Failed to connect to database" -Level ERROR
Write-Log "Access denied to folder" -Level ERROR
# DEBUG - Detailed troubleshooting (only when needed)
Write-Log "SQL Query: SELECT * FROM..." -Level DEBUG
Write-Log "API Response: {json}" -Level DEBUG
Always Create Log Directory First
function Write-Log {
param([string]$Message, [string]$LogFile)
# Create directory if it doesn't exist
$logDir = Split-Path $LogFile
if (-not (Test-Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
Add-Content -Path $LogFile -Value $Message
}
# Now safe to call without worrying about path
Write-Log "Started" "C:\Logs\SubFolder\app.log"
Mask Sensitive Data
# GOOD - Mask passwords and keys
$username = "jdoe"
Write-Log "Connecting as user: $username"
Write-Log "Authentication: Using provided credentials"
# GOOD - Redact partial data
$apiKey = "sk_live_abcdef1234567890"
$maskedKey = $apiKey.Substring(0, 7) + "***"
Write-Log "Using API key: $maskedKey"
# GOOD - Log success/failure, not content
Write-Log "API call succeeded" -Level INFO
Write-Log "Database query returned 150 records" -Level INFO
Log What Matters
# DO log these:
Write-Log "Script started: $(Get-Date)" -Level INFO
Write-Log "Parameters: Source=$Source, Dest=$Dest" -Level INFO
Write-Log "Processed 1500 files in 45 seconds" -Level INFO
Write-Log "Script completed successfully" -Level INFO
# DON'T log every detail:
# for ($i = 0; $i -lt 10000; $i++) {
# Write-Log "Loop iteration $i" # TOO MUCH!
# }
# Instead, log summaries:
Write-Log "Processing 10,000 items..." -Level INFO
# ... process items ...
Write-Log "Completed processing 10,000 items" -Level INFO
Never Log Passwords or Secrets
# NEVER DO THIS!
$credential = Get-Credential
Write-Log "Password: $($credential.GetNetworkCredential().Password)" # DANGEROUS!
# NEVER log these:
# - Passwords or credentials
# - API keys or tokens
# - Credit card numbers
# - Social security numbers
# - Any PII (personally identifiable information)
# DO THIS INSTEAD:
Write-Log "Authenticating as: $($credential.UserName)" -Level INFO
Write-Log "Using provided credentials" -Level INFO
Don't Use Write-Host for Data
# BAD - Cannot capture output
function Get-ServerList {
Write-Host "Server01"
Write-Host "Server02"
}
$servers = Get-ServerList # $servers is EMPTY!
# GOOD - Use proper output
function Get-ServerList {
"Server01"
"Server02"
}
$servers = Get-ServerList # Now $servers has the data
# Use Write-Host ONLY for display messages:
Write-Host "Processing..." -ForegroundColor Yellow
Batch Writes for Performance
# SLOW - Opens/closes file 10,000 times!
for ($i = 0; $i -lt 10000; $i++) {
Add-Content -Path "C:\Logs\app.log" -Value "Item $i"
}
# FAST - Collect in memory, write once
$logEntries = for ($i = 0; $i -lt 10000; $i++) {
"Item $i"
}
$logEntries | Out-File "C:\Logs\app.log" -Append
# Or use a buffer in your Write-Log function
Related Topics
- Error Handling - Handling and logging errors
- Functions - Creating reusable logging functions
- Script Structure - Organizing scripts with logging