Introduction to the PowerShell Log Buffer Script
This PowerShell script enhances logging performance by temporarily storing logs in a PowerShell log buffer and periodically writing them to a log file. By minimizing the frequency of disk I/O operations, this script ensures more efficient logging, which is particularly useful in scenarios requiring extensive log generation.
The script effectively manages log data, ensuring system performance remains optimal even with high volumes of logging activity.
Pros:
- Improved performance
- Reduced disk I/O operations
- Thread-safe logging
Cons:
- Slight delay in log availability
Download Link: PowerShell Buffer Script
Detailed Explanation
Initialization
The script starts by setting up a concurrent queue to store log messages temporarily, a timer to periodically trigger the writing of logs, and a lock object to ensure thread safety during the log writing process. It also defines the path for the log file and ensures the directory exists.
BEGIN {
Clear-Host
# Create a buffer to store log messages temporarily
$script:logBuffer = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
# Create a timer with a 5-second interval
$script:timer = New-Object Timers.Timer
$script:timer.Interval = 5000 # 5000 milliseconds = 5 seconds
# Define the log file path
$logDir = "C:\Windows\Logs"
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir -Force
}
$script:Logfile = "$logDir\log_name.log"
# Create a lock object for thread safety
$script:lockObject = New-Object System.Object
}
In this initialization block:
$script:logBuffer
is a concurrent queue used to temporarily store log messages.$script:timer
is set to trigger every 5 seconds to manage log writing.$logDir
ensures the log directory exists, creating it if necessary.$script:lockObject
is used to synchronize access to the log file, ensuring thread safety.
Logging Function: LogWrite
The LogWrite
function is used to add log messages to the buffer. It prepends each log message with a timestamp for accurate tracking.
# Function to add a log message to the buffer
Function LogWrite {
Param ([string]$logstring)
$logstring = (Get-Date -Format "MM-dd-yyyy - HH:mm:ss.fff") + " | $logstring"
# Add the log message to the buffer
$script:logBuffer.Enqueue($logstring)
}
In this function:
Get-Date -Format "MM-dd-yyyy - HH:mm:ss.fff"
generates a timestamp for each log entry.$script:logBuffer.Enqueue($logstring)
adds the timestamped log message to the buffer.
Buffer Writing Function: WriteLogsBuffer
The WriteLogsBuffer
function is responsible for writing the buffered log messages to the log file. It ensures thread safety using a lock.
# Function to write buffered logs to the log file
Function WriteLogsBuffer {
$lockToken = $false
try {
[System.Threading.Monitor]::TryEnter($script:lockObject, [ref]$lockToken) | Out-Null
if ($lockToken) {
# Transfer logs from the buffer to a temporary buffer
$tempBuffer = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
$log = $null
while ($script:logBuffer.TryDequeue([ref]$log)) {
$tempBuffer.Enqueue($log)
}
# If there are logs in the buffer, write them to the log file
if ($tempBuffer.Count -gt 0) {
# Create the log file if it doesn't exist
if (-not (Test-Path $script:Logfile)) {
New-Item -ItemType File -Path $script:Logfile -Force
}
# Write the buffered logs to the log file
$logsToWrite = @()
$log = $null
while ($tempBuffer.TryDequeue([ref]$log)) {
if ($null -ne $log) {
$logsToWrite += $log
}
}
# Convert $logsToWrite to an array of strings
$logsToWrite = $logsToWrite -as [string[]]
[System.IO.File]::AppendAllLines($script:Logfile, $logsToWrite)
}
}
}
catch {
# Display an error message if writing logs fails
Write-Host "Error occurred while writing logs to the log file: $($_.Exception.Message)"
}
finally {
if ($lockToken) {
[System.Threading.Monitor]::Exit($script:lockObject)
}
}
}
In this function:
[System.Threading.Monitor]::TryEnter($script:lockObject, [ref]$lockToken)
attempts to acquire a lock to ensure thread safety.$script:logBuffer.TryDequeue([ref]$log)
moves log messages from the buffer to a temporary buffer.[System.IO.File]::AppendAllLines($script:Logfile, $logsToWrite)
appends the buffered log messages to the log file.
Final Buffer Write Function: WriteRemainingLogsBuffer
The WriteRemainingLogsBuffer
function ensures that any remaining log messages in the buffer are written to the log file when the script ends.
# Function to write remaining buffered logs when the script ends
Function WriteRemainingLogsBuffer {
$lockToken = $false
try {
while (-not [System.Threading.Monitor]::TryEnter($script:lockObject, [ref]$lockToken) | Out-Null) {
Start-Sleep -Milliseconds 100
}
WriteLogsBuffer
}
finally {
if ($lockToken) {
[System.Threading.Monitor]::Exit($script:lockObject)
}
}
}
In this function:
[System.Threading.Monitor]::TryEnter($script:lockObject, [ref]$lockToken)
attempts to acquire a lock to ensure thread safety.WriteLogsBuffer
is called to write any remaining log messages to the log file.Start-Sleep -Milliseconds 100
introduces a short delay if the lock is not immediately available.
This function is called in the END
block of the script, ensuring that all remaining logs are flushed from the buffer and written to the file, guaranteeing no log data is lost even if the script is interrupted.
For a complete view of the script and its implementation, visit the GitHub repository.
Why use Write-Host in favor of the default Write-Output? Also when comparing objects against $null it is more performant to put the $null statement first. The same situation exists for Out-Null if you simply assign your statement to $null it will be much faster.
Thank you for your feedback. I often default to using Write-Host due to habit, but I acknowledge that Write-Output could enhance clarity, particularly in scenarios where outputs need to be captured for testing. Also, your suggestions about optimizing performance by positioning $null on the left in comparisons and preferring direct assignments to $null over using Out-Null are helpful. I will make these adjustments to refine the script.
Is it possible to combine everything in one function. In the case of when main script can’t be update or modify.. the flushing our remaining it has to go to the end of the main script.
Yes, you can put them all in one function. I don’t think it’s complicated to adapt.