Raise your hand if you’ve never encountered that mythological, ancient, sworn enemy of digitalization, the unwitting lover of plastic gardens, who infests all offices: the compulsive printer!
The one who prints (almost) all emails to keep them in weathered file cabinet, the one who downloads a 150-page PDF document to have a look at a random chapter but on the screen “struggles to read” and therefore it’s better to print it (all) to read it on paper in positions that the orthopedist appreciates, the one who says “but you never know…” you get the idea!
So, hopefully, the moment arrives when the environmentally conscious Manager or more likely, whoever sees or pays the management costs, finally requests a tool for reporting and monitoring these poor, overused printers (the Amazon rainforest thanks you).
If you’re reading this article, you’ve probably already found some software more or less suitable for the purpose, there’s just one problem, they’re so nice and well-integrated that they have a commercial license to buy (fair enough).
So here’s a solution, in the Windows environment, that we can use as a starting point and then enrich it according to needs, we’ll use:
- a PowerShell script to query client side data
- a SQL database for storage, i.e. MSSQL instance
- a Web Service receiving client data
The script#
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
|
<#
.SYNOPSIS
Enable operational print log and push updates on print activity
.DESCRIPTION
Enable operational print log and push updates on print activity
Exit 0 and log with ID 0 if successfull, 1 if something wrong
.NOTES
Author: Lestoilfante
Email: [email protected]
Date: 18NOV2021
PSVer: 5.0
#>
$serverUrl = "https://mioserver.contoso.local/api/PrintLog"
# Definiamo un nome da usare come Event Source in EventLog di Windows
$eventName = "Contoso_PrintReport"
If ([System.Diagnostics.EventLog]::SourceExists($eventName) -eq $False) {
New-EventLog -LogName Application -Source $eventName
}
$printLog = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration "Microsoft-Windows-PrintService/Operational"
if ($printLog.IsEnabled -eq $False) {
$printLog.IsEnabled = $True
$printLog.MaximumSizeInBytes = 5242880
$printLog.LogMode = Circular
$printLog.SaveChanges()
}
$filterEvtScript = @{
Logname='Application'
ProviderName=$eventName
ID=0
}
$evt = Get-WinEvent -filterHashTable $filterEvtScript -MaxEvents 1 -ErrorAction SilentlyContinue
if ($evt -eq $null){
$evtStartTime = [datetime]::Today.AddDays(-30)
}
else {
$evtStartTime = $evt.TimeCreated
}
$filterEvtPrint = @{
LogName = 'Microsoft-Windows-PrintService/Operational'
ID=307
StartTime = $evtStartTime
}
[xml[]]$xml = Get-WinEvent -filterHashTable $filterEvtPrint -ErrorAction SilentlyContinue | ForEach-Object{ $_.ToXml() }
if ($xml.Count -eq 0) {
Write-EventLog -LogName "Application" -Source $eventName -EventID 0 -EntryType Information -Message "0 events found" -Category 1
exit 0
}
$collectionWithItems = New-Object System.Collections.ArrayList
Foreach ($event in $xml.Event)
{
if ($event.Userdata.DocumentPrinted -ne $null){
$temp = "" | select "UserName", "PageQty", "PrinterName", "Date"
$temp.UserName = $event.Userdata.DocumentPrinted.Param3
$temp.PageQty = $event.Userdata.DocumentPrinted.Param8
$temp.PrinterName = $event.Userdata.DocumentPrinted.Param5
$temp.Date = $event.System.TimeCreated.SystemTime
$collectionWithItems.Add($temp) | Out-Null
}
}
try {
$json = ConvertTo-Json $collectionWithItems
}
catch {
Write-EventLog -LogName "Application" -Source $eventName -EventID 1 -EntryType Information -Message "Error in json conversion" -Category 1
exit 1
}
try
{
$Response = Invoke-WebRequest -Method 'Post' -Uri $serverUrl -Body $json -ContentType "application/json" -TimeoutSec 15 -UseBasicParsing
$StatusCode = $Response.StatusCode
$Message = [String]::new($Response.Content)
Write-EventLog -LogName "Application" -Source $eventName -EventID 0 -EntryType Information -Message "Report sent, response: $Message" -Category 1
exit 0
}
catch
{
$StatusCode = $_.Exception.Response.StatusCode.value__
Write-EventLog -LogName "Application" -Source $eventName -EventID 1 -EntryType Information -Message "Http error $StatusCode" -Category 1
exit 1
}
exit 0
|
The Database#
We want to have a daily summary record for User and Printer to know Who/Where/When/How much.
Let’s make a “PrintLog” table then
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
USE [ContosoDB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[PrintLog](
[Username] [nvarchar](10) NOT NULL,
[Date] [date] NOT NULL,
[Printer] [nvarchar](30) NOT NULL,
[Pages] [int] NOT NULL,
CONSTRAINT [PK_PrintLog] UNIQUE NONCLUSTERED
(
[Username] ASC,
[Date] ASC,
[Printer] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
|
and a Stored Procedure handling data input
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
|
USE [ContosoDB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[printLog_update](
@_Username AS nvarchar(10),
@_Date AS date,
@_Printer AS nvarchar(30),
@_Pages AS int
)
AS
BEGIN
DECLARE @_varPages AS int;
SET @_varPages = (SELECT Pages FROM PrintLog WHERE Username = @_Username and Date = @_Date and Printer = @_Printer);
IF (@_varPages IS NULL)
BEGIN
INSERT INTO PrintLog (Username,Date,Printer,Pages) VALUES (@_Username,@_Date,@_Printer,@_Pages)
END
ELSE
BEGIN
UPDATE
PrintLog
SET
Pages = @_Pages + @_varPages
WHERE
Username = @_Username AND Date=@_Date AND Printer=@_Printer
END
END;
GO
|
The Web Service#
The rule of using what you know best or what is better supported in your infrastructure applies, in this case, an example in .NET.
First, let’s define a model for the data we will receive.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class PrintLogData
{
public class LogCollection
{
public List<LogEntry> Entry { get; set; }
}
public class LogEntry
{
public string UserName { get; set; }
public int PageQty { get; set; }
public string PrinterName { get; set; }
public DateTime Date { get; set; }
}
}
|
So let’s define our minimal Web Service that will call the stored procedure defined on the DB.
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
|
public class PrintLogController : ApiController
{
private readonly string sqlServer = "";
private readonly string sqlDB = "ContosoDB";
private readonly string sqlUser = "";
private readonly string sqlPwd = "";
// POST: api/PrintLog
public string Post([FromBody] ICollection<PrintLogData.LogEntry> json)
{
// Raggruppiamo i dati ricevuti per user/data/stampante sommandone le pagine
var newList = json.GroupBy(x => new { x.UserName, x.Date.Date, x.PrinterName })
.Select(y => new PrintLogData.LogEntry()
{
UserName = y.Key.UserName,
Date = y.Key.Date.Date,
PrinterName = y.Key.PrinterName,
PageQty = y.Sum(c => c.PageQty)
});
int count = newList.Count();
int entriesProcessed = 0;
try
{
using (var sqlCon = new SqlConnection("Data Source=" + sqlServer + ";Initial Catalog=" + sqlDB + "; User ID = " + sqlUser + "; Password = " + sqlPwd))
{
sqlCon.Open();
foreach (var item in newList)
{
SqlCommand sql_cmnd = new SqlCommand("printLog_update", sqlCon);
sql_cmnd.CommandType = CommandType.StoredProcedure;
sql_cmnd.Parameters.AddWithValue("@_Username", SqlDbType.NVarChar).Value = item.UserName;
sql_cmnd.Parameters.AddWithValue("@_Date", SqlDbType.Date).Value = item.Date;
sql_cmnd.Parameters.AddWithValue("@_Printer", SqlDbType.NVarChar).Value = item.PrinterName;
sql_cmnd.Parameters.AddWithValue("@_Pages", SqlDbType.Int).Value = item.PageQty;
entriesProcessed += sql_cmnd.ExecuteNonQuery();
}
sqlCon.Close();
}
}
catch (Exception e)
{
return SendStructResponse(e.Message, HttpStatusCode.BadRequest);
}
return SendStructResponse(entriesProcessed.ToString(), HttpStatusCode.OK);
}
internal T SendStructResponse<T>(T response, HttpStatusCode statusCode = HttpStatusCode.OK)
{
if (statusCode != HttpStatusCode.OK)
{
var r =
new HttpResponseMessage(statusCode)
{
Content = new StringContent(JsonConvert.SerializeObject(response), Encoding.UTF8, "text/plain")
};
throw new HttpResponseException(r);
}
return response;
}
}
|
Let’s put the pieces together#
Now that we have all the components, how do we handle deployment on the clients?
Obviously, it depends on you and the tools you have available. Just keep in mind that the script must run at least the first time with elevated privileges to activate logging correctly.
I would have a completely automatic solution in Windows Domain networks… let’s see if you reach the same solution too!