Logování uživatelských akcí

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

Neřešil někdo z vás jak krásně a přehledně logovat uživatelské akci v aplikaci postavené na nette? Jde mě třeba o info „User XY vytvořil článek XX“, „User XY uzavřel úkol ZZ“ atd?
V předchozí non-nette aplikaci jsem to řešil tak že v modelech kde se prováděly akce co jsem chtěl logovat (save, update,…) jsem si provedl vložení dané informace do DB. Aby to nebylo tak jednoduché, chtěl jsem po tom aby dané informační texty byly přeložitelné a také aby jejich hodnoty byly klikatelné, takže se dalo v hlášce klinout na XX a dalo se dostat na konkrétní položku.

V současné chvíli mám appku založenou na nette + doctrine + úžasné kdyby + méně úžasné gedmo. Díky gedmo můžu logovat nějaké změny entit, ale to spíš slouží pro verzování konkrétních entit, kdy vím kdo, kdy a co změnil.

Napadlo mě využít extension kdyby\events a vytvořit si jednotlivé listenery třeba pro uživatele, články, úkoly a v nich jednotlivé eventy a tyto eventy pak volat v metodách kde se provádí ukládání. Tak třeba v metodě uložení článku se zavolá metoda loggerlisteneru onCreate co provede vložení záznamu do logovací tabulky? A aby tam byla ta funkcionalita přeložitelnosti, tak se to vloží jako řetězec ze slovníku a uložím si informace třeba o tom jak na danou položku vytvořit routu?

Nějaké nápady? Zda uvažuji správným směrem?

greeny
Člen | 405
+
0
-

V jednom starém projektu typu úkolníček jsem používal toto: http://postimg.org/…e/jwret8jzd/ jako inspirace to může posloužit, ale není to tolik obecné, jako potřebuješ.

akadlec
Člen | 1326
+
0
-

@greeny: toto je takový jendoúčelový log, jen ti to loguje zdrojového a cílového uživatele a nějaký text, což v okamžiku kdy to chceš mít jako log napříč aplikací je nepoužitelné.
Jako já to měl už předtím nějak vyřešeno a klidně to aplikuju do netteapplikace, ale spíš mě zajímá zda je vhodné a správné to řešit přes kdyby\events a volat ty logovací eventy na daných místech.

Jan Suchánek
Člen | 404
+
0
-

@akadlec: ja si myslím že určitě v kdyby\events, jen by mě zajímalo co všechno budeš logovat, protože něco se hodí nechat přímo na databázi ne?

Jiří Nápravník
Člen | 710
+
0
-

Než jsem přečetl poslední tvůj odstavec. Tak mě napadlo v podstatě to samé. kdyby\events, které je podle mě na tohle jak dělané.

akadlec
Člen | 1326
+
0
-

@jenicek: co tím myslíš „na databázi“?

Jan Suchánek
Člen | 404
+
0
-

@akadlec: myslel jsem logovat trigerem asi do separe tabulky „_backup“ a v ní přidávat i datum, user_id.
Nebo pomocí kdyby\events a data si serializovat?

Editoval jenicek (1. 4. 2014 17:52)

akadlec
Člen | 1326
+
0
-

hmm triger a nějaké „tvrdé“ insterty nevím nevím, datum a userId si tam dokáži hodit automaticky pomocí listenerů, to není problém.

Co říkáte na to uložit si do jendoho sloupce translate řetězec třeba: „app.userLogs.postCreated“ co by se pak pomocí kdyby\translate přeložilo na „Uživatel: {{username}} vytvořil článek: {{postTitle}}“.
Pak do dalšího sloupce uložit json objekt který bude obsahovat info o uživateli, článku a dalších zůčastněných entitách? A při zobrazení si jednoduše udělat str_replace daných prvků v textu?

kedrigern
Člen | 102
+
0
-

