Донецкий техникум промышленной автоматики

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

  1. Частина 2
  2. Використані джерела:

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

07.01.2004

Тетяна Ільченко < [email protected] > Ростов-на-Дону, 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 досить спритно "пішов в народ" і зажив самостійним життям, а я іноді з подивом отримую листи від колег по цеху з самих різних міст із зауваженнями і пропозиціями.

PS Деякі фрагменти вихідного коду змінювалися вже в ході роботи над цією статтею в процесі переосмислення, тому вони можуть не збігатися з нині здорової версією SquidLog-1.0.2beta , Первісно запропонованої читачеві для розбору. Все ж зміни, сподіваюся, побачать світло в наступній версії, яку я планую випустити на простори Мережі незабаром.

Використані джерела:

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

2. "Hello, Perl!", Дмитро Рєпін aka cmapuk [0nline] [ [email protected] ].

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

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

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

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

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


Крім нього є ще квантіфікатори '+', '?
Висновок: при необхідності точного збігу з умовою пошуку використовувати максимальні квантіфікатори потрібно з обережністю, а ще краще використовувати мінімальні квантіфікатори - '*?
? '{}?