Projektowanie stron WWW od podszewki
Losowy artykuł: [PHP] Potęgowanie

Artykuły na każdy temat

[PHP] Logger, czyli klasa do zapisywania niefortunnych zdarzeń

Dodano 07.05.2011r. o 14:42
W ostatnim komentarzu obiecałem, że napisze o loggerze i tak się stało. Dawno temu poszukiwałem rozwiązań godnych użycia jednak na takie się nie natknąłem. Wczoraj ponownie rozglądałem się w internecie pod tym kątem i znowu nic ciekawego nie znalazłem. Te poszukiwania nie wzięły się z niczego. Kiedy zacząłem pracować nad nowym silnikiem swojej strony jedną z pierwszych rzeczy których potrzebowałem był właśnie moduł loggera. O ile dobrze pamiętam to mój logger miał trzy lub cztery wcielania, wliczając w to te ostatnie, które zaprezentuje Wam poniżej. Niestety wcześniejsze wersje miały pewne ograniczenia. Pierwszy z nich mógł logować ID błędu, czas wystąpienia problemu, IP, browser. Jedynym mankamentem było to, że nie można było zapisywać tablic bądź obiektów. Dla przykładu, gdy przydarzyło się coś naprawdę ciekawego i chciałem zapisać np. tablice $_GET lub $_POST to fizycznie nie miałem takiej możliwości. W drugiej wersji loggera można było już zapisywać tablice i obiekty, jednak wystąpił inny problem. Format logów był tak skonstruowany, że napastnik znający znak rozdzielający logi mógł je zakrzaczyć. Zakrzaczyć to znaczy utrudnić ich odczytanie. Oczywiście tym znakiem nie był typowy \n, lecz zmyślnie wyrafinowany krzaczek z tablicy UTF-8 o ile dobrze pamiętam. Po jakimś czasie doszedłem do wniosku, że trzeba unormować zapis logów i zoptymalizować kod. Wczoraj ten cel został zrealizowany. Zanim przejdę do właściwego miejsca (podania kodu), to powinienem go troszeczkę opisać. Klasa logger składa się z następujących elementów:
Pola:
  • $config - pole jak sugeruje nazwa zawierające konfiguracje
  • $log - pole zawierające logi
  • $dumped - pole zawierające informację, mówiącą o tym czy doszło do zrzut logów
  • $critical_error - pole zawierające informację, czy któraś z dodanych wiadomości była błędem krytycznym ($level > 2)
Metody:
  • logger() - konstruktor pobierający jeden argument ($config)
  • add_event() - metoda dodająca zdarzenie do listy logów
  • is_critical_error() - metoda zwracająca informacje czy wystąpił błąd krytyczny
  • dump() - metoda "zrzucająca" logi do pliku
W największym skrócie właśnie tak to wygląda. Teraz trochę więcej szczegółów. Konstruktor pobiera jeden parametr będący tablicą. Obiekt ten może przybrać następujące wartości:
  • $config['dir'] - zawiera ścieżkę do folderu z logami
  • $config['extension_file'] - zawiera rozszerzenie pliku z logami
  • $config['format_file'] - zawiera format pliku logu, czyli ta wartość zostanie przepuszczona przez funkcje date() i dodane zostanie rozszerzenie, co w rezultacie da nam finalną nazwę pliku
  • $config['format_time'] - zmienna zawierająca informacje, w jakim formacie ma być przechowywana data ramki zdarzenia. Jeżeli nie zostanie zdeklarowana to czas zdarzenia będzie pochodził z funkcji time().
  • $config['ip2long'] - jeżeli wartością tej zmiennej jest true i nie zdefiniowano $config['ip'] to IP osoby, która uruchomiła dodanie zdarzenia będzie przepuszczone przez funkcje ip2long() natomiast w innym przypadku zostanie wrzucone po prostu $_SERVER['REMOTE_ADDR'] bądź $config['ip'], jeżeli została zadeklarowana. Tą zmienną można zignorować przy deklaracji konfiguracji, jeżeli podajemy $config['ip'].
  • $config['ip'] - zmienna nie wymagająca deklaracji zawierająca IP odwiedzającego. W związku z tym, że IP odwiedzającego pobieram metodą get_ip() będącą elementem klasy silnika mojego nowego CMS'u to zwracana wartość po prostu przekazuje do poszczególnych modułów, jeżeli wymaga tego sytuacja. Głownie chodzi o to, aby nie powtarzać fragmentów kodu i o to, aby poszczególne klasy (moduły) były uniwersalne.