No moc chytrý způsob jsem nikdy nevymyslel. V momentě, kdy začne aplikace růst se najdou případy, kdy chybí data a musíš to hackovat, popřípadě je dat moc.

Ale ten překlad jsem vždycky řešil tím, že jsem do DB ukládal user_id a pak jen „enumy“ (ne vždy přímo DB enum) typu „edit“, „delete“ a to jsem pak někde uměl vypsat v přirozeném jazyku. Ale samozřejmě stále tam habrují cizí klíče (což mě štve, v DB jsem hodně háklivý na pořádek).

Také jsem hodněkrát řešil jak s aktualizací záznamů. V logování by určitě měly být jen změněné sloupce. Ale já to upřímně v appce většinou neřeším (což určitě může být chyba). U textů je to kapitola sama pro sebe. Mít v logu třeba 100× dlouhý text?


Popřípadě máte třeba někdo vícero modulů, které logují nějaké logické celky? Tím se dá dosti optimalizovat jak DB reprezentace logu, tak místo kde to logovat v appce.

mkoubik
Člen | 728
+
0
-

Ideální by bylo logovat čistě domain eventy, ze kterých jde pak vygenerovat stav dat v jakémkoliv čase v minulosti. Samozřejmě ne na každou aplikaci se hodí CQRS ₊ event-sourcing (ve skutečnosti skoro na žádnou), ale je dobré tam i tak mít nějaký stream domain eventů s možností je minimálně logovat do nějakého append-only úložiště (textový soubor s JSONem na každém řádku).
Pokud jsou data v tom eventu moc velká (a duplicitní), tak je dobré ho specifikovat nějak konkrétněji. Např. místo „položka byla upravena“ s tunou různých dat stačí „položka byla přejmenována“, nebo „článek byl publikován“ apod. Je to navíc dobrý cvičení, který tě vede k domain-driven-designu, task-based-ui a vůbec zamyšlení nad tím, proč je daná akce potřeba (i když to chvíli trvá než si člověk zvykne místo AddressWasUpdated psát CustomerHasMoved).

Editoval mkoubik (4. 4. 2014 20:47)

Jan Suchánek
Člen | 404
+
0
-

@mkoubik: Googlil jsem nějaký příklad jak by mohlo logování vypadat podle toho co jsi psal jsem narazil na článek Introduction to Domain Driven Design, CQRS and Event Sourcing je to ono nebo máš lepší příklad?

llsm
Člen | 121
+
0
-

Zdravím, já si zatím vždy vystačil s triggery na databázi + trochu logiky v aplikaci, která do nich dodávala id uživatelů aplikace. Samozřejmě od toho neočekávám, že bych se dokázal vrátit do nějakého předchozího stavu, ale to nepotřebuji. A když pak klient zavolá s problémem, dokážu si v db najít každý stav záznamu a kdo ho provedl.
Vytvoření triggerů je jednoduché, protože jsem si na to napsal skript, který mi vytvoří rovnou příkazy do db podle zdrojové tabulky, takže si jen naklikám tabulky z aktuální databáze a vložím vygenerované SQL do db.

Jan Suchánek
Člen | 404
+
0
-

llsm: O triggeru jsem psal také, a jak psal akadlec není to ono a budeš ukládat víc dat ne?

akadlec
Člen | 1326
+
0
-

tak mě osobně se ten triger vubec nelíbí. to už rovnou můžu využít eventy a nastavit si to v appce. Standardně mám dejme tomu entitu článek a ta obsahuje info o autorovi a editorovi samo s daty. Takže vím kdo článek jako poslední editovat a kdy, ale v základu nevím co v něm editoval. Takže jsem přidal logování (verzování) takže se dokáži vrátit ke konkrétní změně a vím kdo a jakou změnu udělal. Vím třeba že user XY provedl změnu titulku z ABC na CBA a vím kdy to udělal. Tak přemýšlím zda nevyužít těchto informací a udělat si k nim nějaký dekompiler co mě řekne že uživatel XY změnil titulek tehdy a tehdy, že uživatel XZ změnit titule a obsah a stav atd…co?

