16+
ComputerPrice
НА ГЛАВНУЮ СТАТЬИ НОВОСТИ О НАС




Яндекс цитирования


Версия для печати

Модуль поиска не установлен.

Практика написания сценария на PERL для нужд системного администратора.(Нестандартный анализ лог-файла прокси-сервера Squid Web Proxy Cache)

07.01.2004

Татьяна Ильченко < ti@sysadmins.ru > Ростов-на-Дону, 2003

Данная статья предназначена для системных администраторов, которые уже немного знакомы с PERL и, кроме того, желают получить от сервера максимум полезной в работе информации. Автор НЕ ЯВЛЯЕТСЯ ни экспертом, ни гуру в PERL-программировании. Статья родилась в результате желания рассказать о своем опыте в изучении некоторого количества вопросов, которые могут быть полезны коллегам по цеху.

Часть 2

Что можно взять из лог-файла прокси-сервера и как это применить

Остановимся чуть подробнее на работе с файлами в PERL - поскольку это основа нашего сценария - обработка текстового файла. Все операции ввода/вывода осуществляются через файловые манипуляторы (filehandle). Манипулятор представляет собой символическое имя, идентифицирующее и представляющее файл в операциях чтения/записи. Он не является переменной, и в его имени отсутствуют префиксы @,$,%. Манипуляторы бывают прямыми и косвенными - в обычном случае это будет прямой манипулятор, когда вы не передаете имя файлового манипулятора специальным образом подпрограмме через переменную (для этого надо использовать префикс *, который является признаком тип-глоба, или базовой единицы символьной таблицы PERL) - такой манипулятор будет косвенным (indirect filehandle). Кроме того, в PERL есть два стандартных модуля IO::File и FileHandle, позволяющие создавать и использовать анонимные файловые манипуляторы. Последние, в свою очередь, позволяют ускорить работу программы. В некоторых источниках упоминается еще и то, что использование косвенных файловых манипуляторов улучшает "удобочитаемость" программ, что мне представляется достаточно сомнительным, но тут, видимо, дело в разном восприятии и подходах к написанию исходного кода.

Откроем лог-файл, путь к которому мы описали ранее переменной $accesslog, в режиме чтения функцией open(), которой мы пользовались уже при разборе конфигурационного файла. И, последовательно читая строки из него, разберем их с помощью регулярных выражений. Стоит отметить, что регулярные выражения (regular expressions - сокр. regexp) - весьма удобный и гибкий инструмент создания шаблонов для отбора совпадений строк или подстрок, хотя и обладающий "хитроумным" синтаксисом записи. При том, что семантика регулярных выражений достаточно нетривиальна, в их PERL-реализации наблюдаются три особенности, которые суть ключи к пониманию работы поиска с использованием регулярных выражений (поиска по шаблону) - в книге Т.Кристиансена и Н. Торкингтона "PERL. Библиотека программиста" они упоминаются как "жадность", "торопливость" и "возврат". "Жадность" поиска по шаблону проявляется в том, например, что поиск по шаблону, содержащему квантификатор '*', может дать несколько вариантов, но результатом станет строка наибольшей длины. "Торопливость" заключается в том, что в поиск с использованием '*' (кроме него есть еще квантификаторы '+', '?' и '{}' - все они называются еще "жадными" или максимальными квантификаторами) в шаблоне PERL удовлетворится первым совпадением, содержащим "0 и более символов", а это может оказаться совсем не та строка, которая нужна. Вывод: при необходимости точного совпадения с условием поиска использовать максимальные квантификаторы нужно с осторожностью, а еще лучше использовать минимальные квантификаторы - '*?', '+?', '??' и '{}?'. А возврат объясняется тем, что при поиске по шаблону должно совпасть всё регулярное выражение, и, следовательно, если совпадает только начало шаблона, механизм поиска возвращается к началу и пытается найти любое другое совпадение. В данном случае, мы будем использовать простой шаблон, содержащий определенную комбинацию подстрок и пробелов, поскольку нам необходимо выделять для начала все строки, содержащие нужную нам информацию. Те же строки, которые не удовлетворяют нашему шаблону-условию, будем пропускать. Опишем шаблон - использование простой комбинации последовательностей символов и пробелов в нашем случае сыграет нам на руку (зачем усложнять, когда можно обойтись простым инструментом). Укажем, что при несовпадении шаблона нужно перейти к выполнению следующего шага в цикле, а при совпадении присвоим соответствующим переменным значения, которые принимают встроенные переменные $1, $2,...$10 (вообще, PERL не останавливается на $9 встроенной переменной, и их может быть любое количество, равное количеству обратных ссылок в шаблоне, то есть частей шаблона, заключенных в круглые скобки). (См. Листинг № 1).

