ugrás a tartalomhoz

Hibanapló Zend Framework alkalmazásokhoz

tiku I tikaszvince · 2011. Jan. 31. (H), 16.46
Hibanapló Zend Framework alkalmazásokhoz

A korábbi cikkben megnéztük, hogyan lehet Zend Framework alkalmazásunkat rávenni hozzáférési napló írására. Ezúttal vegyük rá a rendszert a hibák naplózására!

A sorozatban megjelent

Mit is kellene a naplóba írni? Rögzítsük

  • az aktuális felhasználónk adatait,
  • milyen IP-ről jött a kérés, ami hibához vezetett,
  • az URL-t ahol a hiba keletkezett
  • – természetesen kell időbélyeg is –,
  • valamint maga a hiba.
  • És a biztonság kedvéért a kérés lehető legtöbb adata.

A naplót most is SQLite adatbázisba írjuk. A hozzáférési naplóval ellentétben ezúttal csak egy naplót fogunk írni. A Zend Framework szállít már egy hibakezelő bővítményt (Zend_Controller_Plugin_ErrorHandler). Mivel ennek a működését szeretnénk megtartani, származtatjuk azt, majd ezt a leszármazott osztályt fogjuk használni az alkalmazásunkban.

Zend_Loader::loadClass('Zend_Controller_Plugin_ErrorHandler');

