Tuesday, April 1, 2008

Audit Windows 2003 print server usage

This post provides information on a solution to provide audit logs and summary information on printer usage on a Windows print server. This will provide daily and monthly printer event logs, and summary results based on one or more log files.

Pre-requisites:
  • A scheduled task that runs every day to collect and process the logs, it doesn’t have to be on the print server.
  • 'Log Spooler Information Events' on the printer spooler in question, which will write Event ID 10 entries every time someone prints through the spooler
  • A system event log big enough to capture at least one day's logs

Create a batch file that contains the following commands. Note that you will need to modify the variables or to reference paths as appropriate, eg for dumpel.exe and the VBScript, and the print/log dir.

Set PrintDir=c:\Print
Set LogDir=C:\logs
for /f "tokens=1-8 delims=/:. " %%i in ('echo %date%') do Set DateFlat=%%l%%k%%j
dumpel -s \\%print_server% -l System -e 10 -m Print -d 1 >> %logDir%\%server%_jobs_%DateFlat%.csv
for /f "tokens=3,4 delims=/ " %%i in ('echo %date%') do copy %server%_jobs_%%j%%i??.csv %PrintDir%\PrintJobs_%%j%%i.csv /y
cscript ProcessPrinterLogs.wsf /f:%LogDir%


If you create a scheduled task to run this batch file every day at the same time, these commands will:

  1. Dump the event logs for the past day to a daily log file with YYYYMMDD suffix.
  2. Collate the daily log files into a monthly log file, by appending each daily file. For each day in a month, this command will overwrite the previous monthly log, until it runs on the last day of the month.
  3. The script processes the dumpel log entries, providing different views in the form of per-printer, per-user, per-day totals of jobs/pages/bytes (useful for graphics), as well as summary totals of the information.

This has the following advantages:

  • It’s simple and not very intensive. A SQL database with a recurring DTS job to import the logs and then using SQL Reporting Services would be a lot prettier, but really not that much more functional or useful.
  • You will have a permanent set of log files, one for each month that you can store for historical purposes, while purging the daily log file directory every so often.

The ProcessPrinterLogs.vbs script that does all the work is listed below. To run:
cscript ProcessPrinterLogs.vbs /f:%logDir%



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 = objShell.ExpandEnvironmentStrings("%Temp%")
        sOutputFile = 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 dusers = CreateObject("Scripting.Dictionary")
    Set dDates = CreateObject("Scripting.Dictionary")
    Set dJobs = CreateObject("Scripting.Dictionary")

    For Each sFile in Split(sFileset, vbCRLF)                                ' For Each 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)                ' Process the log entry
        Next
    Next

    Call ProduceOutput(sOutput, dPrinters, dUsers, dDates, dJobs)                        ' 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)
    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

    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)
    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
    Else
        wscript.echo "skipped '" & sLine & "'"
    End If
End Function


* Please don’t print this post :) *

Wayne's World of IT (WWoIT), Copyright 2008 Wayne Martin.

6 comments:

Anonymous said...

Is there any way I can get the client name from which a print job originated?

regards
jan

Brian E. Tower said...

Hey Wayne, great post! I used your scripts as a starting point for my print server auditing needs.

In addition to modifying the batch portion to loop through a list of servers, I've also added a section to the vbs script that outputs a list of the users printing to each printer. I also used this opportunity to learn VBS as I've not worked with it before.

I'm willing to share with you my additions and would like to know how you're using the output to generate reports, charts, etc. Please feel free to contact me at towerbe@gmail.com if you're interested.

bt

Brian E. Tower said...
This comment has been removed by the author.
Brian E. Tower said...

Hey Wayne,

I've gotten a number of emails from people asking me about my version of your script(s). Hope you don't mind if I use this post to direct them to my blog on the subject.

http://pyreanspring.blogspot.com/2008/09/auditing-windows-2003-print-servers.html

On a related note, I also have a post on a script I developed to unmap and remap printers, changing the print server used.

http://pyreanspring.blogspot.com/2008/10/more-fun-with-windows-printers.html

bt

Unknown said...

Hi Wayne,

cool script.

It so happens that I just recently did a powershell version of a print monitor for the same scenario.

It has been uploaded to MS Script repository, check it out

http://gallery.technet.microsoft.com/ScriptCenter/en-us/44ba12ff-8c7f-41ad-8d7f-85421ba4198b

Regards Konráð Hall

mayavi said...

Hi

I am looking for a script to work with windows 2008 print server!

Post a Comment