Da Dove Nasce: Due Piattaforme, Stesso Problema
Tutto è partito da due progetti completamente diversi che mi hanno fatto la stessa richiesta: “Vogliamo sapere cosa fanno gli utenti nella piattaforma”.
Il primo era una piattaforma didattica. Volevano tracciare il progresso degli studenti: quali lezioni avevano aperto, quando avevano completato un quiz, quanto tempo passavano su ciascun contenuto. Non per fare il Grande Fratello, ma per capire dove gli studenti si perdevano e migliorare i materiali.
Il secondo era un sistema documentale per una PA. Qui la richiesta era più seria: tracciare le responsabilità. Chi aveva aperto un documento? Chi lo aveva modificato? Chi lo aveva approvato? Audit trail completo, perché quando le cose vanno male qualcuno deve poter rispondere.
In entrambi i casi i log file tradizionali di Laravel erano totalmente inadeguati. Serviva qualcosa di queryabile, relazionale, e soprattutto che funzionasse come dicevo io. Quindi mi sono imbarcato nell’impresa di scrivere una delle librerie che ho più riutilizzato nei progetti successivi.
Il Problema: Log File vs Database
I log file vanno benissimo per il debugging. Apri storage/logs/laravel.log, cerchi l’errore, capisci cosa è successo, risolvi. Ma quando ti servono per business intelligence o audit trail, sono un disastro:
- Stringhe di testo non strutturate
- Impossibile fare query complesse, grep va bene al volo ma poi tanti auguri
- Nessuna relazione con le entities del tuo sistema
- Retention policy? Te la gestisci tu a mano
- Performance che degrada quando i file diventano enormi
La soluzione è ovvia: database-driven logging. Activity log come entità first-class, con tutte le relazioni del caso.
Diary: Architecture Overview
Ho chiamato il pattern “Diary” perché fondamentalmente è un diario di bordo dell’applicazione. Ogni riga è un’attività loggata.
// Model Diary
- user_id: chi ha fatto l'azione
- type: evento (configurabile)
- diarized_type, diarized_id: polymorphic relation
- data: JSON per metadati custom
- from, to: datetime range (per eventi che hanno una durata)
- url: URL della richiesta
- created_at, updated_at
Ovviamente è una polymorphic relation: così posso attaccarla a un qualsiasi model e non. Un solo model Diary può loggare qualsiasi entity del tuo sistema. User, Order, Document, Post, quello che vuoi. Attacchi il trait e funziona.
Pattern 1: Trait Diarizable
L’idea è che aggiungere logging a un model deve essere banale. Trait, done.
use App\Models\Traits\Diarizable;
class Order extends Model
{
use Diarizable;
}
A questo punto hai auto-logging su update e delete (via model events):
public static function boot()
{
parent::boot();
static::deleting(function ($model) {
$model->diary('delete');
});
static::updating(function ($model) {
// Logga solo i campi che sono stati modificati
$model->diary('update', $model->getDirty());
});
}
E puoi anche fare manual logging quando ti serve:
$order->diary('shipped', [
'tracking_number' => 'ABC123',
'carrier' => 'DHL'
]);
La cosa bella del polymorphic è che funziona anche su cose che non sono model. Puoi loggare da un Command, da un Job, da dove ti pare. Basta che passi l’istanza giusta.
Pattern 2: Type System Configurabile
Ovviamente le label vanno in un file di config, perché siamo delle persone decenti, non certo dei grezzi frontend developer che hardcodano stringhe a caso.
config/diary.php:
return [
'type' => [
'order' => [
'created' => ['label' => 'Ordine creato'],
'shipped' => ['label' => 'Ordine spedito'],
'delivered' => ['label' => 'Ordine consegnato'],
],
'user' => [
'login' => ['label' => 'Login effettuato'],
'password_reset' => ['label' => 'Password reimpostata'],
],
],
];
Centralizzato, human-readable, facilmente estendibile. Quando devi aggiungere un nuovo tipo di evento modifichi qui, non vai a caccia di stringhe sparse in 20 file diversi.
Pattern 3: Query & Analytics
Qui è dove il database-driven logging si paga tutto. Vuoi sapere cosa ha fatto un utente? Query. Vuoi la storia completa di un ordine? Query. Vuoi contare quanti ordini sono stati spediti questa settimana? Query.
Tutti i log di un utente:
Diary::where('user_id', $userId)
->with('diarizable')
->latest()
->get();
Tutti i log di un ordine:
$order->diaries()->get();
Eventi per tipo:
Diary::where('type', 'order.shipped')
->whereBetween('created_at', [$start, $end])
->count();
Join con entity loggata (polymorphic):
Diary::where('diarized_type', Order::class)
->get()
->each(fn($d) => $d->diarizable->customer_name);
Nella piattaforma didattica usavamo queste query per generare report sul progresso degli studenti. Nel sistema documentale per rispondere a richieste tipo “mostrami tutti i documenti aperti dall’utente X negli ultimi 30 giorni”. Con i log file? Buona fortuna.
Pattern 4: Retention Policy
Quando ho fatto il primo deploy del sistema documentale, non avevo messo retention policy. Pensavo “vabbè, vedremo”. Dopo qualche mese la tabella diaries stava crescendo abbastanza velocemente. Non era ancora un problema, ma l’ho aggiunto lo stesso per principio.
// Elimina log più vecchi di 6 mesi
Diary::deleteOlderThan(month: 6);
Scheduled command in app/Console/Kernel.php:
$schedule->call(function () {
Diary::deleteOlderThan(6);
})->monthly();
Oltre a tenere sotto controllo la dimensione del database, ti aiuta anche con il GDPR. Se devi cancellare i dati di un utente dopo N mesi, ce l’hai già pronto.
Pattern 5: Dual Logging
Una cosa che mi piace di questo pattern è che non devi scegliere tra database e log file. Puoi avere entrambi.
public function diary(string $type, $data = null): Diary
{
$diary = new Diary();
// ... save to database
// ANCHE log file per debugging
logger()->info('Diary::' . $type, [
'user_id' => $diary->user_id,
'model_class' => get_class($this),
'model_id' => $this->id,
'data' => $data,
]);
return $diary;
}
Database per le query e l’audit trail. Log file per quando stai debuggando in real-time e vuoi vedere cosa sta succedendo. Best of both worlds.
Use Cases Reali
1. Audit Trail (Sistema Documentale)
“Chi ha modificato questo documento e quando?”
$document->diaries()
->where('type', 'LIKE', '%.update')
->get();
Risposta in 10ms, con tutti i dettagli. Nome utente, timestamp, campi modificati (nel JSON data).
2. User Activity Timeline (Piattaforma Didattica)
“Cosa ha fatto questo studente negli ultimi 7 giorni?”
Diary::where('user_id', $userId)
->whereBetween('created_at', [now()->subDays(7), now()])
->with('diarizable')
->get();
Lezioni aperte, quiz completati, documenti scaricati. Tutto in una query.
3. Business Analytics
“Quanti ordini spediti questa settimana?”
Diary::where('type', 'order.shipped')
->whereBetween('created_at', [now()->startOfWeek(), now()])
->count();
Semplice, veloce, preciso.
Quando Usarlo vs Log File
Non è che i log file siano inutili. Hanno il loro posto. La regola è abbastanza semplice:
Usa Diary (DB) quando:
- Serve queryare i log
- Audit trail richiesto (compliance, GDPR, etc.)
- Analytics su azioni utente
- Relazioni con entities del sistema
- UI per visualizzare activity log
Usa log file quando:
- Debugging developer
- Error tracking
- Performance profiling
- Non serve persistence a lungo termine
Best practice: Usa entrambi. Dual logging pattern, come mostrato sopra.
Performance Considerations
Se stai loggando migliaia di eventi al giorno (o anche di più), devi pensare alla performance.
Index essenziali (senza questi sei morto):
$table->index('user_id');
$table->index('type');
$table->index(['diarized_type', 'diarized_id']);
$table->index('created_at');
Partitioning (opzionale, solo se hai volumi davvero alti):
-- Partition by month
ALTER TABLE diaries PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at));
Async logging (se l’overhead è troppo):
// Dispatch job invece di save sincrono
dispatch(new LogDiaryEvent($model, $type, $data));
Io personalmente uso il sync logging per la maggior parte dei casi. L’overhead è minimo se hai gli index giusti, e non rischi di perdere log se il job fallisce.
Conclusioni
Diary è un pattern abbastanza semplice ma che risolve un problema reale. L’ho usato in almeno 4-5 progetti diversi ormai, e ogni volta mi risparmia un sacco di tempo.
✅ Polymorphic (1 model, N entities) ✅ Type system configurabile ✅ Auto-tracking via model events ✅ Queryable & relational ✅ Retention policy built-in ✅ Dual logging (DB + file)
Aggiungi il trait Diarizable, configuri i types, e sei pronto. Da zero a audit trail completo in meno di un giorno.
Pattern testato in production con milioni di record. Scalabile, maintainable, GDPR-compliant.