Logging is the practice of recording operational events, execution status, and diagnostic messages to persistent storage. It captures what a script did, when it ran, and what output or errors it produced, which is essential for auditing, troubleshooting, and compliance in production automation. PowerShell provides built-in mechanisms for recording full session transcripts, emitting events directly into the Windows Event Log, enforcing enterprise-wide execution auditing via Group Policy, and implementing structured custom file loggers.
Session Transcription: Start-Transcript and Stop-Transcript
The Start-Transcript cmdlet records all interactive commands, console text output, and error streams into a single plain text file. It operates as an automated recorder for entire shell sessions or unattended script executions. Once work is finished, call Stop-Transcript to flush remaining buffers and close the file handle.
$TranscriptFile = "$PSScriptRoot\execution-transcript.log"
Start-Transcript -Path $TranscriptFile -Append
Write-Output "Executing scheduled storage assessment..."
Get-Service -Name "Spooler" | Select-Object -Property Name, Status
Stop-Transcriptwhere:
-Pathdefines the target output file path.-Appendadds new transcript entries to the existing file rather than overwriting it.Stop-Transcriptfinalizes the transcript block and logs execution duration.
Execute the script from the console:
pwsh ./record-session.ps1and the output file contains:
**********************
Windows PowerShell transcript start
Start time: 20260927225443
Username: TUTREF\Tom
RunAs User: TUTREF\Tom
Configuration Name:
Machine: WEB01 (Microsoft Windows NT 10.0.19045.0)
Host Application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Process ID: 2528
PSVersion: 5.1.19041.6456
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.19041.6456
BuildVersion: 10.0.19041.6456
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
Transcript started, output file is C:\Users\Tom\Desktop\execution-transcript.log
Executing scheduled storage assessment...
**********************
Windows PowerShell transcript end
End time: 20260927225443
**********************Writing to the Windows Event Log: Write-EventLog
On Windows environments, scripts can record operational status, alerts, and errors directly into the centralized Windows Event Log using Write-EventLog.
Registering an Event Source
Before writing custom entries to an event log, you must register a custom Source identifier. Registering an event source alters the system registry (HKLM:\SYSTEM\CurrentControlSet\Services\EventLog) and requires administrator privileges:
if (-not [System.Diagnostics.EventLog]::SourceExists("StorageAuditor")) {
New-EventLog -LogName "Application" -Source "StorageAuditor"
}Emitting Structured Event Entries
Once the source is registered, standard users and scripts can emit events using Write-EventLog:
$EventParams = @{
LogName = "Application"
Source = "StorageAuditor"
EventId = 1001
EntryType = "Information"
Message = "Storage audit completed. All logical partitions operating within safety limits."
}
Write-EventLog @EventParams
Write-Output "Event successfully recorded in Application log."where:
LogNamespecifies the target log container (typicallyApplication).Sourcematches the pre-registered application identifier.EventIdprovides an integer identifier used by monitoring systems (SIEMs) for indexing.EntryTypeassigns severity:Information,Warning, orError.@EventParamsutilizes parameter splatting to pass the arguments cleanly.
pwsh ./log-windows-event.ps1Event successfully recorded in Application log.To verify the event was logged:
Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'StorageAuditor' } -MaxEvents 1 |
Select-Object TimeCreated, Id, LevelDisplayName, MessageTimeCreated Id LevelDisplayName Message
----------- -- ---------------- -------
10/24/2026 7:15:22 PM 1001 Information Storage audit completed. All logical partitions...PowerShell Script Logging via Group Policy
Enterprise administrators can mandate system-wide PowerShell logging via Active Directory Group Policy Objects (GPOs) or local computer policy (gpedit.msc). These policies capture script execution across all users, preventing administrators or attackers from clearing local console history to hide activity.
The settings reside under: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell
The main PowerShell auditing mechanisms are:
- Module Logging
- Records pipeline execution events for specified PowerShell modules.
- Commonly associated with Event ID 4103.
- Administrators can configure logging for specific modules, such as
Microsoft.PowerShell.*.
- Script Block Logging
- Records PowerShell script blocks as they are executed.
- Commonly associated with Event ID 4104.
- Can provide visibility into script content, including code that has been obfuscated before execution.
- PowerShell Transcription
- Records PowerShell sessions as text transcripts.
- Can capture commands and output from user sessions.
- Organizations can configure transcript storage on a centralized location, such as a network share, to improve collection and retention.
Core Security Logging Policies
| Feature | Location | Purpose |
|---|---|---|
| Module Logging | Computer Configuration > Administrative Templates > Windows Components > Windows PowerShell |
Records pipeline execution details, evaluated parameters, and cmdlet invocations for specified modules. Setting the module pattern to * records all system commands. |
| Script Block Logging | Same path | Captures the complete content of script blocks as they are parsed and executed by the PowerShell engine. Even if an attacker executes code that is base64-encoded, XOR-encrypted, or dynamically invoked using Invoke-Expression, the engine logs the final decoded script block right before execution. Stored in Applications and Services Logs > Microsoft > Windows > PowerShell/Operational. |
| Transcription | Same path | Automatically initializes Start-Transcript across every PowerShell process launched on the machine. Allows configuring a read-only, append-only centralized network share path (e.g., \\AuditServer\PSTranscripts$), preventing local users from altering or deleting their own logs. |
Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -ErrorAction SilentlyContinueEnableScriptBlockLogging : 1where A value of 1 for EnableScriptBlockLogging confirms the policy is active, and detailed script execution is being logged to the Microsoft-Windows-PowerShell/Operational event log.
Production Custom Logging Function Pattern
While transcripts and event logs serve auditing purposes, production automation scripts typically require formatted, timestamped log files on disk. A production-ready logger should support distinct severity levels, write structured entries to disk, and echo formatted status to the console host.
The following reusable function demonstrates a standard enterprise logging pattern:
function Write-Log {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Message,
[Parameter(Position = 1)]
[ValidateSet("INFO", "WARN", "ERROR", "DEBUG")]
[string]$Level = "INFO",
[string]$LogPath = "$PSScriptRoot\application.log"
)
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$LogRecord = "{0} [{1,-5}] {2}" -f $Timestamp, $Level, $Message
Add-Content -Path $LogPath -Value $LogRecord -Encoding utf8
switch ($Level) {
"INFO" { Write-Host $LogRecord -ForegroundColor Cyan }
"WARN" { Write-Host $LogRecord -ForegroundColor Yellow }
"ERROR" { Write-Host $LogRecord -ForegroundColor Red }
"DEBUG" { Write-Host $LogRecord -ForegroundColor Gray }
}
}where:
[ValidateSet("INFO", "WARN", "ERROR", "DEBUG")]restricts severity input to known logging tiers.Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"includes millisecond-level precision."{0} [{1,-5}] {2}" -f ...uses composite string formatting to ensure column-aligned output.Add-Contentsafely appends the log line to disk using UTF-8 encoding.- The
switchblock also surfaces the message through the appropriate console color based on severity.
Practical Example: Automated Maintenance with Unified Logging
The following script integrates the Write-Log pattern into an administrative workflow that inspects a backup directory, removes outdated archives, and handles potential errors:
. "$PSScriptRoot\Write-Log.ps1"
$TargetDirectory = "$env:TEMP\MaintenanceBackup"
$RetentionDays = 14
$CutoffDate = (Get-Date).AddDays(-$RetentionDays)
Write-Log -Message "Starting maintenance purge cycle for: $TargetDirectory" -Level INFO
if (-not (Test-Path -Path $TargetDirectory)) {
Write-Log -Message "Directory does not exist. Creating path." -Level WARN
$null = New-Item -ItemType Directory -Path $TargetDirectory -Force
}
try {
$PurgeCandidates = Get-ChildItem -Path $TargetDirectory -File |
Where-Object { $_.LastWriteTime -lt $CutoffDate }
Write-Log -Message "Discovered $($PurgeCandidates.Count) expired files to remove." -Level INFO
foreach ($file in $PurgeCandidates) {
Remove-Item -Path $file.FullName -Force
Write-Log -Message "Purged: $($file.Name) (Modified: $($file.LastWriteTime))" -Level DEBUG
}
Write-Log -Message "Maintenance purge cycle finished successfully." -Level INFO
}
catch {
Write-Log -Message "Purge failed with terminating error: $($_.Exception.Message)" -Level ERROR
exit 1
}where:
. "$PSScriptRoot\Write-Log.ps1"dot-sources the logging function into the current session.Write-Logis invoked across multiple severity levels (INFO,WARN,DEBUG,ERROR).try/catchintercepts runtime exceptions and records the failure message before exiting.
Execute the script from the console:
pwsh ./run-maintenance-cycle.ps12026-10-24 19:35:12.102 [INFO ] Starting maintenance purge cycle for: C:\Users\Tom\AppData\Local\Temp\MaintenanceBackup
2026-10-24 19:35:12.145 [WARN ] Directory does not exist. Creating path.
2026-10-24 19:35:12.180 [INFO ] Discovered 0 expired files to remove.
2026-10-24 19:35:12.182 [INFO ] Maintenance purge cycle finished successfully.Inspect the persistent file created on disk:
Get-Content -Path "$PSScriptRoot\application.log"2026-10-24 19:35:12.102 [INFO ] Starting maintenance purge cycle for: C:\Users\Tom\AppData\Local\Temp\MaintenanceBackup
2026-10-24 19:35:12.145 [WARN ] Directory does not exist. Creating path.
2026-10-24 19:35:12.180 [INFO ] Discovered 0 expired files to remove.
2026-10-24 19:35:12.182 [INFO ] Maintenance purge cycle finished successfully.