The ramblings of an old IT Pro travelling the digital byways.

Tuesday, September 9, 2008

Auditing Windows 2003 Print Servers

As many of you know I lead a team that manages a large number of Windows servers - along with other stuff - for a large corporation. One of the most dreaded things we deal with are print servers. In general, they're just a pain, but I digress.

A while back I was trying to come up with a way to audit the print servers to determine who was using each queue, how many pages were being printed by each queue, which queues were actually being used, etc. Much to my surprise, there were no programs or tools available that do what I want. Not even from Uncle Bill's big blue company. Not even Open Source. Then I found this really good blog with a script that did about 90 percent of what I need, written by a gentleman in Brisbane Australia named Wayne. For a full explanation of how to use these scripts, please see Wayne's blog.



To make a long story short, I took Wayne's script and in the spirit of open source modified it to do what I needed it to do. Now I'm returning the script to the "wild" and hoping some other lucky slob finds it useful.

So, here's my version of the ProcessPrinterLogs.vbs script originally posted by Wayne.

---

Const ForReading = 1, ForWriting = 2, ForAppending = 8

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")

Main()


Sub Main()
If WScript.Arguments.Named.Exists("f") Then
sSource = Wscript.Arguments.Named("f")
Else
Wscript.Arguments.ShowUsage()
Wscript.Echo "Source file or directory must be supplied"
Wscript.Quit(2)
End If

If Wscript.Arguments.Named.Exists("o") Then
sOutputFile = Wscript.Arguments.Named("o")
Else
dNow = Now
dLogDate = DatePart("yyyy", dNow)
dLogDate = dLogDate & String(2 - Len(DatePart("m", dNow)),"0") & DatePart("m", dNow)
dLogDate = dLogDate & String(2 - Len(DatePart("d", dNow)),"0") & DatePart("d", dNow)
sOutputFile = Left(WScript.ScriptName, InStrRev(WScript.ScriptName,".vbs")-1) & "_" & dLogDate & ".csv"
End If

wscript.echo "Input file/dir: '" & sSource & "'"
wscript.echo "Output file: '" & sOutputFile & "'"


If objFSO.FileExists(sSource) Then
sFileSet = sSource ' Process a single file
wscript.echo "Single file specified - " & sFileSet
ElseIf objFSO.FolderExists(sSource) Then
wscript.echo "Source specified was a directory, reading files from '" & sSource & "'"
sFileSet = ""
Set oFolder = objFSO.GetFolder(sSource) ' Get the folder
Set oFiles = oFolder.Files
For Each oFile in oFiles ' For each file
sFileset = sFileset & vbCRLF & oFile.Path ' Append to the fileset
Next
If Len(sFileSet) > Len(vbCRLF) Then sFileSet = Right(sFileSet, Len(sFileSet) - Len(vbCRLF)) ' Trim the leading CRLF
End If

Set dPrinters = CreateObject("Scripting.Dictionary") ' Create the dictionary objects
Set dUsersPerPrinter = CreateObject("Scripting.Dictionary")
Set dusers = CreateObject("Scripting.Dictionary")
Set dDates = CreateObject("Scripting.Dictionary")
Set dJobs = CreateObject("Scripting.Dictionary")

For Each sFile in Split(sFileset, vbCRLF) ' For Each file

Set objFile = objFSO.GetFile(sFile)
If objFile.size > 0 then ' Don't process the file if it is a 0 byte file
wscript.echo "Processing '" & sFile & "'"
sBuffer = ""
Set objTextStream = objFSO.OpenTextFile(sFile, ForReading)
sBuffer = objTextStream.ReadAll

For Each sLine in Split(sBuffer, vbCRLF) ' For each line in this file
Call ProcessLogEntry(sLine, dPrinters, dUsers, dDates, dJobs, dUsersPerPrinter) ' Process the log entry
Next
End If
Next


Call ProduceOutput(sOutput, dPrinters, dUsers, dDates, dJobs, dUsersPerPrinter) ' Produce the output
Set objTextStream = objFSO.OpenTextFile(sOutputFile, ForWriting, True)
objTextStream.Write sOutput
wscript.echo "Output saved to '" & sOutputFile & "', " & Len(sOutput) & " characters."