Как вы помните, дата и время в лог-файле SQUID записываются в виде количества секунд с начала эпохи (см. выше описание лог-файла SQUID), а нам нужен удобочитаемый формат. Для преобразования мы воспользуемся функцией gmtime(), поскольку у меня время на сервере установлено по UTC (исторически сложившаяся необходимость). В другом случае вы можете воспользоваться функцией localtime, которая возвращает т.н. локальное время, то есть соответствующее вашему часовому поясу. Еще две особенности преобразования времени в PERL - отсчет месяцев у него начинается с 0, которому соответствует Jan (январь), а нам нужно прибавлять к полученному единицу, чтобы получить актуальный месяц; и год представлен как разница между текущим годом и 1900. (См. Листинг № 2).

Теперь преобразуем получившиеся дату и время в формат ISO-8601 так же, как мы это делали с аргументом командной строки в самом начале. Для этого мы воспользуемся функцией sprintf(), которой в качестве параметров для преобразования формата записи времени укажем "%2d:%2d:%2d" и очередность подстановки переменных $hour, $min, $sec - час, минуты, секунды соответственно. А для преобразования формата записи даты - год, месяц, день, с тем чтобы год отображался полностью четырьмя знаками, а месяц и день - двумя (тогда сюда же попадут и незначащие нули, что необязательно, конечно, но при выводе отчета придаст "стройность" этим двум колонкам - даты и времени). (См. Листинг № 3).

Теперь присвоим $matched значение $status, чтобы разделить код ответа сервера и его статус выполнения. (См. Листинг № 4).

После того как мы разобрали всю строку, проверим, попадает ли она в указанный в командной строке интервал дат. В PERL для сравнения строк существует отдельный набор операторов. Механизм же сравнения строк я постараюсь коротко пояснить. Если говорить в общем случае, то все операторы сравнения сравнивают величины заданных операндов (или аргументов). Так же, как при арифметических операциях (как мы помним, в PERL нет "жестокого" определения типов переменных), Perl преобразует строчные операнды в численные, перед тем как выполнять сравнение. Для сравнения строк, не являющихся числами, в PERL есть специальные операторы строкового сравнения. Они сравнивают строки, используя величины ASCII. Если численное значение задано как операнд при сравнении строк, оно сначала преобразуется в строку, а затем уже выделяется его код ASCII, и сравнение идет так, как описано выше. Операторы сравнения строк в PERL (для удобства я привожу соответствие операторов сравнения строк численным операторам сравнения) приведены в таблице 1.

Теперь, когда мы разобрались со сравнением строк в PERL, перейдем к следующему фрагменту нашего сценария - отбору строк, совпадающих с описанным ранее шаблоном, по условию вхождения даты в указанный интервал. В школьном курсе математики это называлось нестрогим неравенством - аргумент, он же операнд, должен быть большим или равным началу интервала и меньшим или равным концу того же интервала. Затем разобьем каждые сутки из интервала дат на часовые интервалы для сбора статистики работы сервера по часам. Интервалы у нас получатся такого вида: "00:00 - 00:59, 01:00-01:59, ..., 23:00-23:59". После этого каждую уникальную дату из интервала занесем в отдельный массив @dates (его элементы нам пригодятся в качестве ключей к хэш-массивам почасовой статистики). Уникальность даты будем проверять путем сравнения новой даты с уже имеющимися в массиве. (См. Листинг № 5).

Теперь, собственно, начнем собирать ту самую почасовую статистику работы сервера. Два ассоциативных массива: %recordsh - для количества запросов за указанный часовой интервал из суток (нам понадобятся два ключа - сам временной интервал $timeperiod и дата $daterpt, к которой он относится) и %recordsd - для общего количества запросов за каждые сутки (ключом для него будет опять же $daterpt). Соответственно, каждый из элементов массива инкрементируется (увеличивается на заданный шаг - единицу), таким образом, мы подсчитываем количество запросов за каждый час и за сутки в целом. И так - для каждых суток из интервала дат, указанных в командной строке. Как говорится, "долго сказка сказывается" - на самом деле, все вышеописанное помещается в две строчки кода. (См. Листинг № 6).