mkoubik
Člen | 728
+
0
-

@jenicek:

Jo, to je ono. Problém je v tom, že věci okolo DDD/CQRS/ES řeší hlavně komunita kolem Javy a C#, takže ukázek v PHP moc není. Doporučuju třeba skupinu http://dddinphp.org. A slajdy http://www.slideshare.net/…ent-sourcing. Mně osobně trvalo celkem dlouho než jsem se všemi těmi pojmy proklestil, tak radši na úvod shrnu co si pod tím představuju já.

CQRS

Command Query Responsibility Segregation – tohle je buzzword pod který se schovává leccos, ale základní myšlenka je jednoduchá. Při práci s databází bys měl oddělit třídy/vrstvy aplikace/atd. které ten stav mění od těch které ten stav jen načítají.
Důvod je jasný – tyhle dvě zodpovědnosti se tak diametrálně liší, že není rozumné je řešit v jedné třídě. Vzniká tak „read model“ a „write model“ ke kterým se přistupuje odlišně:

Read model
  • Musí být rychlý
  • Kešuje se
  • Zajímají tě data, ne chování → nepotřebuješ objekty → nepoužíváš ORM
  • Chceš jednoduše přidávat pohledy na data aniž bys rozbil byznys logiku
Write model
  • Tady je veškerá byznys logika
  • Udržování konzistence, invarianty, validace
  • Dobře testovatelný nezávisle na úložišti
  • Invalidace cache, eventy

Někteří soudí, že v CQRS bys měl mít oddělené db pro read a pro write, ale základní myšlenka je mnohem jednodušší než ti tvrdí google.

CQRS je základ SOLIDního návrhu → tohle chceš úplně vždycky.

DDD

Domain-driven design. Tohle se týká mnohem víc způsobu komunikace s klientem/product ownerem a definování business logiky než samotného programování. Vyplývají z toho ale některé best-practices pro programování write modelu, které je fajn si osvojit.

  • Doménové objekty jsou rozděleny podle zodpovědnosti do „agregátů“, každý má jednu entitu, která je „aggregate root“.
  • Agregát vždy ručí za svoji vnitřní konzistenci (žádné if ($entity->isValid()) {...}).
  • Všechny operace s agregátem se provádí jako metody aggregate rootu, ostatní entity by neměly mít public metody měnící stav (tady v PHP chybí package-private visibility).
  • Agregát je načtený v paměti vždy celý.

DDD je vyšší dívčí vyplatí se u složitější logiky při spolupráci s domain-expertem → tohle asi chceš, ale chvíli trvá se to naučit.

Event sourcing

Každá metoda aggregate rootu, která mění stav aplikace vygeneruje event (např. „zboží xyz bylo přidáno do košíku). Tyto eventy se serializují a ukládají do logu. Stav aplikace pak není nikde explicitně uložen, ale dá se jednoznačně odvodit ze streamu těch událostí.
Každý "pohled“ z read modelu má denormalizovanou tabulku s daty přesně v tom formátu v jakém je vypisuje + má službu, která odchytává události a podle nich upravuje ta denormalizovaná data.
Např. máme výpis článků na blogu (tabulka se sloupci id, title, perex, author_name, date, comments_count). Projector daného pohledu zachytí událost „přidán komentář“, vytáhne z ní id článku a inkrementuje sloupeček comments_count.
Výhoda je, že se dají ty pohledy přidávat za chodu a dá se pracovat i s informacemi, které by se jinak zahodily (v kterém kroku objednávky nás zákazníci obvykle opouštějí? Přidávají si častěji do košíku nejdřív zubní pastu a pak kartáček, nebo naopak?).
Nevýhoda je spousta infrastrukturního kódu, ke kterému když přijde (i nadprůměrný) PHP programátor, tak na to bude nejspíš koukat jako husa do flašky.
Event sourcing se hodí jen pro specifické případy aplikací a musíš vědět co děláš → to nechceš.

