Bölüm 1 – Üretim için Laravel Mimari Modelleri
~10 dk okumalı · Uygunluk · Model günlüğü · İstek izleme
Bir işlem kaydı değiştirilmişti.
Tutar, kullanıcının gönderdiğinden farklıydı. Destek durumu yükseltti. Kullanıcı hiçbir şeyi değiştirdiğini yalanladı. Kodu en son dokunan geliştirici izinde idi. Veritabanına baktık — kaydın bir updated_at değeri vardı ve beklenenden farklı bir değerden oluşuyordu. Elimizdeki tek şey buydu.
Kesin kim? Hangi alanlar? Hangi isteğin neden olduğu hakkında hiçbir bağlam yoktu.
Çalışan bir uygulamamız vardı. Ancak hiçbir şeyi hatırlayan bir sistemimiz yoktu.
Bu olay, bunun yapılmasına neden oldu.
Ne Yapacaksınız
Ne Yapacaksınız
- Her istek için otomatik olarak her log satırında görünen bir istek ID’si — manuel işlem gerektirmeden
- Alan bazında model değişiklikleri, sadece güncellenmenin ötesinde, neyin ve nasıl değiştiğini yakalayacak
- Milyonlarca kayıtla hızlı kalmak için segmentlere ayrılmış yalnızca ekleme günlük dosyaları
- Bir güvenlik kapısı bağlamı — izin kontrol hatalarını, olaylar haline gelmeden kaydeden bir rapor
Bir denetim izi gerçekten neyi kanıtlamalı
Bir denetim izi gerçekten neyi kanıtlamalı
Kod yazmadan önce, ne yaptığınızı tam olarak belirlemek önemlidir — zira “denetim izi” farklı bağlamlarda farklı şeyler ifade eder ve gereksinimler mimariyi belirler.
Eğer sorgulanabilir, veritabanı destekli bir denetim günlüğüne ve hazır bir API’ye ihtiyacınız varsa, spatie/laravel-activitylog standart bir tercihtir ve iyi inşa edilmiştir. Aşağıda sunulanlar ise gereksinimleriniz bir adım daha ileri gittiğinde — ekleme garantileri, alan bazında farklar ve gerçekten değiştirilmesi zor denetim kayıtları oluştururken gereklidir.
Uyum gereksinimlerinin yoğun olduğu ortamlarda — fintech, sağlık hizmetleri veya herhangi bir düzenlenmiş alanda — bir denetim izi herhangi bir veri değişikliği hakkında dört soruyu yanıtlamalıdır:
- Kim değişikliği yaptı (kullanıcı ID’si, IP adresi, kullanıcı aracısı)
- Ne değişti — sadece “kayıt güncellendi” değil, hangi alanlar, hangi değerlerden hangi değerlere
- Ne zaman değişti
- Neden güvenilirdir — denetim kaydı kendiliğinden değiştirilemez olmalıdır
Çoğu uygulama ilk üç soruyu karşılar. Dördüncü ise başarısızdır ve gerçek bir denetim için en önemli olanıdır.
Bir veritabanı denetim tablosuyla ilgili problem şudur: uygulama kodunuz ona yazıyor. Bu, uygulama kodunuzun onu UPDATE edebileceği anlamına gelir. Uygulama kodunun değiştirebileceği bir tablo değiştirilebilir bir kayıt değildir — değişen bir geçmiştir. Bir denetçi bunu anladığında, düzenleme yapılmasını nasıl önlediğinizi soracaktır ve “kendi kodumuza güveniyoruz” tatmin edici bir yanıt değildir.
Bunu dosya tabanlı günlüklemenin kararını verme şekli olarak etkiliyor. Ancak önce, daha temel bir problem var: ilişki.
İstek ID’si: Her günlüğe bir ipucu
İstek ID’si: Her günlüğe bir ipucu
Bir model değişikliği yalnızca bir istek sırasında gerçekleşmez — belirli bir kullanıcıdan gelen belirli bir HTTP çağrısında ve belirli bir anda oluşur. Model değişikliğini bu isteğe bağlamadan, zaman damgalı gerçekler arasında hiçbir hikaye olmadan duruyorsunuz.
Çözüm, her istek başında üretilen ve her log satırında yazılan bir istek ID’sidir.
Middleware sınıfını oluşturmadan önce, request ve query kanallarını config/logging.php‘ye ekleyin:
// config/logging.php
'channels' => [
<span class="c1">// ... mevcut kanallarınız ...</span>
<span class="s1">'request'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1">'driver'</span> <span class="o">=></span> <span class="s1">'daily'</span><span class="p">,</span>
<span class="s1">'path'</span> <span class="o">=></span> <span class="nf">storage_path</span><span class="p">(</span><span class="s1>'logs/request.log'</span><span class="p">),</span>
<span class="s1">'level'</span> <span class="o">=></span> <span class="s1>'debug'</span><span class="p">,</span>
<span class="s1">'days'</span> <span class="o">=></span> <span class="mi">90</span><span class="p">,</span> <span class="c1">// 90 gün sakla — uyum gereksiniminize ayarlayın</span>
<span class="p">],</span>
<span class="s1">'query'</span> <span class="o">=></span> <span class="p">[</span>
<span class="s1">'driver'</span> <span class="o">=></span> <span class="s1>'daily'</span><span class="p">,</span>
<span class="s1">'path'</span> <span class="o">=></span> <span class="nf">storage_path</span><span class="p">(</span><span class="s1>'logs/query.log'</span><span class="p">),</span>
<span class="s1">'level'</span> <span class="o">=></span> <span class="s1>'debug'</span><span class="p">,</span>
<span class="s1">'days'</span> <span class="o">=></span> <span class="mi">30</span><span class="p">,</span>
<span class="p">],</span>
<span class="p">],</span>
</code></pre>
Ardından middleware:
class RequestLogger
{
public const HEADER_NAME = ;
public const REQUEST_ID_ATTRIBUTE = ;
<span class="k">public</span> <span class="k">function</span> <span class="n">handle</span><span class="p">(</span><span class="kt">Request</span> <span class="nv">$request</span><span class="p">,</span> <span class="kt">Closure</span> <span class="nv">$next</span><span class="p">):</span> <span class="kt">Response</span>
<span class="p">{</span>
<span class="nv">$requestId</span> <span class="o">=</span> <span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="nc">Str</span><span class="o">::</span><span class="nf">uuid</span><span class="p">();</span>
<span class="c1">// İstek içinde iç erişim için isteğe özelliklerde saklayın</span>
<span class="nv">$request</span><span class="o">-></span><span class="n">attributes</span><span class="o">-></span><span class="nf">set</span><span class="p">(</span><span class="k">self</span><span class="o">::</span><span class="no">REQUEST_ID_ATTRIBUTE</span><span class="p">,</span> <span class="nv">$requestId</span><span class="p">);</span>
<span class="c1">// Ayrıca bir header olarak ayarlayın — aşağı sistemler ve tarayıcı okuyabilir</span>
<span class="nv">$request</span><span class="o">-></span><span class="n">headers</span><span class="o">-></span><span class="nf">set</span><span class="p">(</span><span class="k">self</span><span class="o">::</span><span class="no">HEADER_NAME</span><span class="p">,</span> <span class="nv">$requestId</span><span class="p">);</span>
<span class="c1">// shareContext, her Log:: çağrısını otomatik olarak makes context'e enjekte eder</span>
<span class="c1">// bu isteğe özgü herhangi bir logda işlenir — manuel işlem gerektirmeden</span>
<span class="nc">Log</span><span class="o">::</span><span class="nf">shareContext</span><span class="p">([</span><span class="s1>'request_id'</span> <span class="o">=></span> <span class="nv">$requestId</span><span class="p">]);</span>
<span class="nv">$startedAt</span> <span class="o">=</span> <span class="nb">hrtime</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="c1">// Monotonik saat — microtime()'dan daha doğru</span>
<span class="nv">$response</span> <span class="o">=</span> <span class="nv">$next</span><span class="p">(</span><span class="nv">$request</span><span class="p">);</span>
<span class="nv">$response</span><span class="o">-></span><span class="n">headers</span><span class="o">-></span><span class="nf">set</span><span class="p">(</span><span class="k">self</span><span class="o">::</span><span class="no">HEADER_NAME</span><span class="p">,</span> <span class="nv">$requestId</span><span class="p">);</span>
<span class="nv">$this</span><span class="o">-></span><span class="nf">logRequest</span><span class="p">(</span><span class="nv">$request</span><span class="p">,</span> <span class="nv">$response</span><span class="p">,</span> <span class="nv">$requestId</span><span class="p">,</span> <span class="nv">$startedAt</span><span class="p">);</span>
<span class="k">return</span> <span class="nv">$response</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">private</span> <span class="k">function</span> <span class="n">logRequest</span><span class="p">(</span>
<span class="kt">Request</span> <span class="nv">$request</span><span class="p">,</span>
<span class="kt">Response</span> <span class="nv">$response</span><span class="p">,</span>
<span class="kt">string</span> <span class="nv">$requestId</span><span class="p">,</span>
<span class="kt">int</span> <span class="nv">$startedAt</span>
<span class="p">):</span> <span class="kt">void</span> <span class="p">{</span>
<span class="nc">Log</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1>'request'</span><span class="p">)</span><span class="o">-></span><span class="nf">info</span><span class="p">(</span><span class="s1>'request.completed'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1>'request_id'</span> <span class="o">=></span> <span class="nv">$requestId</span><span class="p">,</span>
<span class="s1>'method'</span> <span class="o">=></span> <span class="nv">$request</span><span class="o">-></span><span class="nf">method</span><span class="p">(),</span>
<span class="s1>'path'</span> <span class="o">=></span> <span class="nv">$request</span><span class="o">-></span><span class="nf">getPathInfo</span><span class="p">(),</span>
<span class="s1>'status'</span> <span class="o">=></span> <span class="nv">$response</span><span class="o">-></span><span class="nf">getStatusCode</span><span class="p">(),</span>
<span class="s1>'duration_ms'</span> <span class="o">=></span> <span class="nb">round</span><span class="p">((</span><span class="nb">hrtime</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="o">-</span> <span class="nv">$startedAt</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1_000_000</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
<span class="s1>'user_id'</span> <span class="o">=></span> <span class="nv">$request</span><span class="o">-></span><span class="nf">user</span><span class="p">()</span><span class="o">?-></span><span class="nf">getAuthIdentifier</span><span class="p">(),</span>
<span class="s1>'ip'</span> <span class="o">=></span> <span class="nv">$request</span><span class="o">-></span><span class="nf">ip</span><span class="p">(),</span>
<span class="p">]);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
Küresel middleware olarak kaydedin:
// Laravel 11 — bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\RequestLogger::class);
})
<span class="c1">// Laravel 10 ve öncesi — app/Http/Kernel.php</span>
<span class="k">protected</span> <span class="nv">$middleware</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1">// ...</span>
<span class="nc">\App\Http\Middleware\RequestLogger</span><span class="o">::</span><span class="n">class</span><span class="p">,</span>
<span class="p">];</span>
</code></pre>
Bu noktadan itibaren uygulamanızdan yazılan her günlük girişi artık istek ID'sini otomatik olarak içerecektir. İşte storage/logs/request.log'de bir istek günlüğü girişi:
{
"message": "request.completed",
"request_id": "9d4e2f1a-83bc-4a7c-b291-7e5f3d9a1c84",
"method": "PATCH",
"path": "/transactions/1101",
"status": 200,
"duration_ms": 43.2,
"user_id": 42,
"ip": "192.168.1.10",
"level": "info",
"level_name": "INFO"
}
Ve bir model değişikliği günlüğü girişi storage/logs/activities/transactions/1100/transaction_1101.log:
{
"event": "updated",
"request_id": "9d4e2f1a-83bc-4a7c-b291-7e5f3d9a1c84",
"time": "2024-03-15 14:32:07",
"model_id": 1101,
"changes": { "amount": { "old": "5000.00", "new": "500.00" }
},
"user_id": 42,
"ip": "192.168.1.10",
"user_agent": "Mozilla/5.0 ..."
}
Aynı request_id her iki dosyada da görünür. Bu, ilişkiyi sağlar. Bir destek bileti "Transaction 1101'de bir şey değişti" dediğinde, o ID'yi her iki kayıt arasında inceleyerek tüm resme hızla ulaşabilirsiniz.
Neden Log::shareContext() yerine ID'yi her yere geçmek yerine? shareContext() verilen veriyi, isteğin geri kalanı boyunca her Log:: çağrısına otomatik olarak enjekte eder. İstek ID'sini hizmetlerinize, model gözlemcilerinize veya Gate kancalarınıza geçmeniz gerekmez. Neyi kaydederseniz kaydedin, istek ID'si zaten orada olacaktır.
Neden rastgele bir tamsayı yerine UUID? Altı haneli rastgele bir tam sayı, eşzamanlı yük altında gerçek çakışma olasılığı taşır. UUID ise 128 bitlik bir rastgelelik sunar — çakışma gerçek bir endişe değildir. Ayrıca, ID yanıt başlığına (X-Request-Id) de eklenir, böylece bir destek bileti açan kullanıcı bunu dahil edebilir ve siz onu birkaç saniye içinde bulabilirsiniz.
Neden cevapsız bir girişi tek bir yapılandırılmış giriş yerine ayrı ayrı istek-in ve yanıt-out girdileri yerine kullanıyoruz? Bir isteğe bir giriş, yöntemi, yolu, durum kodunu ve süresini — bir satırda — çapraz referans olmadan görmenizi sağlar. hrtime(true) monotonik saati, microtime()dan daha doğru bir süre ölçüsü sağlar çünkü sistem saatinin ayarlamalarından etkilenmez.
Bir not: X-Forwarded-For: Uygulamanız bir yük dengeleyici arkasında olduğunda, request()->ip() proxy'nin IP'sini döndürür, kullanıcıya değil. TrustProxies middleware bunu çözer — yapılandırıldığında, request()->ip() gerçek istemci IP'sini döndürür. Bunu harici API çağrılarına geçirin, böylece alt sistem logları da gerçek kullanıcıyı kaydetsin, sunucunuzun adresini değil.
Yavaş sorgular: burada bulunduğunuzda ekleyin
Yavaş sorgular: burada bulunduğunuzda ekleyin
Bu durum denetim izi ile ilgili ayrı bir endişe — ama zaten günlükleme altyapısını kurarken, eklemek neredeyse hiçbir maliyet gerektirmiyor ve bir performans sorunu üretime düştüğünde çok şey kazandırıyor.
Fikir: yavaş sorguları, yavaşlığın ciddiyetine göre eşleşen önem seviyeleriyle kaydedin.
// AppServiceProvider.php
// Bunu üretim dışı ortamlara veya bir yapılandırma bayrağı arkasına işle
// yüksek trafikli sistemlerde — DB::listen her sorguda ateş eder.
if (config() || config()) {
DB::listen(function ($query) {
$time = $query->time;
<span class="k">match</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$time</span> <span class="o">></span> <span class="mi">10000</span> <span class="o">=></span> <span class="nc">Log</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1>'query'</span><span class="p">)</span><span class="o">-></span><span class="nf">critical</span><span class="p">(</span><span class="s2>"Aşırı yavaş (</span><span class="si">{</span><span class="nv">$time</span><span class="si">}</span><span class="s2>ms)"</span><span class="p">),</span>
<span class="nv">$time</span> <span class="o">></span> <span class="mi">1000</span> <span class="o">=></span> <span class="nc">Log</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1>'query'</span><span class="p">)</span><span class="o">-></span><span class="nf">error</span><span class="p">(</span><span class="s2>"Çok yavaş (</span><span class="si">{</span><span class="nv">$time</span><span class="si">}</span><span class="s2>ms)"</span><span class="p">),</span>
<span class="nv">$time</span> <span class="o">></span> <span class="mi">100</span> <span class="o">=></span> <span class="nc">Log</span><span class="o">::</span><span class="nf">channel</span><span class="p">(</span><span class="s1>'query'</span><span class="p">)</span><span class="o">-></span><span class="nf">warning</span><span class="p">(</span><span class="s2>"Yavaş (</span><span class="si">{</span><span class="nv">$time</span><span class="si">}</span><span class="s2>ms)"</span><span class="p">),</span>
<span class="k">default</span> <span class="o">=></span> <span class="kc">null</span><span class="p">,</span> <span class="c1">// Hızlı sorgular gürültü ekler, ancak değer katmaz</span>
<span class="p">};</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre>
Neden süreyi günlük seviyesine eşlemek gerekiyor? Çünkü log seviyeleri bir anlam taşır — ya da taşımalıdır. Eğer her şey info ise, hiçbiri öne çıkmaz. 12 saniyelik bir sorgu critical olarak kaydedilmişse, bu, kritik günlük girdileri için tetiklenen herhangi bir uyarı kuralını harekete geçirir, ek bir izleme kodu yazmadan.
Neden yapılandırma kapısı? DB::listen her bir sorguda ateş eder. Yüksek trafikli bir üretim ortamında, ek yük gerçektir. Bunu app.debug veya özel bir yapılandırma bayrağının arkasına alarak kontrol edebilirsiniz, böylece ne zaman çalıştığını yönetebilirsiniz.
100ms'lik uyarı eşiği, kayıp indeksler ve N+1 problemlerini görselleştirmenin en hızlı yoludur. Loglarda uyarıyı görüyorsunuz, indeksi ekliyorsunuz, uyarı duruyor. Hiçbir kullanıcı şikayet etmedi.
Bu durumda — opsiyoneldir. Eğer bu bölgeden başka bir şey eklemezseniz, denetim izi yine de çalışır. Sorgu dinleyici, kazancını hak eden düşük maliyetli bir ekleme olmuştur, gereksinim değildir.
Model değişiklik günlüğü
Model değişiklik günlüğü
Bir istek ID'si sistemi boyunca geçip gittiğinde, model düzeyindeki değişiklikler izlenebilir bir hikayede anlamlı girişler haline gelir, yalıtılmış veritabanı gerçekleri değil.
Uygulama, denetim gerektiren herhangi bir Eloquent model üzerinde bir trait olarak şöyle olmalı:
trait ModelChangeLogger
{
public static function bootModelChangeLogger(): void
{
static::updating(function ($model) {
$changed = array_diff_assoc(
$model->getAttributes(),
$model->getOriginal()
);
<span class="c1">// updated_at her güncellemede bulunur. O her zaman ilginç değildir.</span>
<span class="k">unset</span><span class="p">(</span><span class="nv">$changed</span><span class="p">[</span><span class="s1>'updated_at'</span><span class="p">]);</span>
<span class="k">if</span> <span class="p">(</span><span class="k">empty</span><span class="p">(</span><span class="nv">$changed</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1">// Alan bazında bir fark oluşturun — yalnızca yeni değerleri değil,</span>
<span class="c1">// her alan *nereden* neye değişti</span>
<span class="nv">$diff</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">foreach</span><span class="p">(</span><span class="nv">$changed</span> <span class="k">as</span> <span class="nv">$key</span> <span class="o">=></span> <span class="nv">$newValue</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$diff</span><span class="p">[</span><span class="nv">$key</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1>'old'</span> <span class="o">=></span> <span class="nv">$model</span><span class="o">-></span><span class="nf">getOriginal</span><span class="p">(</span><span class="nv">$key</span><span class="p">)</span> <span class="o">??</span> <span class="s1>'N/A'</span><span class="p">,</span>
<span class="s1>'new'</span> <span class="o">=></span> <span class="nv">$newValue</span><span class="p">,</span>
<span class="p">];</span>
<span class="p">}</span>
<span class="nv">$model</span><span class="o">-></span><span class="nf">logChanges</span><span class="p">(</span><span class="nv">$diff</span><span class="p">,</span> <span class="s1>'updated'</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">static</span><span class="o">::</span><span class="nf">created</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="nv">$model</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$model</span><span class="o">-></span><span class="nf">logChanges</span><span class="p">(</span><span class="nv">$model</span><span class="o">-></span><span class="nf">getAttributes</span><span class="p">(),</span> <span class="s1>'created'</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">static</span><span class="o">::</span><span class="nf">deleting</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="nv">$model</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Silinmeden önce tam durumu yakalayın — silindikten sonra,</span>
<span class="c1">// getAttributes() hiçbir yararlı şey döndürmez</span>
<span class="nv">$model</span><span class="o">-></span><span class="nf">logChanges</span><span class="p">(</span><span class="nv">$model</span><span class="o">-></span><span class="nf">getAttributes</span><span class="p">(),</span> <span class="s1>'deleted'</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="k">protected</span> <span class="k">function</span> <span class="n">prepareLogData</span><span class="p">(</span><span class="kt">array</span> <span class="nv">$changes</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$event</span><span class="p">):</span> <span class="kt">array</span>
<span class="p">{</span>
<span class="c1">// Hassas alanları yazmadan önce gizleyin — neyin değiştiğini değil,</span>
<span class="c1">// neyin değiştiğini kaydedin</span>
<span class="k">foreach</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="n">maskedAttributes</span> <span class="o">??</span> <span class="p">[]</span> <span class="k">as</span> <span class="nv">$attr</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">isset</span><span class="p">(</span><span class="nv">$changes</span><span class="p">[</span><span class="nv">$attr</span><span class="p">]))</span> <span class="p">{</span>
<span class="nv">$changes</span><span class="p">[</span><span class="nv">$attr</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1>'old'</span> <span class="o">=></span> <span class="s1>'[REDACTED]'</span><span class="p">,</span>
<span class="s1>'new'</span> <span class="o">=></span> <span class="s1>'[REDACTED]'</span><span class="p">];</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">[</span>
<span class="s1>'event'</span> <span class="o">=></span> <span class="nv">$event</span><span class="p">,</span>
<span class="s1>'request_id'</span> <span class="o">=></span> <span class="nf">request</span><span class="p">()</span><span class="o">-></span><span class="n">attributes</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="nc">RequestLogger</span><span class="o">::</span><span class="no">REQUEST_ID_ATTRIBUTE</span><span class="p">,</span> <span class="s1>'N/A'</span><span class="p">),</span>
<span class="s1>'time'</span> <span class="o">=></span> <span class="nf">now</span><span class="p">()</span><span class="o">-></span><span class="nf">toDateTimeString</span><span class="p">(),</span>
<span class="s1>'model_id'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getKey</span><span class="p">(),</span>
<span class="s1>'changes'</span> <span class="o">=></span> <span class="nv">$changes</span><span class="p">,</span>
<span class="s1>'user_id'</span> <span class="o">=></span> <span class="nf">auth</span><span class="p">()</span><span class="o">-></span><span class="nf">id</span><span class="p">(),</span>
<span class="s1>'ip'</span> <span class="o">=></span> <span class="nf">request</span><span class="p">()</span><span class="o">-></span><span class="nf">ip</span><span class="p">(),</span>
<span class="s1>'user_agent'</span> <span class="o">=></span> <span class="nf">request</span><span class="p">()</span><span class="o">-></span><span class="nf">userAgent</span><span class="p">(),</span>
<span class="p">];</span>
<span class="p">}</span>
<span class="c1">// Her modelda, düz metin olarak görünmemesi gereken alanları listelemek için geçersiz kılınması gerekir</span>
<span class="k">protected</span> <span class="kt">array</span> <span class="nv">$maskedAttributes</span> <span class="o">=</span> <span class="p">[];</span>
<span class="p">}</span>
</code></pre>
Bir not: konsol bağlamı:
ModelChangeLoggerbir kuyruk işi veya zamanlanmış bir komut içinde çalıştığında,request()->attributes->get(REQUEST_ID_ATTRIBUTE)'N/A'döndürür — bu amaçlı bir geri dönüş, bir hata değildir. Arka plan işlerindeki korelasyonu tutmak istiyorsanız, iş oluşturuşunda bir iş düzeyinde UUID oluşturun ve bunuLog::shareContext()aracılığıyla,handle()başında enjekte edin;RequestLogger'ın HTTP istekleri için yaptığı gibi.
Neden getOriginal() yerine yalnızca yeni değerleri kullanıyoruz? Çünkü "amount alanı şimdi 500" ifadesi, hikayenin yalnızca yarısıdır. "amount alanı 5000'den 500'e değişti" ifadesi, kanıttır. Bir anlaşmazlık durumunda, fark kanıtı sağlar — yalnızca mevcut durumu değil.
Neden silinmeden önce deleting'i yakalamak gerekir, silindikten sonra değil?
Yaygın hata:
static::deletedyerinestatic::deletingkullanmak.deletedsatır gidildiğinde ateş eder —getAttributes()hiçbir şey döndürmez. Her zamandeletingkullanmalısınız.
deleting satır kaldırılmadan önce ateş eder — tam kayıt hala bellekte bulunur. deleted sonrasında ateş eder ve getAttributes() o noktada boş bir dizi döndürür.
Dosya tabanlı loglar ve klasör segmentasyon problemi
Dosya tabanlı loglar ve klasör segmentasyon problemi
Kaydedilecek günlüğün nereye kaydedileceği yönündeki karar, günlüğün mimarisini şekillendirir.
Veritabanı seçeneği caziptir — sorgulanabilir, dizinlenebilir ve Laravel uygulamasına doğal olarak uyar. Sorun şu: uygulamanızın yazabileceği bir tablo, uygulamanızın onu da değiştirebileceği anlamına gelir. Bir UPDATE audit_logs geçerli SQL'dir. Düzenlenmiş bir ortamda, değiştirilebilir denetim kayıtları, bir uyum sorunudur.
Yalnızca eklemeli günlük dosyaları değiştirilmesi daha zor olan bir yapıdır. OS düzeyinde FILE_APPEND her yazmanın sona eklenmesi anlamına gelir — güncelleme operasyonu yoktur, sadece ekleme vardır. Web kullanıcısının yazabileceği ancak silemeyeceği dosya sistemi izinleri ile birleştirildiğinde, gerçekten değiştirilmesi zor loglarınız olur.
protected function logChanges(array $changes, string $event): void
{
$logPath = $this->buildLogPath();
<span class="nb">file_put_contents</span><span class="p">(</span>
<span class="nv">$logPath</span><span class="p">,</span>
<span class="nb">json_encode</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">prepareLogData</span><span class="p>(</span><span class="nv">$changes</span><span class="p">,</span> <span class="nv">$event</span><span class="p">))</span> <span class="mf">.</span> <span class="kc">PHP_EOL</span><span class="p">,</span>
<span class="no">FILE_APPEND</span> <span class="o">|</span> <span class="no">LOCK_EX</span> <span class="c1">// LOCK_EX, dosya yazımının bozulmasını önler</span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre>
Yaygın hata: Yalnızca
FILE_APPENDkullanmak. Yoğun bir sistemde, birden fazla istek kaydları aynı anda değiştirdiklerinde, iki işlem yazım işlemlerini iç içe geçirebilir ve bir günlüğü bozabilir.LOCK_EXözel bir kilit alır - bir yazım tamamlandığında, bir sonraki başlar. Her iki bayrak da gereklidir.
Klasör yapısı sorunu: Her bir kaydın günlüğünü tablo adını taşıyan bir klasörde depolarsanız, sonunda transactions/ içinde her işlem için bir dosya elde edersiniz. Bir milyon işlem, bir milyon dosya demektir. Çoğu dosya sistemi bunu teknik olarak yönetir, ancak ls, yedekleme araçları ve dizin listeleme işlemleri rahatsız edici bir şekilde yavaşlayabilir.
Çözüm segmentasyondur — kayıtları kimlik aralığına göre alt dizinlerde gruplandırın:
protected function buildLogPath(): string
{
$id = $this->getKey();
$table = $this->getTable();
<span class="c1">// Segment klasörü: en yakın 100'e yuvarla</span>
<span class="c1">// 1–99 ID'leri → klasör "0", 100–199 ID'leri → klasör "100", 1100–1199 ID'leri → klasör "1100"</span>
<span class="nv">$segment</span> <span class="o">=</span> <span class="p">(</span><span class="n">int</span><span class="p">)</span> <span class="p">(</span><span class="nb">floor</span><span class="p">(</span><span class="nv">$id</span> <span class="o">/</span> <span class="mi">100</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">);</span>
<span class="nv">$folder</span> <span class="o">=</span> <span class="nf">storage_path</span><span class="p>(</span><span class="s2>logs/activities/</span><span class="si">{</span><span class="nv">$table</span><span class="si">}</span><span class="s2>/{</span><span class="nv">$segment</span><span class="si">}</span><span class="s2>"</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nb">is_dir</span><span class="p">(</span><span class="nv">$folder</span><span class="p">))</span> <span class="p">{</span>
<span class="nb">mkdir</span><span class="p">(</span><span class="nv">$folder</span><span class="p">,</span> <span class="mo">0755</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="p">}</span>
<span class="nv">$filename</span> <span class="o">=</span> <span class="nb">strtolower</span><span class="p">(</span><span class="nf">class_basename</span><span class="p>(</span><span class="nv">$this</span><span class="p">))</span><span class="mf">.</span><span class="s2>_"</span><span class="si">{</span><span class="nv">$id</span><span class="si">}</span><span class="s2>.log"</span><span class="p">;</span>
<span class="k">return</span> <span class="s2>"</span><span class="si>{</span><span class="nv">$folder</span><span class="si">}</span><span class="s2>/{</span><span class="nv">$filename</span><span class="si">}</span><span class="s2>"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre>
Disk üzerindeki sonuç:
storage/logs/activities/
transactions/
0/
transaction_1.log
transaction_42.log
1100/
transaction_1101.log
transaction_1102.log
1200/
transaction_1200.log
users/
0/
user_1.log
Her bir klasör en çok 100 dosya içerir. Belirli bir kaydın geçmişini bulmak, kimliğini bilmekle — ki her zaman bilirsiniz — bir dosyayı okumakla mümkündür. Yapı, herhangi bir kayıt sayısına kadar ölçeklenebilir, böylece hiçbir klasör aşırı derecede ağırlaşmaz.
Hassas alanları gizlemek
Hassas alanları gizlemek
Plaintext milli kimlik numaraları veya şifreler içeren bir denetim izi, kendi uyumsuzluk sorununu oluşturur. Birçok yetkili bölgede, günlüğü açmamış PII içeren bir dosya, herhangi bir diğer PII depolama alanı gibi kabul edilir — saklama süreleri, erişim kontrolleri ve silme haklarına tabidir.
Buradaki yaklaşım, alanın değiştiğini kaydetmekle birlikte, neye değiştiğini kaydetmeyerek iz bırakmaktır. Bir denetçi, national_id'nin 14:32'de Transaction 1101'de değiştirildiğini görebilir — bu, denetim gereksinimini karşılar — ancak günlük dosyası gerçek değerleri tutmaz.
Hassas verileri depolayan herhangi bir modelde $maskedAttributes'ı geçersiz kılın:
class Transaction extends Model
{
use ModelChangeLogger;
<span class="k">protected</span> <span class="kt">array</span> <span class="nv">$maskedAttributes</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1>'national_id'</span><span class="p">,</span>
<span class="s1>'card_number'</span><span class="p">,</span>
<span class="p">];</span>
<span class="p">}</span>
</code></pre>
RBAC'in bu resimde nereye oturduğu
Erişim kontrolü ve denetim izleri ilişkili problemdir. Kayıtları kimlerin değiştirdiğini kaydediyorsanız, kimlerin denemesi gereken bir şey yapmadığını da kaydetmelisiniz.
Laravel'in Gate'i, bunun için tam bir kancadır:
Gate::after(function (User $user, string $ability, bool $result) {
if (!$result) {
// request_id, Log::shareContext() aracılığıyla otomatik olarak dahil edilir
// set in RequestLogger — burada manuel olarak almaya gerek yok
Log::channel()->warning(, [
=> $user->id,
=> $ability,
=> request()->ip(),
]);
}
});
Neden özellikle hataları kaydetmeliyiz? Başarılı bir yetkilendirme girişimi normal bir çalışmadır. Başarısız bir girişim sinyaldir — bir kullanıcı, yetkisi olmadığı uç noktalara inceleme yapıyor olabilir veya ihlal edilmiş bir hesap yetkileri artırmaya çalışıyordur. Birden fazla başarısız girişim üzerindeki desen, bunu yakalayamadığınız bir erken uyarı haline gelir.
Belirtmeye değer bir tasarım ilkesi: izinleri yetenekler etrafında tasarlayın, rol adları etrafında değil. $user->can('transaction.approve') ifadesi, kullanıcının ne yapabileceğini ifade eder; sahip olduğu rol etiketinden bağımsız. $user->role === 'admin' ifadesi, "admin" çeşitli bağlamlarda farklı şeyler ifade ettiğinde aniden kırılır — büyüyen sistemlerde bu her zaman sonunda olur.
Artık Ne Sahipsiniz
Artık Ne Sahipsiniz
RequestLogger'ı küresel middleware olarak kaydettiniz. ModelChangeLogger traitini her hassas veya denetime tabi veriyi işleyen modelinize eklediniz. Gate::after kancasını kaydettiniz.
Artık: her istek bir ID taşır. Her model değişikliği, kimin yaptığını, hangi alanın hangi değerden hangi değere değiştiğini, ne zaman ve hangi isteğin neden olduğunu kaydediyor. Başarısız izin kontrolü kayıt altına alınıyor. Yığınınız boyunca her log satırı ortak bir ilişki ID'sini paylaşır.
Bir destek bileti geldiğinde "Transaction 1101'de bir şey değişti" dendiğinde, storage/logs/activities/transactions/1100/transaction_1101.log dosyasını açarsınız. Tam farkı, bunu yapan kullanıcıyı ve istek ID'sini görürsünüz. O istek ID'sini istek günlüklerinde greplerseniz, tüm resmi bir dakika içinde görebilirsiniz.
Bu, çalışan bir uygulama ile her şeyi hatırlayan bir uygulama arasındaki farktır.
Bu makaleden gelen ana içgörü: Veri tabanı denetim tablosu değiştirilebilir — uygulama kodunuz onu
UPDATEedebilir. Her kayıdı işleyen, istek ID'sinin her girişe bağlı olduğu, değişikliklerin kim, ne ve neden sorularını cevapladığı günlüğü dosyası, size kırılmaya dayanak, iç içe geçmiş kayıtlar sunar.
Göndermeden Önce — Kontrol Listesi
Göndermeden Önce — Kontrol Listesi
- [ ]
RequestLoggerbootstrap/app.php'de küresel middleware olarak kaydedildi - [ ]
ModelChangeLoggertraiti hassas veya denetime tabi veri işleyen her modele eklendi - [ ]
$maskedAttributesPII (parolalar, kimlik numaraları, kart numaraları) saklayan modellerde tanımlandı - [ ]
storage/logs/activities/web kullanıcısı tarafından yazılabilir, ancak silinemezdir - [ ]
DB::listenözel bir yapılandırma bayrağının veyaapp.debug'nin arkasında yer alıyor — üretimde koşmuyor - [ ]
Gate::afterkancasıAuthServiceProvider'da kaydedildi - [ ] Bilinen bir istek ID'si için yapılan bir grep, hem istek günlüğü hem de model değişiklik günlükleri arasında sonuç döndürür
Sonraki: Bölüm 2 — Kuyruk Mimarisi: Geri Plan Çalışmalarını Tasarlamak
Kaynak: Orijinal Makale
- Ne Yapacaksınız
- Bir denetim izi gerçekten neyi kanıtlamalı
- İstek ID’si: Her günlüğe bir ipucu
- Yavaş sorgular: burada bulunduğunuzda ekleyin
- Model değişiklik günlüğü
- Dosya tabanlı loglar ve klasör segmentasyon problemi
- Hassas alanları gizlemek
- RBAC'in bu resimde nereye oturduğu
- Artık Ne Sahipsiniz
- Göndermeden Önce — Kontrol Listesi