Najważniejszą metodą w całej klasie jest add_event(). To właśnie ona dodaje zdarzenie do ramki. Parametr $error_id odpowiada za kod błędu. Oczywiście musicie sobie wypracować jakąś tabelkę mówiącą, co dany ID oznacza. Dla przykładu 1 może oznaczać problem z połączeniem do bazy danych, 2 może oznaczać problem z wykonaniem zapytania, itd. Zmienna $level oznacza poziom błędu. Moja skala składa się z trzech elementów. Poziom pierwszy to błędy niskiego poziomu, bez których aplikacja może jeszcze funkcjonować. Poziom drugi oznacza błąd średniego szczebla, który jest już poważniejszy. Trzeci poziom błędu jest nazywany przeze mnie krytycznym. Jeżeli wystąpi taka sytuacja to kontynuacja wykonywania skryptu mija się z celem. Kolejną zmienną jest $file i powinna zawierać wartość stałej __FILE__. Następną zmienną jest $line i analogicznie jak do poprzedniej sytuacji jej zawartością powinna być stała __LINE__. Ostatnie dwie zmienne nie są wymagane i mogą służyć pomocą w celu ustalenia usterki. Dla przykładu do $extra_information możemy wrzucić wybrane przez nas tablice superglobalne takie jak $_POST, $_GET, $_COOKIE, $_SESSION i inne potrzebne informacje. Ostatnią zmienną jest $debug_backtrace, która zawiera wartość zwracaną przez funkcje debug_backtrace(), jeżeli jest taka konieczność.

Jak już wcześniej powiedziałem albo i nie ramka, to nic innego jak jeden przebieg skryptu. W ramce może znajdować się jedno lub więcej zdarzeń (eventów). Skonstruowałem taki a nie inny format "bazy danych”, aby łatwiej było potem przeglądać dane. Wcześniejsza wersja loggera zapisywała zdarzenia do odpowiedniego pliku bądź plików na podstawie daty z loga bez uwzględnienia przebiegu skryptu. W efekcie tego trzeba było się domyślać czy ten błąd wystąpił po innym czy też to był osobny przypadek. Pamiętajcie, że plik logu z konkretnego dnia nie może być pusty. Jest to związane z miksowaniem danych. Konkretnie chodzi o to, że jeżeli spróbujemy wykonać operacje łączenia tablic, kiedy jedna z nich będzie pusta to w wyniku tego przedsięwzięcia otrzymamy stosowną informacje w postaci błędu. Przypadek ten został rozwiązany za pomocą zastosowania funkcji filesize(). Kolejną sprawą jest dumpowanie. Tą operacje można wykonać tylko raz przy obecnym stanie rzeczy. Chwile rozmyślałem nad tym, aby dodać ekstra pętle w przypadku, kiedy nastąpi niepowodzenie przy ładowaniu bądź zapisywaniu logu jednak zrezygnowałem z takiego rozwiązania. Jeżeli będziecie chcieli coś takiego dodać, to zmienną $path trzeba będzie wpakować do jakiegoś pola w klasie, wkleić pętle na kilka okrążeń (np. 3) i pod koniec skoku dodać jakiegoś małego sleepa jeżeli operacja nadal nie powiodła się natomiast jeżeli się powiodła to wyłamanie ze struktur przy pomocy break i tyle w tym temacie. Ponadto nie loguje takich pierdół jak ekstra dodatek do czasu, pochodzący z microtime,() bo i po co? Chyba nikomu nie przydadzą się takie informacje, w której milisekundzie dany kod popełnił faux pas. Inną kwestią jest, że takie pierdoły przydają się w klasach służących do testów jednostkowych i...

