Для аудита сервера печати можно использовать разные средства, как платные и бесплатные. Бесплатных, удовлетворяющих моим требованиям, я так и не нашёл. Мои требование просты: кто, когда, сколько и на каком принтере. Лишнего мне не надо. Но в любом случае, дополнительное программное обеспечение необходимо устанавливать на сервере, а в некоторых случаях и агенты на клиентские компьютеры. Как следствие - дополнительные процессы, и нагрузка. Именно по этому, я решил найти вариант штатными средствами.

 Я никак не рашелся серьёзно занятся этим делом пока однажды на одном торренте (не скажу в каком разделе) был скачен журнал "Системный администратор. Июнь. 2012". Там и была статья посвящённая аудиту печати на Windows Server 2008 R2. Однако при выполнении начальных действий по статье не привели к результату. Я специально выделил R2. Т.к. в этой ОС логи печати включаются не так как описано в статье и собираются не в журнале SYSTEM. Однако, возможно автор писал статью с сервера без SP1. Т.к. у меня нет под рукой 2008 или 2008 R2 без SP1 - проверить не смогу. Но, спасибо автору - направление указал. Кому интересна статья вот ссылка.

 Уже изначально я решил что буду записывать логи в базу даных, а потом красиво показывать через веб-интерфейс. У меня есть виртуальные машины на Debian (Cacti, Nagios) по этому ещё одна малюсенькая база данных там не помешает. Для записи из PowerShell в MySQL нам понадобится коннектор MySQL Connector Net. Но позже, я попробую реализовать тоже самое для MS SQL Express. Начнём по порядку. По умолчанию логи печати отключены. Для того, чтобы их включить необходимо раскрыть "Просмотр Журналов":

 В свойствах журнала Operational включается логирование почты. На рисунке у меня уже включено логирование. Так как данные будут записываться в базу данных mysql, то дополнительной настройки для журнала не потребуется. Но если вам нужно, в свойствах вы можете указать размер журнала, действия в случае достижения максимального размера. Так же, возможно, потребуется настройка принтера. Но для перед этим нужно несколько раз распечатать документ с количеством копий больше чем одна. Если в событии 805 при количестве копий больше чем одина, всё равно будет "1", то необходимо обновить драйвера принтера, а, если не поможет, отключить рендеринг на стороне клиента. К сожалению, для некоторых приложений может не помочь. У меня количество копий не работало для Word 2010, но при этом работало для Excel 2010 из тогоже дистрибутива.

 Для статистики нам нужно событие с номером 307 и событие 805. Про 805 я узнал спустя почти год после написания данной статьи. Узнал случайно, когда на форуме Oszone спросили про аудит печати скриптом с MSDN. Оказалось, что параметр $Pages = $event.Event.UserData.DocumentPrinted.Param8 содержит количество страниц только для одной копии. А количество копий как раз можно узнать из лога 805. Логи из журнала обрабатываются с помощью PowerShell v3. С журнале СА рекомендуется использовать команду: get-eventlog. Однако, она умеет анализировать только Windows Logs и не работает напрямую с ID лога:

PS C:\Users\admin> Get-EventLog -List
  Max(K) Retain OverflowAction        Entries Log
  ------ ------ --------------        ------- ---
     512      7 OverwriteOlder            334 Active Directory Web Services
  20 480      0 OverwriteAsNeeded      16 831 Application
  15 168      0 OverwriteAsNeeded       1 075 DFS Replication
     512      0 OverwriteAsNeeded       2 621 Directory Service
  16 384      0 OverwriteAsNeeded         513 DNS Server
   8 192      0 OverwriteAsNeeded           0 Doctor Web
  20 480      0 OverwriteAsNeeded           0 HardwareEvents
     512      7 OverwriteOlder              0 Internet Explorer
  20 480      0 OverwriteAsNeeded           0 Key Management Service
                                              Security
  20 480      0 OverwriteAsNeeded      54 581 System
  15 360      0 OverwriteAsNeeded           9 Windows PowerShell

 Для анализа других логов используется Get-WinEvent, который имеет ключ -ComputerName для подключения к удалённому компьютеру. Но я буду собирать логи с локального компьютера. Для начала назначим права для пользователя, который будет подключаться с сервера печати с IP адресом XXX.XXX.XXX.XXX. Пароль не должен содержать знак $. Выполнить на сервере MySQL:

mysql -h localhost -u root -p
Enter password:

mysql> CREATE USER 'root'@'XXX.XXX.XXX.XXX' IDENTIFIED BY 'asdASD';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'XXX.XXX.XXX.XXX' WITH GRANT OPTION;
Query OK, 0 rows affected (0.00 sec)

 Разрешаем выполнение скриптов PS1 на сервере печати:

Set-ExecutionPolicy RemoteSigned

 После этого создаём базу данных из PowerShell, где yyy.yyy.yyy.yyy IP адрес mysql сервера:

Add-Type -Path "C:\Program Files (x86)\MySQL\MySQL Connector Net 6.9.9\Assemblies\v4.5\MySql.Data.dll" 
$connectionString = "server=yyy.yyy.yyy.yyy;uid=root;pwd=asdASD;"
$connection = New-Object MySql.Data.MySqlClient.MySqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$sql = New-Object MySql.Data.MySqlClient.MySqlCommand
$sql.Connection = $connection
$sql.CommandText = "CREATE DATABASE PRINT CHARACTER SET utf8 COLLATE utf8_general_ci;"
$sql.ExecuteNonQuery()
$sql.CommandText = "CREATE TABLE PRINT.LOGS (
`id` INT(11) NOT NULL,
`Day` DATE NOT NULL,
`Time` TIME NOT NULL,
`User` VARCHAR(30) NOT NULL,
`Printer` VARCHAR(21) NOT NULL,
`Port` VARCHAR(15) NOT NULL,
`Document` VARCHAR(255) NOT NULL,
`Pages` INT NOT NULL,
`Copy` INT NOT NULL,
`Size` INT NOT NULL);"
$sql.ExecuteNonQuery()
$sql.CommandText = "SHOW COLUMNS FROM LOGS FROM PRINT;"
$Table=New-Object Data.DataTable
$Table.Load($sql.ExecuteReader())
$Table | ft
$connection.Close()

 Вывод:

PS C:\> C:\scripts\mysql1.ps1
1
0
Field	 Type           Null Key Default Extra                            
-----    ----           ---- --- ------- -----                            
id       int(11)        NO
Day      datetime       NO
Time     datetime       NO
User     varchar(30)    NO
Printer  varchar(20)    NO
Port     varchar(15)    NO
Document varchar(255)	NO
Pages    int(11)        NO
Copy     int(11)        NO
Size	 int(11)        NO

 Далее, я приведу готовый скрипт для анализа логов и разберу его части. 9,458 заданий с 4-х принтеров за год были обработаны им за 7 минут на сервере c Intel E31230 3,2 GHz.

Add-Type -Path "C:\Program Files (x86)\MySQL\MySQL Connector Net 6.9.9\Assemblies\v4.5\MySql.Data.dll" 
$connectionString = "server=doc;uid=root;pwd=asdasd;database=allprint;"
$connection = New-Object MySql.Data.MySqlClient.MySqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$sql = New-Object MySql.Data.MySqlClient.MySqlCommand
$sql.Connection = $connection
   $today = get-date -DisplayHint date -UFormat %Y-%m-%d
   Get-WinEvent -FilterHashTable @{LogName="Microsoft-Windows-PrintService/Operational";starttime="$today";id=307} | Foreach {
   $event = [xml]$_.ToXml()
      if($event)
      {
      $Time = Get-Date $_.TimeCreated -UFormat "%Y-%m-%d %H:%M:%S"
      $Job = $event.Event.UserData.DocumentPrinted.Param1
      $Document = $event.Event.UserData.DocumentPrinted.Param2.ToString().Replace("\","\\")
      $User = $event.Event.UserData.DocumentPrinted.Param3
      $Port = $event.Event.UserData.DocumentPrinted.Param6
      $Printer = $event.Event.UserData.DocumentPrinted.Param5
      $Size = $event.Event.UserData.DocumentPrinted.Param7
      $Pages = $event.Event.UserData.DocumentPrinted.Param8
      $sql.CommandText = "INSERT INTO alllog (User,Printer,Port,Time,Document,Pages,Size,Job) VALUES ('$User','$Printer','$Port','$Time','$Document','$Pages','$Size','$Job')"
   $sql.ExecuteNonQuery()
   }
}
   Get-WinEvent -FilterHashTable @{LogName="Microsoft-Windows-PrintService/Operational";starttime="$today";id=805} | Foreach {
   $event = [xml]$_.ToXml()
      if($event)
      {
      $Time = Get-Date $_.TimeCreated -UFormat "%Y-%m-%d %H:%M:%S"
      $Copy = $event.Event.UserData.RenderJobDiag.Copies
      $sql.CommandText = "UPDATE alllog set Copy=$Copy WHERE Time='$Time'"
   $sql.ExecuteNonQuery()
   }
}
$connection.Close() 

 А теперь разбор скрипта. Некоторые поля я не смогу объяснить...
Add-Type -Path   добавляет путь к библиотеке коннектора к MySQL. Необходим при старте каждой сессии. Путь зависит от версии коннетора. Будьте внимательны.
$connectionString =   здесь определяем имя сервера с MySQL, пользователя и его пароль для подключения к базе. Базу тут же указываем.
$connection = 
$connection.ConnectionString =
  вот эти две строчки я не знаю для чего :)
$connection.Open()   наверное открывает сессию...
$sql = New-Object MySql.Data.MySqlClient.MySqlCommand
$sql.Connection = $connection
  вот эти две строчки я не знаю для чего :)