End Sub

Function ProduceOutput(ByRef sOutput, ByRef dPrinters, ByRef dUsers, ByRef dDates, ByRef dJobs, ByRef dUsersPerPrinter)
Dim strPrinter, strPort, dtmDate, strUser, strserver, strDocumentName, intSize, intPages, strInformation, strTotal
Dim strUserTotal, strPrinterTotal, strDateTotal, strJobTotal, aJobTotal

sOutput = ""
For Each strPrinter in dPrinters.Keys
sOutput = sOutput & vbCRLF & strPrinter & "," & dPrinters.Item(strPrinter)
Next

sOutput = sOutput & vbCRLF
For Each strUser in dUsers.Keys
sOutput = sOutput & vbCRLF & strUser & "," & dUsers.Item(strUser)
Next

'new portion to output list of users per print queue
sOutPut = sOutput & vbCRLF
For Each strPrinter in dUsersPerPrinter.Keys
sOutput = sOutput & vbCRLF & strPrinter & "," & dUsersPerPrinter.Item(strPrinter)
Next
'end of new output

sOutput = sOutput & vbCRLF
For Each dtmDate in dDates.Keys
sOutput = sOutput & vbCRLF & dtmDate & "," & dDates.Item(dtmDate)
Next

sOutput = sOutput & vbCRLF
For Each strTotal in dJobs.Keys
strJobTotal = dJobs.Item(strTotal)
aJobTotal = Split(strJobTotal, ",")
sOutput = sOutput & vbCRLF & "Total Jobs," & aJobTotal(0)
sOutput = sOutput & vbCRLF & "Total Pages," & aJobTotal(1)
sOutput = sOutput & vbCRLF & "Total Size (MB)," & aJobTotal(2)
Next

sOutput = sOutput & vbCRLF
strUserTotal = UBound(dUsers.Keys)+1
strPrinterTotal = UBound(dPrinters.Keys)+1
strDateTotal = UBound(dDates.Keys)+1
sOutput = sOutput & vbCRLF & "Printers," & strPrinterTotal
sOutput = sOutput & vbCRLF & "Users," & strUserTotal
sOutput = sOutput & vbCRLF & "Days," & strDateTotal

aJobTotal = Split(strJobTotal, ",")
sOutput = sOutput & vbCRLF

sOutput = sOutput & vbCRLF & "Average jobs/person," & CInt(aJobTotal(0)/strUserTotal)
sOutput = sOutput & vbCRLF & "Average pages/person," & CInt(aJobTotal(1)/strUserTotal)
sOutput = sOutput & vbCRLF & "Average pages/person/day," & CInt(CInt(aJobTotal(1)/strUserTotal) / strDateTotal)
sOutput = sOutput & vbCRLF & "Average pages/minute," & CInt(aJobTotal(1) / (strDateTotal * 8 * 60))

End Function

Function ProcessLogEntry(ByRef sLine, ByRef dPrinters, ByRef dUsers, ByRef dDates, ByRef dJobs, ByRef dUsersPerPrinter)
Dim strPrinter, strPort, dtmDate, strUser, strserver, strDocumentName, intSize, intPages, strInformation
Dim aPrintJob, intOffset, strTemp, aTemp

aPrintJob = Split(sLine, vbTAB)


If UBound(aPrintJob) = 9 Then
dtmDate = aPrintJob(0) ' & " " & aPrintJob(1)
aTemp = Split(dtmDate, "/")
dtmDate = Right("00" & Trim(aTemp(1)), 2) & "/" & Right("00" & Trim(aTemp(0)), 2) & "/" & aTemp(2) ' Trim, pad and switch to dd/mm/yyyy instead of mm/dd/yyyy
strServer = aPrintJob(8)

strInformation = Trim(aPrintJob(9))
strInformation = Right(strInformation, Len(strInformation) - InStr(strInformation, " ")) ' Remove the job ID
intOffset = InStrRev(strInformation, " ")
intPages = Right(strInformation, Len(strInformation) - intOffset) ' Extract the number of pages from the end
strInformation = Left(strInformation, intOffset-1) ' Trim the string