Jeżeli chodzi o format zapisywania logów to jest on stosunkowo prosty. Każda następna ramka to po prostu kolejny ID z układanki. Czyli de facto coś jak auto_increment w bazach danych. Najnowsze wiadomości z danego dnia mają najwyższe ID w tablicy. Oczywiście można sobie to posortować w drugą stronę. Co kto lubi, serio. Wartościami uniwersalnymi dla każdej ramki są IP wywołującego zdarzenie, identyfikacja jego browsera oraz czas popełnienia zbrodni. Każde zdarzenie jest zapisywanie w tablicy i kluczem do tej tablicy jest ID. Czyli jeżeli zdarzeń jest kilka, to będą występowały na takiej samej zasadzie, jaka została opisana powyżej. Ażeby Wam to zobrazować możliwie klarownie proponuje przeanalizowanie poniższego listingu:
Kod:
Array
(
    [0] => Array
        (
            [time] => 2011-05-05 019:46
            [ip] => 127.0.0.1
            [browser] => Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.8.131 Version/11.10
            [0] => Array
                (
                    [id] => 1
                    [level] => 2
                    [file] => D:\xampp\htdocs\test\index.php
                    [line] => 4
                    [extra_information] => 
                    [debug_backtrace] => 
                )

        )

    [1] => Array
        (
            [time] => 2011-05-05 01:10:13
            [ip] => 127.0.0.1
            [browser] => Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.8.131 Version/11.10
            [0] => Array
                (
                    [id] => 1
                    [level] => 2
                    [file] => D:\xampp\htdocs\test\index.php
                    [line] => 4
                    [extra_information] => 
                    [debug_backtrace] => 
                )

            [1] => Array
                (
                    [id] => 2
                    [level] => 2
                    [file] => D:\xampp\htdocs\test\index.php
                    [line] => 5
                    [extra_information] => 
                    [debug_backtrace] => 
                )

        )

    [2] => Array
        (
            [time] => 2011-05-05 01:11:20
            [ip] => 127.0.0.1
            [browser] => Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.8.131 Version/11.10
            [0] => Array
                (
                    [id] => 1
                    [level] => 2
                    [file] => D:\xampp\htdocs\test\index.php
                    [line] => 4
                    [extra_information] => 
                    [debug_backtrace] => 
                )

        )

)
Najprostszą formą użycia tego loggera jest format:
Kod:
<?php
// Load the necessary things
include('./logger.class.php');

$logger = new logger;
// Add an event
$logger -> add_event(11__FILE____LINE__);
// Save logs if they exist
$logger -> dump();
?>
I teraz najbardziej oczekiwany fragment kodu, czyli sam rdzeń aplikacji:
Kod:
<?php
/**
 * @author CapaciousCore
 * @version 1.00.00
 */

class logger
{
 private $config;
 private $log;
 private $dumped;
 private $critical_error;

 public function logger($config '')
 {
  if(!$config)
  {
   $this -> config['dir'] = './logs/';
   $this -> config['extension_file'] = '.log';
   $this -> config['format_file'] = 'Y-m-d';
   $this -> config['format_time'] = 'Y-m-d H:i:s';
   $this -> config['ip2long'] = false;
  }
  else
  {
   $this -> config $config;
  }

  $this -> critical_error $this -> dumped false;
 }

 public function add_event($error_id$level$file$line$extra_information ''$debug_backtrace '')
 {
  if(!$this -> dumped)
  {
   if(!$this -> log)
   {
    $this -> log[] = array('file_name' => date($this -> config['format_file']), 'time' => ($this -> config['format_time'] ? date($this -> config['format_time']) : time()), 'ip' => ($this -> config['ip'] ? $this -> config['ip'] : ($this -> config['ip2long'] ? ip2long($_SERVER['REMOTE_ADDR']) : $_SERVER['REMOTE_ADDR'])), 'browser' => $_SERVER['HTTP_USER_AGENT']);
   }

   $this -> log[0][] = array('id' => $error_id'level' => $level'file' => $file'line' => $line'extra_information' => $extra_information'debug_backtrace' => $debug_backtrace);

   if(!$this -> critical_error && $level 2)
   {
    $this -> critical_error true;
   }
  }
 }

