У нашего заказчика из финансовой сферы были два домена, несколько тысяч терминальных пользователей, пара тысяч принтеров, десять принт-серверов, а также гигабайты логов печати. Именно поэтому неудивительно, что у него появилась необходимость создать сервис, который сможет обрабатывать логи со всех принт-серверов и выгружать информацию в подходящем виде (по определенным требованиям).
Сначала это выглядело как разовая задача: есть excel-файл со списком «табельный номер — имя пользователя», необходимо собрать логи и предоставить суммарную статистику по печати за месяц с разбивкой по городам. Однако, когда приступили к реализации, поняли, что все не так-то просто, как казалось и начали думать над отдельным сервером и сайтом. Взяли .NET 6, Blazor, немного PowerShell, WMI и sqlite. Что же получилось из этого «винегрета»? Читайте ниже.
Анализ
Нужные нам логи находились по адресу: Event Viewer/Applications and Services Logs/Microsoft/Windows/PrintService/Operational (Просмотр событий/Журналы приложений и служб/Microsoft/Windows/PrintService/Работает). Но нас интересовали не все события, а только с id 307 — именно там содержатся полезные сведения о напечатанном документе:
Номер документа (обычный счётчик, периодически обнуляется).
Имя документа (включается политикой Computer Configuration/Administrative Templates/Printers/Allow job name in event logs — Enabled. Или Конфигурация компьютера/Административные шаблоны/Принтеры/Разрешить указывать имя задания в журналах событий — Включено).
Логин учётной записи, с которой было отправлено задание на печать.
Имя компьютера, с которого было отправлено задание.
Shared-имя принтера, на который было отправлено задание.
Порт принтера.
Исходный размер документа в байтах (не путать с размером задания на печать).
Количество напечатанных страниц (не путать с количеством страниц в документе: при печати нескольких копий или диапазона страниц будет указано фактическое количество отпечатков. А двусторонняя печать считается за два отпечатка).
SID пользователя (единственное, что неизменно в профиле пользователя).
Имя принт-сервера.
EventRecordID (уникальный id записи в журнале события, инкрементируется (увеличивается) на 1 для каждой следующей).
Дата и время записи в журнале.
Там есть и другие сведения, но нам они не требовались.
Задачу разбивки по городам существенно облегчало строгое именование принтеров по схеме «Город-Инвентарный номер». В итоге мы написали небольшую программу и парочку powershell-скриптов.
Скрипты
Один скрипт опрашивал AD на предмет учётных записей (это было нужно для двух разных доменов, поэтому скрипт запускался дважды): запрашивались имя пользователя, логин, табельный номер (если он был). Требовалась установка модуля ActiveDirectory для использования его командлетов.
$DC='dc01.contoso.com'
Get-ADUser -Server $DC -Filter {(EmployeeID -ne "null")} -Properties EmployeeID, DisplayName, SamAccountName |`
ft EmployeeID, DisplayName, SamAccountName -AutoSize|`
Out-File -FilePath 'C:\Path\to\result\file.txt'
Стоит учесть один нюанс — количество отображаемых символов (длина строки) в консоли PowerShell. Это настраиваемый параметр (ПКМ по заголовку консоли — Параметры по умолчанию (или Свойства, если вам в текущем сеансе нужно изменить) — Расположение — Размер буфера экрана — Ширина, настройка разблокируется, если убрать галочку Переноса текстового вывода), и лучше сразу указать его с запасом, чтобы в выводе были необрезанные данные.
Другой скрипт опрашивал AD на предмет расшаренных принтеров. Но итог его работы нас совсем не удовлетворил: было сложно парсить результат, и не все принтеры там оказались.
$PrintServer=@('printServer1','printServer2','...','printServerN')
Foreach ($PS in $PrintServer)
{
Get-Printer -ComputerName $PS | fl Name,PortName,Comment | Out-File -FilePath 'C:\Path\to\result\file.txt'
}
Поэтому вручную (мы же были уверены, что это разовая задача) через оснастку Print Management/Управление принтерами были экспортированы списки принтеров со всех принт-серверов. Предварительно в таблице мы оставили только нужные столбцы — Имя принтера, Порт и Комментарий. В нашей ситуации это сработало получше скрипта, потому что есть жесткий регламент по заведению принтеров — имя принтера и shared-имя одинаковы и состоят из названия города и инвентарного номера, порт не имеет наименования и совпадает с IP-адресом, а в комментарии отдельной строчкой прописывается модель принтера.
Это как раз те сведения, которые нам могут понадобиться для сопоставления данных с логами печати. Поскольку регламент появился не сразу, то в выгруженных данных были небольшие огрехи, которые потом исправлялись вручную, но таких принтеров было немного.
Были скопированы файлы логов печати (путь по умолчанию C:\Windows\System32\winevt\Logs\Microsoft-Windows-PrintService%4Operational.evtx) в отдельную папку для удобства парсинга.
Программа
Одна из основных функций программы: на входе получать текстовые файлы с логинами и принтерами, парсить, и на выходе — красивые списки логинов и принтеров без лишнего мусора (на случай, если понадобятся где-то ещё, да и для контроля правильности выполнения работы программы).
Ещё отдельная функция: парсинг логов и вывод информации в текстовый файл. Это необходимо, потому что в логах на одну печать генерируется несколько записей, из которых нужна только одна (с id 307). Да и фильтр по дате был нужен (идея экспортировать кусок логов за выбранный период и с id 307 пришла нам позже). Процесс занимал, кстати, немного времени для всех файлов общим объёмом более двух гигабайт — минуты полторы. Ну и для последующей обработки (если вдруг нужна будет группировка по другому признаку) можно подгружать данные уже из текстового файла, что делается мгновенно.
Необходимо было из всех полученных данных получить красивую табличку. Это не удалось сделать с первого раза (и хорошо, что был текстовый файл вместо evtx-файла) — оказалось, что далеко не все принтеры соответствуют строгой политике именования. А ещё попался один «хитрый» принтер с именем Kaзань-ХХХХХХ (инвентарный номер не имеет значения, важно имя) и было загадкой, почему для некоторых пользователей существуют две строчки с названием города вместо одной, хотя должно суммироваться.
Ответ
Первые два символа латинские, а не кириллические. Имя принтера не стали переделывать, так как пришлось бы перенастраивать его у всех пользователей (share-имя меняется и все текущие подключения к нему «превращаются в тыкву»).
Ну и ещё один нюанс, связанный уже с символом «-». Для того, чтобы не возиться с определением города, имя принтера просто делится через Split('-') на две части и первая часть используется как название города. Ошибка была замечена нами не сразу — ведь у нас есть город Ростов, да и Орехово тоже существует. Разбираться стали, что не так в алгоритме, когда попались Йошкар и Санкт.
В итоге мелкие огрехи с именами принтеров были поправлены, табличка сформирована, отчёт сдан, и вроде бы всё хорошо. Но мы же все прекрасно понимаем, что такие задачи разовыми не бывают? Поэтому никто не удивился письму: «А можно такой же отчёт, но каждый день делать?». В итоге сошлись на ежемесячном и параллельно приступили к реализации отдельного сервиса для выгрузки отчётов. В конечном виде всё это выглядит примерно так (рис. 1):
Выделенный под данную задачу сервер.
Отдельная учётная запись для доступа к журналам на принт-серверах.
База данных на SQLite (логи много места не занимают + портабельность).
Служба для опроса журналов и сбора логов в базу данных.
Сайт для самостоятельной выгрузки различных отчётов по печати заказчиком.
Программа для администраторов с расширенным функционалом.
Как мы к этому пришли?
Вначале за основу мы взяли статью с Хабра «Централизованный сбор и обработка журналов печати Windows». Казалось бы — бери и делай, да радуйся решённой задаче! Но не в нашем случае:
Зачем копировать логи со всех принт-серверов на какой-то другой сервер, если можно не копировать? Отчёты по печати не требуются каждую минуту, опрос логов можно делать раз в определённый период, по очереди каждый принт-сервер.
Не нужно возиться с настройкой политик и доступов — отдельная учётная запись добавляется в группу «Читатели журнала событий» на каждом принт-сервере, и этого достаточно.
Зачем задача в планировщике, если можно сделать службу? Тем более, большая часть кода была уже написана нами ранее.
Никаких текстовых и прочих файлов. Удобнее работать с базой данных. Идеально подойдёт SQLite — данных мало, база портабельна, работать с ней легко, нет лишнего обвеса.
Мы благодарны автору за статью, было интересно почитать, но мы пошли своим путем.
Программа для администраторов
Решили расширить функционал следующим образом:
Добавилась запись в базу данных.
Опрос AD для получения данных о пользователях — без участия PowerShell, код стал немного сложнее, но намного гибче, и в базу пишутся только нужные данные.
Опрос принт-серверов напрямую для получения данных о принтерах — теперь с помощью WMI, что исключает ручной экспорт списков с принт-сервера.
Каждый этап подготовки отчета можно выполнить отдельно.
Вывод результата в табличном виде для просмотра и копирования необходимых столбцов/строк/ячеек.
Плюс на этой программе отлаживался код, который потом можно использовать для службы и сайта.
База данных
Тут всё проще:
Таблица пользователей (каждая строка — это SID / Табельный номер / Логин / Отображаемое имя).
Таблица принтеров (Инвентарный номер / Имя принтера / Локация (да, есть и такой параметр в AD) / Модель / IP).
Помесячные таблицы логов (каждая строка — отдельная запись из логов печати: SID пользователя / Имя документа / Логин пользователя / Компьютер / Имя принтера / IP принтера / Количество байт / Количество страниц / Метка времени / EventRecordId). Обычно для анализа берется прошедший месяц или меньший период, так что незачем перебирать всю базу, да и удалять устаревшие сведения проще, когда они сгруппированы по месяцам.
Служба опроса
Алгоритм опроса простой: в цикле перебираем все принт-серверы, на каждом сервере, в свою очередь, перебираем все записи журнала, добавляем в базу данных новые записи. Обработали все журналы — пауза на определенное время. И по новой. У нас опрос журналов происходит примерно раз в час, полный цикл занимает около 10 минут (опрос всех принтеров, прочитывание журналов, добавление новых записей в базу). Это время можно уменьшить, если уменьшить физический размер журнала на каждом сервере (на текущих настройках помещаются данные примерно за месяц, в зависимости от активности печати). Но на случай отказа работы службы (или сервера целиком) оставили как есть.
Для универсальности все настройки по максимуму были вынесены в отдельный файл (считывается в начале каждого цикла, перезапуск службы не обязателен):
Список принт-серверов (можно указывать только один), предусмотрен второй список принт-серверов — по нему в базу данные тоже забираются, но при необходимости могут обрабатываться отдельно от основного списка (есть у нас особый принт-сервер, по которому строим отдельную статистику).
Логин/пароль учётной записи с доступом к журналам (у данной учётной записи больше никуда доступа нет, так что бояться нечего).
Список закладок (перекликается со списком принт-серверов, отдельный список закладок для особых принт-серверов). Вообще, хотелось использовать «родную» для файлов логов систему закладок, чтобы не перечитывать журнал с самого начала, но она почему-то не предусматривает возможности сохранения куда-либо вне памяти и дальнейшей загрузки для использования. Поэтому пришлось «изобретать свой велосипед» — в качестве закладки сохраняется EventRecordID, уникальный для каждой записи в рамках одного принт-сервера, и при следующем чтении журнала обработка записи лога начинается, только если id больше, чем у последней обработанной. Такое пролистывание журнала не особо замедляет процесс обработки. У нас не стояло задачи актуализировать базу в режиме онлайн, поэтому небольшое отставание допустимо.
Строка подключения к базе (портабельность — база переехала, конфигурацию поправили, и всё снова работает).
Также предусмотрено минимальное логирование. Было решено не засорять журнал событий Windows, да и усложнять поддержку службы: в отдельный файл пишется время начала очередной операции, количество новых записей, добавленных в базу (отдельно по каждому принт-серверу), какой принт-сервер последним был успешно обработан, время окончания предыдущего успешного цикла. Вообще, это всё больше необходимо для отладки, но было оставлено и для простой проверки работы службы.
Ещё хотелось бы отметить один нюанс — если будете так же разбирать данные логов, то берите SID пользователя, а не его логин. Может так случиться, что пользователь поменяет фамилию и вместе с ней логин, и при сопоставлении можно не найти нужное имя.
При реализации службы возникла пара сложностей. Изначально мы предполагали, что служба будет опрашивать AD на предмет новых или изменённых пользователей (например, смена фамилии) и принт-серверы на предмет новых и изменение текущих принтеров (переезды и замены случаются регулярно). Но почему-то код, который работал в режиме отладки, не работал на опубликованной службе. И если опрос пользователей удалось организовать другими параметрами сборки (зависимые библиотеки не внедряются в исполняемый файл, что уже не так удобно), то опрос принт-серверов стабильно вызывал падение. Причём «падало» на конкретной части кода — при попытке задать кастомные учётные данные для WMI-подключения. Мы попробовали использовать учётные данные, под которыми запускалась служба. Для этого в свойствах службы специально была прописана учётка, имеющая права администратора на принт-сервере, но и это не помогло. Так что пришлось отказаться от опросов по расписанию и оставить только ручное добавление через программу. Благо, изменения вносятся не слишком активно, и если где-то в отчёте проскочит вместо имени логин (да, если имя пользователя в базе не найдено, то подставляется логин, для принтеров значением по умолчанию берётся имя принтера), то это не критично — данные в базе быстро актуализируются вручную.
Сайт
Для реализации был выбран Blazor. Почему именно он? Давно хотелось с ним поработать. Тем более, много страниц не требовалось. Авторизацию с личным кабинетом делать не нужно — разграничение прав делается практически из коробки, необходимо только правильно настроить и прописать в коде политики доступа (документация помогает сделать это быстро). Также на сайт была добавлена поддержка мультиязычности (не самый обязательный элемент, просто на всякий случай), и так же легко — после вдумчивого чтения документации и пары простых действий. Ну и три страницы:
Главная (как же без неё!). Обычная пустая страница с небольшим текстом для всех, кто каким-то чудом умудрился попасть на сайт (хотя сайт не требуется опубликовывать для всех, строгая политика безопасности доступов).
Страница для просмотра-выгрузки отчётов с выбором, что именно выгружать — какие столбцы, что с чем группировать-суммировать. То, ради чего всё и затевалось изначально.
Дополнительная страница для администраторов с почти таким же функционалом, как у отдельной программы.
Как бонус, из коробки доступно масштабирование элементов интерфейса под мобильные платформы — не надо делать отдельную мобильную версию.
Существует много различных возможностей реализовать что-то подобное. Если говорить о задаче конкретного заказчика — считаем, что у нас получилось оптимальное решение. Возникали ли у вас похожие задачи, и как вы с ними справились?
Jet Service Team