$today = get-date -DisplayHint date -UFormat %d.%m.%y   присваиваем переменной $today значение текущего дня. Другие даты нас не будут интересовать,так как скрипт запускается каждую ночь и прошлые данные уже в базе.
Get-WinEvent  команда анализирует логи расположенные по определённому пути, начиная со времени текущего дня (если вам нужно загрузить все логи, то starttime="$today"; можно убрать), только логи с ID 307 или ID 805 и передаёт вывод команде Foreach, которая будет работать циклически.
$Time, $Job, $Document - это та информация, которая нам нужна. Вы можете назвать эти переменные своими именами.
$event.Event.UserData.DocumentPrinted.Param6   эти все строчки опишет одна картинка:

 

$_.TimeCreated    найдёте, если раскроете +System
$Time = Get-Date $_.TimeCreated -UFormat "%Y-%m-%d %H:%M:%S" меняем формат выводимого времени потому, что mysql не воспринимает формат выводимый $_.TimeCreated. Только если таблица имеет тип varchar. Но это не удобно при работе с php. К счастью, get-date работает не только с системным временем.
$sql.CommandText =   выполнение команды MySQL
"INSERT INTO alllog (User,Printer,Port,Time,Document,Pages,Size,Job) VALUES ('$User','$Printer','$Port','$Time','$Document','$Pages','$Size','$Job')"    команда для ввода (но не сам ввод) в таблицу alllog соотвествующих данных.
"UPDATE alllog set Copy=$Copy WHERE Time='$Time'"   команда для обновления базы. Записывает значение Copy (количество копий) в поле, у которого имеющееся время Time совпадает со временем из события 805 $Time. Вначале, я хотел делать соответствие с полем Job, но оказалось, что Job сбрасывается в ноль, после достижения значения 255. В этом случае, UPDATE перезаписал бы все значения, чьи задания имели одинаковое значение.
$sql.ExecuteNonQuery()   выполняет предыдущую команду, т.е. вводит информацию в базу.
$connection.Close()   закрывает сессию.

Далее простой скрипт на php. Позже будет более красивый. Этот просто выводит суммарную информацию:

 

<?php
// Подключаемся к серверу MySQL
$hostname = '192.168.10.17';
$username = 'root';
$password = 'asdasd';

$db = mysql_connect($hostname, $username, $password)
    or die('connect to database failed');
 
// Назначаем кодировку базы
mysql_set_charset('utf8');
 
// выбираем базу
mysql_select_db('allprint')
    or die('db not found');

// Запрашиваем нужную информацию и результат назначаем переменной $result
$query = 'SELECT User,Printer,SUM(Pages) FROM `alllog` GROUP BY User,Printer';
$result = mysql_query($query) or die(mysql_error() ."<br/>". $query);

// Строим таблицу (информация только по пользователям, принтерам, количеству страниц)
        $table = "<table border=0 width=25% align=center>\n";
        while ($row = mysql_fetch_assoc($result))
{
        $table .= "<tr>\n";
        $table .= "<td>".$row['User']."</td>\n";
        $table .= "<td>".$row['Printer']."</td>\n";
        $table .= "<td>".$row['SUM(Pages)']."</td>\n";
        $table .= "</tr>\n";
        }
$table .= "</table>\n";
echo $table;
mysql_close($db);
?>

 Жирным я выделил строчку, которая и определяет выборку содержимого. Вся соль в ней, так как код таблицы можно легко найти в интернете. В приведённом коде выборка идёт за все периоды. А вот строчка, которая показывает за вчерашний день (т.к. за сегодняшний данные загрузятся ночью):

$query = 'SELECT User,Printer,SUM(Pages) FROM `alllog` WHERE Time LIKE "'.date('m', strtotime("now -1 day")).'/'.date('d', strtotime("now -1 day")).'/'.date('Y', strtotime("now -1 day")).'%" GROUP BY User,Printer';

 Если убрать значения strtotime("now -1 day") , то получим данные за сегодняшний день. Всё готово. Помещаем этот скрипт на ваш веб-сервер и получаем простую информацию в вашем браузере о том кто, какой принтер и сколько. Скоро будет - когда :) всё-таки я не программист php.

 
 Благодарю Kazun и Sergeiis за помощь со скритпами PowerShell и PHP.
 Так же благодарю miksoft за помощь с UPDATE - всё гениальное просто!
 Обнаружен баг при печати из некоторых приложений с ОС 2008 R2, и увы, с ОС 2012 R2. При печати из Word 2010\2013, IE 11 - в событии 805 не регистрируются количество копий. Всегда указана одна копия. Так же, начиная с Server 2012 поле "Имя документа" скрыто, т.е. всегда вместо названия документа пишется Document №х, где х - порядковый номер задачи печати. Тесты проводились с универсальными драйверами для Windows Server 2012 R2.
 Обнаружен баг при печати из OS X - количество страниц всегда 0........

 

Первая редакция (без ID 805) - Июль 2012

Вторая редакция (c ID 805) - Июнь  2013