Logovací vrstva chování uživatelů s využitím dibi

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Marsme
Člen | 75
+
0
-

Potřebuji napsat logovací vrstvu nad dibi. Potřebuji logovat akce uživatelů skrze celou aplikaci. Poradíte nějaké best Practice jak to realizovat ?

hrach
Člen | 1834
+
0
-

Jo, nelogovat do relacni db pres dibi.

Marsme
Člen | 75
+
0
-

Jak to řeší můj problém ? Logovat potrebuji… S tím ze na tom projektu je dibi nic neudělám…

mkoubik
Člen | 728
+
0
-

Pokud opravdu chceš logovat do relační databáze, tak bude nejlepší si jednotlivé záznamy strkat v nějaké službě do pole a v $application->onShutdown[] to jedním multi insertem zapsat do db.
Lepší by ale asi bylo pužít nějaké hloupé, rychlé úložiště, třeba redis.

Ot@s
Backer | 476
+
0
-

Já to řeším tak, že mám modelový objekt. Ten funguje jako obálka nad funkcionalitou dibi knihovny (nebo jakékoli jiné). Tj. mám v ní vlastní obecné metody pro insert, update a delete (+ další). Kromě jiného si v těchto metodách zajišťuji i logování. Mám tam samo i metodu, která nastaví popisku celé transakci, takže více SQL zaloguje jako jednu operaci, viz. třeba vazby n:m, či hromadné operace. Logování provádím do vyhrazené tabulky, která nad sebou nemá žádné indexy (pro rychlý INSERT). Dále nad touto tabulkou cronem zajišťuji rotaci dat po 3 měsících (=data starší 3 měsíců archivuji v CSV pro případné pozdější dohledání).

Marsme
Člen | 75
+
0
-

Ot@s:

máš někde tu vrstvu dostupnou na gitu? Nad tímhle řešením jsem taky uvažoval akorát s tím že bych asi data čistil z db po měsících. Ve většině případů chce „majitel“ webu vědět kdo mu tam co udělal v rámci 2–3 dnů max týdne takže měsíční rezerva je víc než dostačující. Navíc zbytek záznamů pak s nějakým rozumným exportem např do toho csv nebo čehokoliv jiného bude opět dohledatelný.

Ot@s
Backer | 476
+
0
-

Marsme napsal(a):
máš někde tu vrstvu dostupnou na gitu?

Bohužel nemám, je to rok starý komerční projekt. Navíc, dnes bych tu třídu napsal malinko jinak (princip ale platí). Moje řešení bylo z důvodu mojí lenosti více zaměřené na automatické logování událostí (úroveň „base“ objektů modelu) činnosti v databázi, takže pokud jsi třeba chtěl zalogovat něco přímo z presenteru (třeba událost v handleru), tak to nešlo. V praxi to ale až tak moc nevadilo, protože to většinou šlo opět přes nějakou tu metodu modelu.

Navíc píšeš, že to chceš aplikovat nad existujícím projektem, takže bude dost záležet na míře použité abstrakce v projektu. Jestli máš přímo v presenteru kostrukce typu dibi::select(...)->from(...), tak se zásadnějšímu přepisu aplikace stejně nevyhneš.

Tharos
Člen | 1030
+
0
-

Jde to realizovat a svým způsobem velice snadno. Tohle používám v jednom projektu:

SQL

CREATE TABLE `sqllog` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `asked` datetime NOT NULL,
  `username` varchar(50) COLLATE utf8_czech_ci NOT NULL,
  `ip` varchar(15) COLLATE utf8_czech_ci NOT NULL,
  `sql` text COLLATE utf8_czech_ci NOT NULL,
  `type` enum('INSERT','UPDATE','DELETE') COLLATE utf8_czech_ci NOT NULL,
  `table` varchar(50) COLLATE utf8_czech_ci DEFAULT NULL,
  `entryId` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `type` (`type`),
  KEY `table` (`table`),
  KEY `entryId` (`entryId`),
  KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

Logger.php

<?php

namespace Util;

use DibiEvent;
use DibiConnection;
use Nette\Utils\Strings;

/**
 * @author Vojtěch Kohout
 */
class Logger
{

	private $connection;

	private $username;

	private $ip;

	private $queue = array();


	function __construct(DibiConnection $connection)
	{
		$this->connection = $connection;
	}

	public function setActorIdentity($username, $ip)
	{
		$this->username = (string) $username;
		$this->ip = (string) $ip;
	}

	public function logEvent(DibiEvent $event)
	{
		if ($this->username === null) {
			throw new \Util\RuntimeException('Actor was not initialized in modifying queries logger.');
		}
		if (($event->type & 56) !== 0) { // is INSERT, UPDATE or DELETE
			if (!$this->isLoggingEvent($event->sql)) {

				switch ($event->type) {
					case DibiEvent::INSERT: $type = 'INSERT'; break;
					case DibiEvent::UPDATE: $type = 'UPDATE'; break;
					case DibiEvent::DELETE: $type = 'DELETE'; break;
				}
				if (isset($type)) {

					$table = $this->parsetTableName($event->sql) ?: '';

					if ($type === 'INSERT') {
						$entryId = $this->connection->insertId;
					} else {
						$entryId = $this->parseEntryId($event->sql) ?: '';
					}

					$this->queue[] = array($this->username, $this->ip, $event->sql, $type, $table, $entryId);
				}

			}
		}
	}

	public function processQueue()
	{
		foreach ($this->queue as $job) {
			$this->connection->query('
				INSERT INTO [sqllog] ([asked], [username], [ip], [sql], [type], [table], [entryId])
				VALUES (NOW(), %s, %s, %s, %s, %sN, %sN)
			', $job[0], $job[1], $job[2], $job[3], $job[4], $job[5]);
		}
	}

	////////////////////
	////////////////////

	private function isLoggingEvent($sql)
	{
		return Strings::startsWith($sql, 'INSERT INTO `sqllog`');
	}

	private function parsetTableName($sql)
	{
		$matches = array();

		preg_match('#^(INSERT INTO|UPDATE|DELETE FROM) `([a-z_]*)`#', $sql, $matches);
		if (array_key_exists(2, $matches)) {
			return $matches[2];
		} else {
			return null;
		}
	}

	private function parseEntryId($sql)
	{
		$matches = array();

		preg_match('#WHERE.*`id` = (\S+)#', $sql, $matches);
		if (array_key_exists(1, $matches)) {
			return $matches[1];
		} else {
			return null;
		}
	}

}

Ke konci v bootstrap.php:

$application->onShutdown[] = function() use ($container) {
	$container->logger->processQueue();
};

Uvnitř startup metody nějakého abstraktního presenteru, ze kterého dědí ty presentery, jejichž aktivitu chceš logovat:

// $connection instanceof DibiConnection
// $logger instanceof Util\Logger
$connection->onEvent[] = function (DibiEvent $event) use ($logger) {
	$logger->logEvent($event);
};

Editoval Tharos (5. 10. 2012 13:30)

Tharos
Člen | 1030
+
0
-

Jinak, kdyby to nebylo zřejmé, loguje to všechny pozměňující dotazy na databázi (pochopitelně kromě jedné aktivity, a to vlastního vložení záznamu do logu). Snaží se to i vždy detekovat, kterého řádku v které tabulce se úprava týkala. To lze snadno u vkládání (podle ID posledního záznamu) a většinou i při úpravě a výmazu.

Celý log se sype do databáze až v $application->onShutdown() proto, protože průběžné vkládání do logu by znemožňovalo v aplikaci používat last insert ID (to by vždy ukazovalo na ID nějakého řádku v logu).