Závěr

Teď když už víme co je to ES, DDD a CQRS, tak můžu znovu formulovat svůj předchozí komentář. I když nejspíš nechceš používat event sourcing, tak je dobré ty události takhle generovat, většinu z nich klidně zahazovat a ty potenciálně užitečné si logovat pro budoucí použití. Minimálně ti to pomůže zpřehlednit jaké vůbec „domain events“ aplikace používá.

Editoval mkoubik (7. 4. 2014 14:50)

Jan Suchánek
Člen | 404
+
0
-

@mkoubik: +++ díky moc, pěkné schrnutí! Zkusím ho vstřebat, ještě jsem narazil na Domain Driven Design Quickly

OT: Jak moc se mohou tyhle eventy lišit od flashMessage? Protože mě přijde že když odpovídám uživateli nějakou rozumnou zprávou, tak bych měl i takovou rozumnou zprávu mít i v logu, nebo v čem by se měli tyhle zprávy lišit a jak moc?

Editoval jenicek (7. 4. 2014 15:44)

llsm
Člen | 121
+
0
-

akadlec napsal(a):

tak mě osobně se ten triger vubec nelíbí. to už rovnou můžu využít eventy a nastavit si to v appce.

To určitě můžeš, ale trigger je na úrovni databáze, tzn. pokud ho jednou správně nastavíš, tak se záznam do db ani nevloží bez toho, aby trigger proběhl.

Standardně mám dejme tomu entitu článek a ta obsahuje info o autorovi a editorovi samo s daty. Takže vím kdo článek jako poslední editovat a kdy, ale v základu nevím co v něm editoval. Takže jsem přidal logování (verzování) takže se dokáži vrátit ke konkrétní změně a vím kdo a jakou změnu udělal. Vím třeba že user XY provedl změnu titulku z ABC na CBA a vím kdy to udělal. Tak přemýšlím zda nevyužít těchto informací a udělat si k nim nějaký dekompiler co mě řekne že uživatel XY změnil titulek tehdy a tehdy, že uživatel XZ změnit titule a obsah a stav atd…co?

Standardně používám triggery, které ukládají kopie aktuálního záznamu před jeho změnou. V praxi mám (jak psal jenicek) tabulku clanky_backup, která je kopií tabulky, kterou chci verzovat s pár parametry navíc (vlastní ID záznamů + datum a čas založení záznamu + uživatel, který akci vyvolal + druh akce (= INSERT, UPDATE, DELETE)). Na originální tabulku pak navážu triggery, které při insertu udělají do kopii záznamu do backup s parametry navíc, to samé při update a před delete. Takže když si pak v této tabulce zobrazím záznamy podle ID nějakého např. článku, tak vidím třeba 10 verzí.
Jediné, co trigger nezvládne udělat bez aplikace, je vložit uživatele, který akci vyvolal. Pokud by ses bez toho obešel, tak ti na samotné verzování stačí jen triggery a do aplikace nemusíš ani sáhnout. A to je jednoduché, pohodlné a snadno automatizovatelné (= levné), pokud to stačí. Má to jen jednu otravnou záležitost, a to kdyz v originální tabulce přidáš/přejmenuješ/odstraníš sloupec, musíš upravit i backup tabulku a trigger. Proto vytvarim backup tabulky s triggery az kdyz jde databaze a appka na produkci, driv stejne verze nepotrebuji.

jenicek napsal(a):

O triggeru jsem psal také, a jak psal akadlec není to ono a budeš ukládat víc dat ne?

Ad není to ono: to je otázkou toho co clovek potrebuje a co si predstavuje, tj. umim si predstavit lepsi resení, ale nepotrebuji ho.
Ad ukladat vic dat: nevim ted s cim srovnavas, ale tim ze jsou data v tabulce, nad kterou primarne nevyhledavas (jen prilezitostne kdyz je problem), tak ti muze byt jejich objem ukradeny, hlavne ze slouzi svemu ucelu. A zatim nemam databaze, kde by to delalo nejaky problem, dneska proste 100000 zaznamu nic neni.

