OpenStats - logování informací

Pokud vás projekt OpenStats zaujal a nainstalovali jste si ho (viz. instalace a konfigurace), není nic jednoduššího než si vyzkoušet zalogování několika sessions, akcí a parametrů ;-)

Logovací knihovna je tvořena jedinou třídou - OpenStats_Logging, využívající knihovnu OpenStats_DB (a související třídu OpenStats_DB_ResultSet) která pouze zapouzdřuje přístup k databázi a je společná pro všechny části projektu OpenStats.

Podívejme se tedy na jednotlivé třídy - v následujícím textu naleznete pouze stručný popis jednotlivých metod, budete-li potřebovat podrobnější informace, nahlédněte přímo do komentářů v knihovně.

OpenStats_DB

Vzhledem k tomu že projekt OpenStats vznikl ještě před tím než se v PHP objevilo PDO, napsali jsme si vlastní vrstvu která měla za úkol odstiňovat aplikační kód od volání databáze - součástí této vrstvy je také následující třída (OpenStats_DB_ResultSet).

Tato vrstva není zodpovědná za vytváření nebo uzavírání DB spojení - jen a pouze provádí DB dotazy, a případně vytváří a uzavírá transakce.

Součástí projektu je pouze PostgreSQL varianta této vrstvy, nicméně díky tomuto rozdělení je relativně jednoduché portovat projekt i na jiné databáze (například MySQL).

Veřejné metody třídy jsou:

  • __construct($connection) - konstruktor, parametrem je spojení na databázi
  • query($query) - spustí dotaz a výsledek vrátí zabalený jako OpenStats_DB_ResultSet
  • execute($query) - spustí dotaz, a vrátí pouze počet upravených řádek
  • begin() - vytvoří novou transakci
  • commit() - potvrdí transakci
  • rollback() - zruší transakci
  • escape($value) - ošetří hodnotu pro použití v SQL dotazu
  • getLastId($sequence) - vrátí poslední použitou hodnotu ze sekvence
  • getNextId($sequence)  - vrátí následující hodnotu ze sekvence

Varování: Tato knihovna sice do jisté míry odstiňuje aplikační kód od vlastního volání databáze, v žádném případě se však nejedná o odstínění kompletní. Nelze očekávat že pro portování na jinou databázi (např. MySQL) stačí přepsat pouze tuto třídu - stále existuje mnoho implementačních specifik která tato třída neřeší. Mezi hlavní patří rozdíly v podporovaných kontrukcích SQL (a jejich optimálnosti na různých databázích - co běží rychle na jedné databázi může na jiné běžet pomalu), práci se sekvencemi (např. MySQL namísto nich používá AUTOINCREMENT), apod.

Note: One of the possibilities provided by this encapsulation is logging of SQL queries along with their duration - see the PGMon project.

OpenStats_DB_ResultSet

Tato třída zapouzdřuje výsledky dotazů (SELECT) jako iterátor. Díky tomu není aplikační kód závislý na konkrétní databázi a lze ho tak jednoduše portovat i na jiné databáze.

Veřejné metody třídy jsou:

  • __construct($result) - konstruktor, obalí výsledek dotazu
  • hasNext() - je ve výsledku ještě alespoň jeden řádek
  • fetchNext($mapper = null) - vrátí další řádek z výsledku (případně objekt vytvořený mapperem)
  • free() - uvolní výsledek z paměti

OpenStats_Logging

Tato knihovna je zodpovědná za samotné samotné logování statistik - vytváření sezení (sessions), akcí náležících do těchto sezení, a parametrů pro jednotlivé akce.

Ve svém aplikačním kódu budete aktivně pracovat pouze s touto třídou (pro předchozí dvě třídy budete pouze vytvářet instance).

Veřejné metody třídy jsou:

  • __construct($db) - konstruktor, parametrem je instance třídy OpenStats_DB
  • getVisitorId() - vrátí náhodně vygenerované ID návštěvníka
  • createSession($visitorId, $ip, $forwardedFor, $referer, $userAgent, $language) - vytvoří session
  • createAction($sessionId, $language, $typeId, $pageId, $parameters = null) - vytvoří akci
  • createParameter($sessionId, $actionId, $name, $value) - vytvoří parametr
  • createParameters($sessionId, $actionId, $parameters) - vytvoří několik parametrů najednou

Základní použití

Následující kus kódu předvádí vytvoření potřebných instancí tříd, vytvoření nové session, vytvoření jedné akce v rámci session, a následně zalogování několika parametrů pro tuto akci:

<?php

    // načtení potřebných knihoven
    require_once('openstats/openstats_logging.php');

    // spuštění session
    session_start();

    // definice užitečných konstant
    define('TYPE_PAGEVIEW', 1);
    define('PAGE_HOMEPAGE', 1);

    // inicializace proměnných
    $visitorId    = null;
    $ip           = $_SERVER['REMOTE_ADDR'];
    $forwardedFor = $_SERVER['HTTP_X_FORWARDED_FOR'];
    $referer      = $_SERVER['HTTP_REFERER'];
    $userAgent    = $_SERVER['HTTP_USER_AGENT'];
    $language     = $_SERVER['HTTP_ACCEPT_LANGUAGE'];

    // vytvoření instance DB vrstvy
    $conn = pg_open(...);
    $db = new OpenStats_DB($conn);

    // vytvoření instance logovací knihovny
    $stats = new OpenStats_Logging($db);

    // načtení předchozí hodnoty visitorId z COOKIE
    if (isset($_COOKIE['visitorId'])) {
        $visitorId = $_COOKIE['visitorId'];
    } else {
        $visitorId = $stats->getVisitorId();
        setcookie('visitorId', $visitorId, time() + 31*24*60*60);
    }

    // zalogování session (pokud je to třeba)
    if (! isset($_SESSION['sessionId'])) {
        $sessionId = $stats->createSession($visitorId, $ip, $forwardedFor,
                                           $referer, $userAgent, $language);
        $_SESSION['sessionId'] = $sessionId;
    } else {
        $sessionId = $_SESSION['sessionId'];
    }

    // zalogování akce
    $actionId = $stats->createAction($sessionId, 'cs',
                                     TYPE_PAGEVIEW, PAGE_HOMEPAGE);

    // zalogování parametru
    $parameterId = $stats->createParameter($sessionId, $actionId,
                                           'myParameter', 'my value');

?>

Tento příklad poměrně dobře ilustruje použití logovací knihovny - věřím že nebude příliš obtížné přizpůsobit ho vašim potřebám. V zájmu stručnosti jsem si však dovolil několik zjednodušení, která bych v následujícím odstavci rád uvedl na pravou míru.

Několik doporučení

Zaprvé některé proměnné načítané z pole $_SERVER (zejména se to týká HTTP_ proměnných) nemusí být definovány, a tudíž se v logu (nebo jako součást HTML výstupu pokud nedej bože nemáte definován vlastní error handler) může objevit hláška o jejich neexistenci. Nejedná se sice o fatální chybu - kód bude fungovat - ale je to přinejmenším otravné.

Lepší ošetření této situace by tak mohlo vypadat například následovně:

<?php

  $referer = (isset($_SERVER['HTTP_REFERER'])) ? $_SERVER['HTTP_REFERER'] : null;

?>

Zadruhé je vhodné lépe ošetřit hodnotu načtenou z cookie - výše uvedený kód je zranitelný, neboť pro útočníka není obtížné podstrčit libovolnou hodnotu. Sice nezpůsobí žádnou výraznou škodu - při nejhorším poněkud zmate detekci unikátních návštěvníků - ale proč to neošetřit, že?

Jednou z možností je neposílat pouze ID návštěvníka, ale doplnit ho o hodnotu která znemožní jeho úpravu, například hash s využitím salt hodnoty (a tuto hodnotu při načítání zkontrolovat):

<?php

    // definice salt hodnoty (tajná hodnota)
    define('SALT', '3s8f9df');

    // načtení předchozí hodnoty visitorId z COOKIE
    if (isset($_COOKIE['visitorId'])) {
        $tmp = explode(':', $_COOKIE['visitorId]);
        if ($tmp[1] == md5(SALT . $tmp[0]) {
            $visitorId = $tmp[0];
        }
    }

    // pokud ID návštěvníka není načteno, vygeneruj nové
    if (! isset($_COOKIE['visitorId'])) {
        $visitorId = $stats->getVisitorId();
        $cookieValue = $visitorId . ':' . md5(SALT . $visitorId);
        setcookie('visitorId', $cookieValue, time() + 31*24*60*60);
    }

?>

Další možností je šifrovat hodnotu (ale i tak je třeba kontrolovat zda hodnota nebyla změněna "naslepo").

A poslední doporučení - při volání metody createAction nepoužívejte přímo numerické hodnoty, neboť je velmi obtížné se v takto zadaných hodnotách zorientovat. Raději v samostatném souboru definujte konstanty pro stránky a typy akcí, a následně používejte tyto konstanty.

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)