Приступим теперь к отбору строк, содержащих нужные нам коды событий. То есть, $success должен совпадать с одним из элементов массива кодов событий @syms. Отобранные таким образом строки в видоизмененном формате будем складывать в массив @denystrings для последующей записи их в файл отчета. Потом подсчитаем количество наступивших событий за каждый час и за каждые сутки так, как мы это делали для общего количества запросов к серверу выше. Ключи у ассоциативных массивов %tcpmissd (суточное количество событий) и %tcpmiss (количество событий за каждый час) те же, что и у %recordsd и %recordsh, соответственно. И методика подсчета та же - простой инкремент на единицу при совпадении $status с одним из элементов из @syms, и при несовпадении кода ответа сервера с элементами @codes (это может понадобиться, например, чтобы исключить ошибки с кодами "404 - Объект не найден" или "403 - Доступ запрещен", то есть исключить ошибочные запросы пользователей) . (См. Листинг № 7).

После этого можно считать, что все, что нам нужно, мы от лог-файла получили, и теперь мы его со спокойной совестью закроем. Следующее, что нам нужно сделать, - вывести рассчитанные показатели пользователю на экран или в файл в соответствии с конфигурацией, описанной в squidlog.conf.

Сначала организуем вывод данных в файлы отчетов - в дальнейшем можно расширить функционал нашего сценария, дополнив его процедурами дополнительного анализа этих файлов, формирования на их основе html-отчетов и выкладывания их на корпоративный сервер, построения графиков загрузки сервера... Сначала в файл с именем $daterpt.sqr выведем статистику в том же виде, в котором она будет выдана на консоль. Вот как выглядит примерно эта статистика, выданная на консоль (отсортированная по количеству событий в порядке убывания последних). (См. Листинг № 8).

Обратимся к фрагменту исходного кода, "отвечающего" за вывод информации в файл (вывод на консоль будет отличаться только направлением вывода - не надо будет указывать файловый манипулятор, по умолчанию вывод осуществляется на консоль).

Для реализации вывода организовываем перебор в цикле каждого ключа для ассоциативного массива, элементы которого отсортируем с помощью специально написанной функции sort_values_bynum(). Её мы рассмотрим дальше. Соответственно, для каждого ключа в теле цикла будет выполняться следующая последовательность действий: открываем файл для записи и построчно записываем дату, общее количество запросов, количество событий. (См. Листинг № 9).

Так называемую заголовочную часть мы записали в файл... Это у нас как бы заголовок секции (их будет столько, сколько суток в себя включает интервал дат, заданный пользователем). Теперь запишем в файл почасовую статистику, предварительно отсортировав сначала сами временные интервалы как ключи для элементов ассоциативного массива %recordsh с помощью все той же функции sort_values_bynum(). (См. Листинг №10).

Еще раз напомню, что вывод на консоль осуществляется таким же образом, что и в файл, за исключением того, что направлением вывода теперь будет консоль и будет учитываться указанный в order_sort порядок сортировки выводимых данных. Принцип вначале тот же самый - вывести заголовочную часть секции. (См. Листинг №11).

Отсюда начинаются различия: мы обратим внимание на порядок сортировки данных почасовой статистики и переберем все возможные комбинации двух параметров - критерия (либо по временным интервалам, либо по количеству событий) и порядка сортировки (по убыванию или возрастанию). Вариантов может быть всего четыре: 1) по возрастанию временных интервалов (то есть в прямом порядке перечисления от 00:00-00:59 до 23:00-23:59); 2) по возрастанию количества событий; 3) по убыванию временных интервалов (в обратном порядке); 4) по убыванию количества событий. Последовательно переберем эти варианты и соответственным образом отсортируем данные внутри каждой секции. Для примера я приведу только фрагмент кода для сортировки вывода по возрастанию/убыванию временных интервалов, а сортировку по количеству событий предлагаю реализовать читателю самостоятельно. (См. Листинг № 12).