intOffset = InStrRev(strInformation, " ")
intSize = Right(strInformation, Len(strInformation) - intOffset) ' Extract the number of bytes from the end
strInformation = Left(strInformation, intOffset-1) ' Trim the string

intOffset = InStrRev(strInformation, " ")
strPort = Right(strInformation, Len(strInformation) - intOffset) ' Extract the port from the end
strInformation = Left(strInformation, intOffset-1) ' Trim the string

intOffset = InStrRev(strInformation, " ")
strPrinter = Right(strInformation, Len(strInformation) - intOffset) ' Extract the printer from the end
strInformation = Left(strInformation, intOffset-1) ' Trim the string

intOffset = InStrRev(strInformation, " ")
strUser = Right(strInformation, Len(strInformation) - intOffset) ' Extract the user from the end
strInformation = Left(strInformation, intOffset-1) ' Trim the string

strDocumentName = strInformation

If dPrinters.Exists(strPrinter) Then ' Does this printer already exist in the dictionary?
aTemp = Split(dPrinters.Item(strPrinter), ",") ' Find the existing printer job/page count
aTemp(0) = aTemp(0) + 1 ' Increment the job count
aTemp(1) = aTemp(1) + CInt(intPages) ' Add to the page count
aTemp(2) = aTemp(2) + CInt(intSize/1024/1024) ' Add to the byte count
dPrinters.Item(strPrinter) = Join(aTemp, ",") ' Update the dictionary
Else
aTemp = Array(1, intPages, CInt(intsize /1024/1024)) ' Start the job/page count
dPrinters.Add strPrinter, Join(aTemp, ",") ' Create this item
End If

If dUsers.Exists(strUser) Then ' Does this user already exist in the dictionary?
aTemp = Split(dUsers.Item(strUser), ",") ' Find the existing user job/page count
aTemp(0) = aTemp(0) + 1 ' Increment the job count
aTemp(1) = aTemp(1) + CInt(intPages) ' Add to the page count
aTemp(2) = aTemp(2) + CInt(intSize/1024/1024) ' Add to the byte count
dUsers.Item(strUser) = Join(aTemp, ",") ' Update the dictionary
Else
aTemp = Array(1, intPages, CInt(intsize /1024/1024)) ' Start the job/page count
dUsers.Add strUser, Join(aTemp, ",") ' Create this item
End If

If dDates.Exists(dtmDate) Then ' Does this date already exist in the dictionary?
aTemp = Split(dDates.Item(dtmDate), ",") ' Find the existing date job/page count
aTemp(0) = aTemp(0) + 1 ' Increment the job count
aTemp(1) = aTemp(1) + CInt(intPages) ' Add to the page count
aTemp(2) = aTemp(2) + CInt(intSize/1024/1024) ' Add to the byte count
dDates.Item(dtmDate) = Join(aTemp, ",") ' Update the dictionary
Else
aTemp = Array(1, intPages, CInt(intsize /1024/1024)) ' Start the job/page count
dDates.Add dtmDate, Join(aTemp, ",") ' Create this item
End If

If dJobs.Exists(JOB_TOTAL) Then ' Does the total already exist in the dictionary?
aTemp = Split(dJobs.Item(JOB_TOTAL), ",") ' Find the existing total counts
aTemp(0) = aTemp(0) + 1 ' Increment the job count
aTemp(1) = aTemp(1) + CInt(intPages) ' Add to the page count
aTemp(2) = aTemp(2) + CInt(intSize/1024/1024) ' Add to the byte count
dJobs.Item(JOB_TOTAL) = Join(aTemp, ",") ' Update the dictionary
Else
aTemp = Array(1, intPages, CInt(intsize /1024/1024)) ' Start the job/page count
dJobs.Add JOB_TOTAL, Join(aTemp, ",") ' Create this item
End If