Na závěr jen dodám, že to nikomu nevnucuji, jen říkám že to používám a zatím jsem víc nepotřeboval. Určitě existuje spousta možností, jak si verzovat komplexněji, rozšiřetelněji apod. Ale když verzuji záznamy db, proč to nedělat rovnou v db?

P.S. omlouvám se, jak pořád střídám psaní s diakritikou a bez, nemůžu se toho zbavit a opravovat to po sobě je hrozna otrava… ; )

Jan Suchánek
Člen | 404
+
0
-

@llsm: rozumim tomu, ale přecejen v některých situacích bych ten log rád využíval. Například rád bych zjišťoval, kdo a jak na čem dělá v adminu. Jak se mění ceny, v čem se kdo nejčastěji vrtá. A když to bude rozfrcané po x tabulkách, tak to nebude moc user friendly. Jak bys řešil co si lidi dávají do košíku?

Ale co jsem koukal na Wordpress, tak tam se s verzováním moc neperou post_type je při každém udpate označen jako „revision“ a nový jako „post“ a mají to ve stejné tabulce.

To řešení co popisuje mkoubik je nezávislé na databázi a může logovat i věci, které se nedějí při standardní práci s databází, můžeš logovat i události jiné, například 404, chyby při stahování ceníků atd.

Editoval jenicek (7. 4. 2014 17:52)

Jan Suchánek
Člen | 404
+
0
-

@akadlec: Pěkné, a vše serializuješ nebo používaš json? A přidáváš k tomu popisku nebo tu extrahuješ přímo z těch dat? Vše ukládáš do jedné tabulky log (created, type, user_id, serialized_data) nebo jak? Mě ještě štve že vpodstatě ta popiska může být vpodstatě i vygenerovaný text pro flashMessage nebo ne?

Editoval jenicek (7. 4. 2014 18:04)

llsm
Člen | 121
+
0
-

jenicek napsal(a):

Jak bys řešil co si lidi dávají do košíku?

Na eshopu mam obsah kosiku nekoho v jedne tabulce, ktera je pouhou relaci mezi produktem a zakaznikem + pocet kusu produktu v kosiku, takze to bych z toho dostal zrovna docela v pohode ; )

Ale co jsem koukal na Wordpress, tak tam se s verzováním moc neperou post_type je při každém udpate označen jako „revision“ a nový jako „post“ a mají to ve stejné tabulce.

Toto je pomerne bezny system, u relacnich dat nemuzes (teoreticky) puvodni zaznam smazat, proto ho oznacis za neaktualni a vytvoris v tabulce novy. Tim ti ale samozrejme tabulka bobtna, coz je ale opet problem az pri vyssich poctech radku.

To řešení co popisuje mkoubik je nezávislé na databázi a může logovat i věci, které se nedějí při standardní práci s databází, můžeš logovat i události jiné, například 404, chyby při stahování ceníků atd.

Urcite, jak jsem psal v minulem prispevku: „jen říkám že to používám a zatím jsem víc nepotřeboval“

akadlec
Člen | 1326
+
0
-

@jenicek: hele data se ukladaji serializovaně, popisky jsou jen typu update, create, delete. Je možnost to logovat buď jen do jedné tabulky a nebo pro každou entitu vlastní tabulku. Zatím jsem nepřišel co by bylo výhodnější. Když udělám jednu log tabulku tak ztratím vazbu na záznam v innodb, ale toto zase za mě pořeší doctrine. Eviduje se jak id objektu, username, datum a třídu entity. Jinak je to rozšíření pro doctrine Gedmo, ale celkově není moc dobré, tak se pořád odhodlávám udělat něco přímo pod nette.