 public function is_critical_error()
 {
  return $this -> critical_error;
 }

 public function dump()
 {
  if(!$this -> dumped)
  {
   if($this -> log)
   {
    $path $this -> config['dir'].$this -> log[0]['file_name'].$this -> config['extension_file'];
    unset($this -> log[0]['file_name']);

    // If the log file exists then mix it!
    if(file_exists($path) && filesize($path) > 0)
    {
     $old_logs unserialize(@file_get_contents($path));

     // Boom Headshot
     if(!$old_logs)
     {
      return false;
     }

     $this -> log array_merge($old_logs$this -> log);
    }

    if(@file_put_contents($pathserialize($this -> log), LOCK_EX))
    {
     $this -> dumped true;

     return true;
    }
    else
    {
     // Shit happens

     return false;
    }
   }
  }
 }
}
?>
Odnośnie wspomnianej metody get_ip() to jej konstrukcja wygląda nastepujaco:
Kod:
<?php
class engine
{
 // ciach

 protected function get_ip($ip2long true)
 {
  if($_SERVER['HTTP_CLIENT_IP'])
  {
   $ip $_SERVER['HTTP_CLIENT_IP'];
  }
  else if($_SERVER['HTTP_X_FORWARDED_FOR'])
  {
   $ip $_SERVER['HTTP_X_FORWARDED_FOR'];
  }
  else
  {
   $ip $_SERVER['REMOTE_ADDR'];
  }

  if($ip2long)
  {
   $ip ip2long($ip);
  }

  return $ip;
 }

 // ciach
}
?>
Wkrótce udostępnię obiektowy punkt widzenia odnośnie przetwarzania i wyświetlania logów.

Aktualna wersja kodu loggera będzie dostępna w tym artykule oraz w dziale skrypty, do którego gorąco zapraszam.

Inne dodatki

Komentarze

Publikowane komentarze są prywatnymi opiniami użytkowników serwisu. Serwis nie ponosi odpowiedzialności za treść opinii. W trosce o zachowanie poziomu dyskusji wszystkie komentarze podlegają akceptacji przed ich publikacją dlatego proszę cierpliwie czekać aż komentarz zostanie opublikowany.

CapaciousCore

Dodano 07.08.2012r. o 13:59
@hehs klasa engine jest wycinkiem z kodu CMS'a, który jest w trakcie produkcji. Nie mniej jednak nie zamierzam go upubliczniać. Nie widzę potrzeby używania w nim statycznych metod, które tutaj de facto są złym pomysłem Smile W konstruktorze możemy przecież potworzyć to co nam jest potrzebne i potem lecieć z tym koksem.

W założeniu wygląda to tak, że engine steruje wszystkim jest jakby to nazwać otoczką, ładuje sobie moduły, pośredniczy w komunikacji miedzy modułami i tak dalej. Więc odwołujemy się $this -> get_ip() albo parent::get_ip() (z modułów).

hehs

Dodano 05.08.2012r. o 14:32
Zastanawia mnie jak działa ta klasa engine.

Dodajesz do niej init() i tam do każdej metody się odwołujesz przez this? Czy coś w stylu engine::get_ip()? W sumie to nie jest static...

Nie wiem jak dobrze ułożyć schemat aplikacji, a nie chce używać jakiś chorych mvc.

Dodaj komentarz

Zostaw komentarz jeżeli możesz! Nie bądź przysłowiowym botem! Nie bądź obojętny! Ciebie to nic nie kosztuje, a mi sprawi uśmiech na twarzy.
Zezwolono używać: BBCode
Zabroniono używać:
znaczników HTML

(Wymagany)

(Wymagany, niepublikowany)

(Nie wymagana)

Token:

Obrazek dla bota

(Przepisz tylko cyfry!)

(Wymagana)