Skip to content

PowerShell Logging

How to implement PowerShell logging using transcripts, Windows Event Logs, Group Policy auditing, and custom structured loggers for auditing, troubleshooting, and compliance.

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.

record-session.ps1powershell
$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-Transcript

where:

  • -Path defines the target output file path.
  • -Append adds new transcript entries to the existing file rather than overwriting it.
  • Stop-Transcript finalizes the transcript block and logs execution duration.

Execute the script from the console:

Consolepowershell
pwsh ./record-session.ps1

and the output file contains:

execution-transcript.logplaintext
**********************
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:

Consolepowershell
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:

log-windows-event.ps1powershell
$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:

  • LogName specifies the target log container (typically Application).
  • Source matches the pre-registered application identifier.
  • EventId provides an integer identifier used by monitoring systems (SIEMs) for indexing.
  • EntryType assigns severity: Information, Warning, or Error.
  • @EventParams utilizes parameter splatting to pass the arguments cleanly.
Consolepowershell
pwsh ./log-windows-event.ps1
Outputplaintext
Event successfully recorded in Application log.

To verify the event was logged:

Consolepowershell
Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = 'StorageAuditor' } -MaxEvents 1 | 
    Select-Object TimeCreated, Id, LevelDisplayName, Message
Outputplaintext
TimeCreated           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:

  1. 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.*.
  2. 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.
  3. 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.
Consolepowershell
Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -ErrorAction SilentlyContinue
Outputplaintext
EnableScriptBlockLogging : 1

where 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:

Write-Log.ps1powershell
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-Content safely appends the log line to disk using UTF-8 encoding.
  • The switch block 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:

run-maintenance-cycle.ps1powershell
. "$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-Log is invoked across multiple severity levels (INFO, WARN, DEBUG, ERROR).
  • try/catch intercepts runtime exceptions and records the failure message before exiting.

Execute the script from the console:

Consolepowershell
pwsh ./run-maintenance-cycle.ps1
Outputplaintext
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.

Inspect the persistent file created on disk:

Consolepowershell
Get-Content -Path "$PSScriptRoot\application.log"
Outputplaintext
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.
Navigation