' This section creates a list of users that are using each print queue
If dUsersPerPrinter.Exists(strPrinter) Then ' Does the printer exist as a key in the dictionary?
dim bTemp
bTemp = dUsersPerPrinter.Item(strPrinter) & "," & strUser ' build up the list of users
dUsersPerPrinter.Item(strPrinter) = DedupeString(bTemp,",") ' dedupe sting of users and populate dictionary
Else
dUsersPerPrinter.Add strPrinter, strUser ' Create this item
End If
' End of user list creation

Else
wscript.echo "skipped '" & sLine & "'" ' line skipped because number of elemnts in array not equal to 9 ( need to figure this one out)
End If
End Function


' Deduping a string
' must use this function so we end up with a unique list of users with no duplicates
Function DedupeString(inString,strSeperate)
Dim vObjects, myDict, index, strFinal
strFinal = ""
Set myDict = CreateObject("Scripting.Dictionary")
vObjects = split(inString,strSeperate)
for index = 0 to UBound(vObjects)
if ( not myDict.Exists(vObjects(index)) ) then
myDict.Add vObjects(index), vObjects(index)
if (Len(strFinal)) > 0 Then
strFinal = strFinal & strSeperate & myDict(vObjects(index))
else
strFinal = myDict(vObjects(index))
end if
end if
next
DedupeString = strFinal
End Function


---

The other big change I made was in the wrapper batch file that pulls down the log files. I changed it so that a single script loops through a list of print servers.

Here's my version of the auditprinters.bat script.

---


:: bet_auditprint.bat
:: batch wrapper script to collect relevant event log entries from multiple print servers and then process these logs with ProcessPrinterLogs.vbs

for %%S in (Server1, Server2, Server3) do (

Set print_server=%%S
CALL :s_get_logs

)
GOTO eof

:s_get_logs
set MainDir=d:\BETPrinters\%print_server%
Set PrintDir=%MainDir%\printdir
Set LogDir=%MainDir%\logs

:: Delete log files older than 30 days
forfiles /p %LogDir% /d -30 /c "CMD /C del @FILE"

:: Create needed directories
if not exist %PrintDir% md %PrintDir%
if not exist %LogDir% md %LogDir%

:: Set date format
for /f "tokens=1-8 delims=/:. " %%i in ('echo %date%') do Set DateFlat=%%j%%k%%l

:: Set log an backup files
Set LogFile=%print_server%_jobs_%DateFlat%.csv
Set BackFile=PrintJobs_%DateFlat%.csv

:: Dump the printer log
:: Using full path for safety
D:\SystemApps32\ResourceKit\dumpel -s \\%print_server% -l System -e 10 -m Print -d 1 >> %logDir%\%LogFile%

:: Make a backup copy
copy %logDir%\%print_server%_jobs_%DateFlat%.csv %PrintDir%\%BackFile% /y

:: Process the logs
cscript ProcessPrinterLogs.vbs /f:%LogDir% /o:%MainDir%\%print_server%_auditoutput.csv
:eof

---

Okay, so there it is. Enjoy! Use it in good health and for non nefarious purposes!

3 comments:

Chris said...

Just wanted to let you know that the script works great. My only request would be a way to have it do a detailed report as well that can break down each printer and have user and filename printed for each job.

This would help to audit color printers especially, knowing exactly who printed that 300 page color print job.

Brian E. Tower said...

Thanks for the comment and feedback Chris. Actually, all of the detail that you're looking for should be in the log files that the script creates. As I've moved on to a completely different technology area now, I don't have much time to work on this type of stuff anymore, but maybe someone else can pick up the torch and carry it forward as I did. Gotta thank Wayne for the original script!

Pablo said...

Hi guys,
Today met the need to do audit of printers as you did. I have found your developments and will check if them fit my purposes tomorrow, thank you very much for them. But I have noticed that in your solution you make a dump of Windows spooler log, and as I saw this log doesn't provide enough info about print job, e.g. count of printed pages is logged but the number of copies isn't. It means that if someone print 10 pages 10 times you won't see that in the log, but you will find that amount of paper and paint decreases quicker than you expect. Anyway thank you for your job, well done.
Anyway thank you for your job.

About Me

My photo
A living, breathing contradiction.