When it comes to monitoring, as much I like scripting languages, it is never my first choice to rely only on a ‘custom’ script. Considering how many variables are involved: requirements, environments, the overall experience of the team that will manage that piece of software, and all efforts to develop, test, and support the solution over time.
I prefer the implementation of any monitoring solution with basic out-of-the-box features and add on top some scripts if required to reduce the overall codebase to read/maintain. It is not only an early optimisation of a cost/benefit analysis, but it is derived from my professional experiences in different companies and a consideration of the balance needed between the business goal/expectation and the technical value offered by the suggested solution.
In this article you will find something totally different, I wanted to take the opportunity of helping somebody to solve a real case of a Virtual Printer that was causing issues to users and the ops team. The printer needed to be monitored with a living-off-the-land approach, so without adding any software solution but just a few scripts.
Where the project started
ICTWebSolutions reached out to me. I decided to choose the form of a pro-bono consultation/collaboration for this project to keep the solution open source to let everyone the opportunity of learning, sharing, and contributing back to the project.
Why I’ve accepted it
I love my job and I love to contribute and share my passion. I thought that this small project could also show-case some of the structure or approach I have to solve issues and design code or software solutions.
Professional engagements have different expectations and boundaries that I tend to clarify from the start of any project, I liked the approach of ICTWebSolutions and that is the reason why I accepted.
Scenario and Requirements
An Autodoc HSE Printer (by streamlinesoftware.net) is installed on a window server and the number of printing jobs in the queue can grow during the day to a few thousand jobs.
Frequently the printer is (manually) rebooted to get it to function properly and users submit their print jobs again.
This is the full list of requirements.
Proof of Concept
In the first phases of any project regardless of the methodology (waterfall, agile, etc) is always best to verify if the suggested solution satisfies the requirements.
In this scenario, it was just a matter of a few minutes to verify that I was on the right track and gathering all the required information.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#Paolo Frigo, https://www.scriptinglibrary.com #USER SETTINGS $PrinterName = "AutoDoc HSE" $CriticalThreshold = 8 #REQUIREMENTS $PrinterStatus = (Get-Printer -Name $PrinterName).printerstatus #STRING $NumberOfPrintJobs = 9 #(Get-PrintJob -PrinterName $PrinterName).Count #INT $Critical = $NumberOfPrintJobs -ge $CriticalThreshold #BOOLEAN #Printout Write-Output $PrinterName, $PrinterStatus, $NumberOfPrintJobs, $Critical |
Documentation
All meeting notes and requirements were added to the code repository in a MarkDown format.
Project Structure
I need a clean structure from folders to naming conventions to work on a project from the beginning, it helps me to build up the foundations, to visualise my mental model and more intuitive for others.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
tree monitor-printer monitor-printer ├── README.md ├── config-setting.ps1 ├── doc │ ├── improvements.md │ ├── notes.md │ └── requirements.md ├── lib │ ├── append-log.ps1 │ ├── slack-notification.ps1 │ └── teams-notification.ps1 ├── monitor-printer.ps1 └── monitor-printer.tests.ps1 |
Configuration
I’ve decoupled the configuration from the script, it is a common practice and it helps to save time when you want to find and to tweak one setting.
1 2 3 4 5 6 |
$PrinterName = "AutoDoc HSE" $CriticalThreshold = 8 $NotificationChannelTokens = ("", "")# expects multiple values, comma separated like https://hooks.slack.com/... or https://outlook.office.com/webhook/... $LogFile = "monitor-printer.log" $PrintingJobXMLFile = "last-printing-job.xml" $TimeThresholdPrintingJob = 5 # minutes |
Logging
If a script needs to run unattended it needs to log what is doing. I have an article on this subject.
1 2 3 4 5 6 7 8 |
02/07/2020 11:28:06 - CRITICAL: AutoDoc HSE (state: Paused). Jobs currently in queue: 9 at 07/02/2020 20:28:06 02/07/2020 11:28:07 - NOTIFICATION: Slack message sent 02/07/2020 11:31:08 - CRITICAL: AutoDoc HSE (state: Paused). Jobs currently in queue: 9 at 07/02/2020 20:31:08 02/07/2020 11:31:09 - NOTIFICATION: Slack message sent 02/07/2020 11:31:11 - NOTIFICATION: Teams message sent 20/07/2020 10:17:36 - CRITICAL: AutoDoc HSE (state: Normal). Jobs currently in queue: 6 at 07/20/2020 22:17:36. Job ID 9 still in PRINTING state since 07/20/2020 21:10:02 by User Administrator on MyServer, document name: Test Page - printed pages: 1/1 - size 4584819 20/07/2020 10:17:37 - NOTIFICATION: Slack notification sent 20/07/2020 10:17:39 - NOTIFICATION: Teams message sent |
Unit and Functional Testing with Pester (v.5)
I was not looking for 100% test coverage, which is definitely simple to obtain for 100 lines of code, but I wanted to create few tests to make sure that the requirements were always met and if code changes will be applied running those tests will show if something would break. Limit the number of tests to a small number helped to keep the Pester script simple at the same time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
PS D:\Git\monitor-printer> Invoke-Pester -Output Detailed Starting discovery in 1 files. Discovering in monitor-printer.tests.ps1. Found 4 tests. 7ms Discovery finished in 12ms. Running tests from 'monitor-printer.tests.ps1' Describing DEPENDENCY CHECKS [+] fails if one PS D:\Git\monitor-printer> Invoke-Pester -Output Detailed Starting discovery in 1 files. Discovering in monitor-printer.tests.ps1. Found 4 tests. 7ms Discovery finished in 12ms. Running tests from 'monitor-printer.tests.ps1' Describing DEPENDENCY CHECKS [+] fails if one library is missing 19ms (18ms|1ms) Describing FUNCTIONAL CHECKS [+] creates a log file if does not exists 60ms (59ms|1ms) [+] fails if printername "AutoDoc HSE" is not found 39ms (38ms|1ms) [+] if number of jobs in the queue > the critical threshold (8) trigger the notifications 46ms (46ms|1ms) Tests completed in 254ms Tests Passed: 4, Failed: 0, Skipped: 0 NotRun: 0 is missing 19ms (18ms|1ms) Describing FUNCTIONAL CHECKS [+] creates a log file if does not exists 60ms (59ms|1ms) [+] fails if printername "AutoDoc HSE" is not found 39ms (38ms|1ms) [+] if number of jobs in the queue > the critical threshold (8) trigger the notifications 46ms (46ms|1ms) Tests completed in 254ms Tests Passed: 4, Failed: 0, Skipped: 0 NotRun: 0 |
Testing Environment
After spinning up a Windows Server VM and installing the HSE printer it was the time for some manual testing. The simplest way that I could think of for creating a big queue of print jobs was to pause a printer and put some print test pages. A manual test is always required. QA is extremely important.
Notifications
I wanted to create a “pluggable” approach where it is simple to add new notifications in the future if needed, in case after Slack and MS Teams were not enough. If you want to see some other example of Slack and Teams notifications I have written a couple of articles on this subject.
SLACK
MS TEAMS
Let’s review the code
I have tried to keep the code simple to read and hopefully easy to maintain as well.
Depending on the programming language I’m using and how much familiar I’m with it and confident that I can solve the issue, I’m in general pretty good with estimating how big the source code could be, like in this case my “guestimates” were right from the start.
Checking the dependencies, as usual, is done where it should be at the beginning of the script to fail fast.
The script can run unattended so the exit values in the source code are for helping out with the task scheduler log analysis.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
#requires -module printmanagement # Monitor-Printer V.1.1 #Paolo Frigo, https://www.scriptinglibrary.com #region SET THE WORKING DIRECTORY Set-Location -path $PSScriptRoot #endregion #region CHECK DEPENDENCIES AND IMPORT THEM WITH DOT SOURCING $Dependencies = "\config-setting.ps1", "\lib\append-log.ps1", "\lib\slack-notification.ps1", "\lib\teams-notification.ps1" foreach ($Dependency in $Dependencies){ if ((Test-Path "$($PSScriptRoot)$Dependency") -eq $False){ throw "MISSING DEPENDENCY: File not found `"$($PSScriptRoot)$Dependency`"" } else { . $PSScriptRoot$Dependency } } #endregion # CREATE A LOG FILE IF IS NOT EXISTS if ((Test-Path -Path $LogFile) -eq $false){ New-Item "$LogFile" } #endregion # CREATE A PrintingJobXMLFile FILE IF IS NOT EXISTS if ((Test-Path -Path $PrintingJobXMLFile) -eq $false){ "" | Select-Object computername, printername, jobstatus, id, documentname, pagesprinted, totalpages, size, username, submittedtime,NotePropertyValue | Export-Clixml "$PrintingJobXMLFile" } #endregion # FAIL FAST THE PRINTER NAME IS NOT PRESENT try{ if ($printername -notin $(get-printer|Select-Object -exp Name)){ Append-Log -LogFile $LogFile -Message "$PrinterName not found. please check your user settings" Throw "$PrinterName NOT FOUND. PLEASE CHECK YOUR USER SETTINGS" } } catch { Throw "$PrinterName NOT FOUND. PLEASE CHECK YOUR USER SETTINGS" } #region REQUIREMENTS $PrinterStatus = (Get-Printer -Name $PrinterName).printerstatus #STRING $NumberOfPrintJobs = (Get-PrintJob -PrinterName $PrinterName).Count #INT $LastPrintingJob = Import-Clixml $PrintingJobXMLFile #HASHTABLE #endregion #region Jobs in Printing state $PrintingJob = get-printjob -PrinterName $PrinterName | Where-Object {$_.jobstatus -match "Printing"} | Select-Object computername, printername, jobstatus, id, documentname, pagesprinted, totalpages, size, username, submittedtime | Add-Member -NotePropertyName "printingat" -NotePropertyValue "$(get-date)" if ($PrintingJob){ $LongRunningPrintingJob = ($PrintingJob.id -eq $LastPrintingJob.id) -and ([datetime]$PrintingJob.StillPrintingAt) -lt $(get-date).AddMinutes($TimeThresholdPrintingJob) if ($LongRunningPrintingJob -eq $False){ $PrintingJob | Export-Clixml $PrintingJobXMLFile #update the XML File } } else { #There are no long running printing jobs "" | Select-Object computername, printername, jobstatus, id, documentname, pagesprinted, totalpages, size, username, submittedtime,NotePropertyValue | Export-Clixml "$PrintingJobXMLFile" $LongRunningPrintingJob = $false } #endregion $Critical = ($NumberOfPrintJobs -ge $CriticalThreshold) -or $LongRunningPrintingJob #BOOLEAN #region NOTIFICATION if ($Critical) { $Message = "CRITICAL: $PrinterName (state: $PrinterStatus). Jobs currently in queue: $NumberOfPrintJobs at $(Get-Date)." if ($LongRunningPrintingJob){ $Message += " Job ID $($LastPrintingJob.id) still in PRINTING state since $($LastPrintingJob.StillPrintingAt) by User $($LastPrintingJob.username) on $($LastPrintingJob.computername), document name: $($LastPrintingJob.documentname) - printed pages: $($LastPrintingJob.pagesprinted)/$($LastPrintingJob.totalpages) - size $($LastPrintingJob.size)" } Write-Warning $Message Append-Log -LogFile $LogFile -Message $Message foreach ($ChannelURI in $NotificationChannelTokens){ if ($ChannelURI -like ""){ break } elseif ($ChannelURI -match "hooks.slack.com"){ Slack-Notification -ChannelUri $ChannelURI -Message $Message Append-Log -LogFile $LogFile -Message "NOTIFICATION: Slack notification sent" } elseif ($ChannelURI -match "outlook.office.com"){ Teams-Notification -ChannelUri $ChannelURI -Message $Message Append-Log -LogFile $LogFile -Message "NOTIFICATION: Teams notification sent" } else{ Append-Log -LogFile $LogFile -Message "unrecognized notification uri, please check your user settings" Throw "UNRECOGNIZED NOTIFICATION URI, PLEASE CHECK YOUR USER SETTINGS" } } } #endregion exit 0 |
Code Repository
I’ve decided to create a separate repository for this project: https://github.com/PaoloFrigo/monitor-printer
Wrap-Up
Using PowerShell as a monitoring solution for your printer is relatively simple, but this project could be also a good starting point for starting your own one and use it as boilerplate.
I hope you will find it useful and you will clone, fork, and share this project.
2 Replies to “How to monitor a printer with Powershell”