Теперь проверим значение параметра out_denystring, значение которого мы храним в переменной $out_deny, дабы выяснить, нужно ли выдавать на консоль элементы из @denystrings, и если оно совпадает с одним из вариантов написания утвердительного "Yes", построчно выведем все элементы из массива на консоль. (См. Листинг № 13).

После этого запишем все элементы этого массива в файл sqlog.deny (иногда я думаю, что и к этому имени файла неплохо бы добавлять дату его генерации, чтобы созданный предыдущим сеансом файл не затирался новым, но тут возникли разногласия с "соавтором ТЗ, и я решила оставить все как есть).

Вот, собственно, мы и добрались до конца нашего сценария. Осталось описать только функцию сортировки данных. Она сводится к сортировке массивов (в том числе и ассоциативных) в порядке убывания/возрастания значений элементов. Функция sort() возвращает вызывающей строке ключи переданного в функцию sort_values_bynum() массива в последовательном порядке. Другими словами, мы объявляем в функции ассоциативный массив, ключи которого будут отсортированной в зависимости от порядка сортировки (который, как мы помним, определяется переменной $sort_o) копией переданного в качестве параметра нашей функции массива. (См. Листинг № 14.)

Рассмотрим подробнее строку, где непосредственно выполняется сортировка: вот эта часть строки "sort { $array{$b} <=> $array{$a};} " сортирует ключи, определенные строкой "keys %array;" в обратном порядке (наименьшее значение помещается первым). Для сортировки их в прямом порядке достаточно поменять переменные $a и $b местами, либо добавить перед сортировкой ключевое слово reverse ("обратный"). Строго говоря, такая процедура не будет достаточно эффективной из-за того, что здесь для сортировки требуется создавать явную копию исходного массива. Более красивым решением будет использование ссылок на исходный массив и преобразование его "на лету", реализовать это я предоставлю читателю для практики. Я лишь отмечу, что ссылки в PERL аналогичны указателям в С - это просто другое имя или указатель на исходный массив.

Итак, подведем итоги нашего труда. Надеюсь, мне удалось убедить сомневающихся (если такие есть!) в полезности "нестандартной" обработки лог-файлов прокси-сервера на "живом" примере. Я постаралась показать читателю, что для манипуляций и анализа текстовой информации возможность использования прямо в исходном коде регулярных выражений демонстрирует гибкость PERL и большой простор для его применения в целях "облегчения жизни" системного администратора. Помимо этого, многочисленные заимствования конструкций из других языков программирования (в большинстве случаев заимствованные операторы и конструкции PERL, так же как и большая часть синтаксиса, пришли в PERL из C) здорово облегчает изучение и понимание принципов написания PERL-сценариев, особенно для C-программистов. На самом деле, довольно сложно поместить в рамки данной статьи все примеры преимуществ PERL в обработке текстовой информации, замечу только, что описанный в статье SquidLog довольно шустро "пошел в народ" и зажил самостоятельной жизнью, а я иногда с удивлением получаю письма от коллег по цеху из самых разных городов с замечаниями и предложениями.

P.S. Некоторые фрагменты исходного кода менялись уже в ходе работы над этой статьей в процессе переосмысления, посему они могут не совпадать с ныне здравствующей версией SquidLog-1.0.2beta, изначально предложенной читателю для разбора. Все же изменения, надеюсь, увидят свет в следующей версии, которую я планирую выпустить на просторы Сети в скором времени.

Использованные источники:

1."Спецификация языка Perl", Алена Федосеева [http://www.citforum.ru/database/cnit/p2.shtml].

2."Hello, Perl!", Дмитрий Репин aka cmapuk[0nline] [cmapuk@pisem.net].

3."PERL. Библиотека программиста",

Т. Кристиансен, Н. Торкингтон. Изд-во O"REILLY.

4. "PERL. Архив программ", М. Браун. Изд-во "БИНОМ".

Вообще, регулярные выражения так или иначе реализованы во многих языках программирования, но регулярные выражения PERL интегрированы уже на уровне самого языка и позволяют использовать экспертные модели при поиске строк.

Результатом операции сравнения в общем случае будет 1, если сравнение истинно, и 0 в противном случае. Последняя операция (<=> или cmp) может возвращать значения -1, 0 или 1 (значение левого операнда меньше правого, равно ему или больше его)



статьи
статьи
 / 
новости
новости
 / 
контакты
контакты