/**
* ErrorHandler plugin
*
* @author 4image#dev, Vince Tikász
*/
class FI_Controller_Plugin_ErrorHandler extends Zend_Controller_Plugin_ErrorHandler {
/**
* @var Zend_Db_Adapter_Pdo_Sqlite
*/
protected $db;

/**
* @var String
*/
protected $dbName = 'error-log.sqlite3.db';

/**
* @var String
*/
protected $tblName = 'error_log';

protected function initLogDb() {
$logPath  = APPLICATION_PATH . '/log/' . $this->dbName;
$this->db = new Zend_Db_Adapter_Pdo_Sqlite(array('dbname' => $logPath));
$desc     = $this->db->describeTable($this->tblName);

if (count($desc) == 0) {
    // Ha nem létezik, létrehozzok a táblát
    $this->db->query('CREATE TABLE IF NOT EXISTS ' . $this->tblName . ' (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        session_id char(32),
        user_id INTEGER default NULL,
        user_datas text,
        ip_address varchar(45) default NULL,
        request_uri text,
        path text,
        url text,
        hostname varchar(100) default NULL,
        timestamp timestamp NOT NULL default CURRENT_TIMESTAMP,
        method varchar(45) default NULL,
        request blob,
        exceptions blob
    )');
}
}

/**
* Filter sensitive params
* @param Array $array
* @retunr Array
*/
protected function filterParams($array = array()) {
$filtered = array();

foreach((array) $array as $k => $v) {
    if (is_array($v)) {
        $filtered[$k] = $this->filterParams($v);
    } else {
        switch($k) {
            // soroljuk fel ide a jelszómezőknél használt neveket
            case 'password':
            case 'passwrd':
                $filtered[$k] = '[keep-in-secret]';
                break;
            // itt pedig a jelszó megerősítő mezők neveit
            case 'password-confirm':
                $same = $v === $array['password'];
                $filtered[$k] = '[keep-in-secret(' . ($same ? '' : 'not-') . 'match)]';
                break;
        }
    }
}

return $filtered;
}

protected function writeLog() {
$this->intiLogDb();
$user_id = Zend_Auth::getInstance()->hasIdentity() ? Zend_Auth::getInstance()->getIdentity()->id : 0;

$this->db->insert($this->tblName, array(
    'session_id'  => Zend_Session::getId(),
    'user_id'     => $user_id,
    'user_datas'  => serialize(Zend_Auth::getInstance()->getIdentity()),
    'ip_address'  => $this->_request->getServer('REMOTE_ADDR'),
    'request_uri' => $this->_request->getServer('REQUEST_URI'),
    
    'path' =>
        $this->_request->getModuleName() . '@' .
        $this->_request->getControllerName() . '/' .
        $this->_request->getActionName(),
        
    'url' =>
        $this->_request->getScheme() . '://' .
        $this->_request->getHttpHost() .
        $this->_request->getRequestUri(),
        
    'hostname'  => $this->_request->getServer('HTTP_HOST'),
    'timestamp' => date('Y-m-d H:i:s'),
    'method'    => $this->_request->getMethod(),
    
    'request' => serialize( array(
        'SERVER'  => $this->_request->getServer(),
        'POST'    => $this->filterParams($this->_request->getPost()),
        'COOKIE'  => $this->filterParams($this->_request->getCookie()),
        'params'  => $this->filterParams($this->_request->getParams()),
        'request' => $this->filterParams($this->getRequest())
    )),
    
    'exceptions' => serialize($this->_response->getException()),
));

$this->db->closeConnection();
}

/**
* Post dispatch
* @param Zend_Controller_Request_Abstract $request
*/
public function postDispatch(Zend_Controller_Request_Abstract $request) {
   parent::postDispatch();
   // csak abban az esetben kell naplót írni, ha hiba van
   if ( $this->getResponse()->isException() ) {
       $this->writeLog();
   }
}

A plugin betöltése most is a szokásos módon, a Bootstrap osztályban történik.


class Bootstrap extends Zend_Application_Bootstrap_Bootstrap {
   protected function _initPlugins() {
      Zend_Controller_Front::getInstance()
       ->registerPlugin(new FI_Controller_Plugin_ErrorHandler());
   }
}

A hozzáférési napló írását a lehető legkorábban futtatjuk, a hibanaplóét pedig a lehető legkésőbb, ezért ezúttal a postDispatch() pontra kerül. Első lépésben hagyjuk, hogy az ősosztály kezelje a hibát. Akit érdekel, megvizsgálhatja, pontosan hogyan is szedi össze, és adja át a hiba adatait az alkalmazásunk ErrorController-jének. A beépített mechanizmus futását követően létrehozzuk a naplóként használt SQLite adatbázist és benne a szükséges táblát. Záró lépésként pedig elkészítjük a naplóbejegyzést.

Nagyjából ide lehet érdemes beépíteni egy e-mail küldést is, amiben értesíthetjük magunkat az új hibákról. Az ErrorController-hez tartozó sablonban az aktuális felhasználó jogaittól függően akár meg is jeleníthetjük a hiba részleteit.

Az előző cikk hozzászólásai közt Tyrael felhívta a figyelmet az érzékeny adatok védelmének fontosságára. A legegyszerűbb megoldás, hogy a kérés azon részeit, amelyek érzékenynek minősülnek, egyszerűen helyettesítjük valamilyen szöveggel. Azért nem töröljük a tömbből, mert lehetséges, hogy a paraméter megléte vagy hiánya ugyanolyan fontos tényező a hiba kiváltásában. Jelen példában csak a jelszó, valamint a jelszó megerősítő mezőt szűröm meg, de a filterParams() metódus tovább bővíthető saját igényeknek megfelelően. Nyílván lehetne fokozni a védelmet, illetve a szűrendő mezők megadása történhetne például konfigurációs állománnyal, de itt a cél az alapvető logika prezentálása.

A következő cikkben megmutatom, hogyan tudunk egy olyan felületet készíteni, amin keresztül a naplókat lehet vizsgálni.

 
tiku I tikaszvince arcképe
tiku I tikaszvince
Tikász Vince végzettsége szerint könyvtáros, a szemantikus és
hozzáférhető web elkötelezett híve. 2005 óta hivatásszerűen
foglalkozik webes fejlesztéssel. Jelenleg a Full Market Zrt frontend fejlesztője, valamint a 4image Kft „bedolgozó” fejlesztője. Korábban a WebShop-Experts Kft. vezető fejlesztője.
1

Na most vagy az van, hogy

Protezis · 2011. Feb. 1. (K), 00.00
Na most vagy az van, hogy késő van már és nem látom a fától az erdőt, vagy az, hogy a pluginod minden postDispatch után nyom egy logot, függetlenül attól, hogy van hiba, vagy nincs.
2

mea culpa

tiku I tikaszvince · 2011. Feb. 1. (K), 09.18
rossz kódot írtam a :( A "túl takarítás" esete áll fenn; az egyébként használt kódomból nem csak az azokat a részeket sikerült kiszedni, amik itt feleslegesek

A kódban szereplő postDispatch függvény helyesen a következően néz ki

/**
 * Post dispatch
 * @param Zend_Controller_Request_Abstract $request
 */
public function postDispatch(Zend_Controller_Request_Abstract $request) {
   parent::postDispatch();
   // csak abban az esetben kell naplót írni, ha hiba van
   if ( $this->getResponse()->isException() ) {
       $this->writeLog();
   }
}
3

Javítottam a cikkben.

Török Gábor · 2011. Feb. 1. (K), 10.26
Javítottam a cikkben.
4

Kérdés: A lazy init miatt nem

inf3rno · 2011. Júl. 12. (K), 00.25
Kérdés: A lazy init miatt nem lett volna jobb külön osztályba tenni a kód nagyrészét, a plugint pedig a lehető legkisebbre venni?