Bir üretim Laravel sitesindeki iletişim formu çalışmayı durdurdu. Her gönderim aynı yanıtı döndürüyor:
{"message": "Server Error"}
Herhangi bir yığın izleme yok. Hiçbir Sentry olayı yaşanmadı. Ayrıca storage/logs/laravel.log dosyası on bir gündür güncellenmemişti — değiştirme zaman damgası, formun bozulduğu tam günde takılı kalmıştı.
Bu donmuş zaman damgası tüm hikayeydi, ama doğru okumak biraz zaman aldı. Boş bir günlüğü “hiçbir şey olmadı” olarak kabul etme refleksi var. Genellikle tam tersidir: yazıcıya bir şey oldu.
The symptom that misleads you
Aktif bir olay sırasında boş bir günlük, üç makul açıklama sunar ve mühendisler genellikle bunları yanlış sırayla kontrol eder:
- Kod yolu asla çalışmadı. Kolayca çürütülebilir – form 500 döndürdü, yani bir şey çalıştı.
- Günlüğün seviyesi filtrelendi. Ayrıca kolay – yakalanmamış bir istisna
errorseviyesinde kayıt yapar, mantıklı bir yapılandırma bunu filtrelemez. - Yazıcı kendisi başarısız oldu. Nadiren kontrol edilir, çünkü günlüğe yazma, başarısız olamayacak bir altyapı gibi görünür.
Üçüncüsüydü. Ve en elverişsiz şekilde başarısız oldu.
Root cause: two different users, one log file
Bu yığında PHP, ifade edildiği şekilde iki kimlik altında çalışır:
| Context | Runs as | Can write to a 664 file owned by the site user? |
|---|---|---|
| CLI (php artisan, cron, deploy scripts) | site’nin sistem kullanıcısı | Evet – sahibi o. |
| Web requests (LSAPI / PHP-FPM pool) | nobody (uid 65534) | Hayır – ne sahibi, ne de grubunda. |
Bu ayrım, paylaşımlı ve cPanel tarzı barındırmalarda yaygındır ve geliştirme sırasında görünmez çünkü dizüstü bilgisayarda her şey tek bir kullanıcı olarak çalışır. Üretimde de görünmez kalır, ta ki web sürecinin yazması gereken bir dosya CLI süreci tarafından oluşturulana dek.
Ve tam olarak bu oldu. Shell üzerinden çalıştırılan bir bakım görevi, laravel.log dosyasını 664 moduyla ve site kullanıcısı olarak yeniden oluşturdu. O andan itibaren her web isteği, loglama yapmaya çalışan bir hata aldı:
UnexpectedValueException: The stream or file ".../storage/logs/laravel.log"
could not be opened in append mode: Permission denied
Why the exception was completely silent
Kontrolör yaklaşık şöyle görünüyordu – bu yapı, gerçek kod tabanlarında her yerde mevcuttur:
try {
Log::info('Contact form submitted', $payload);
ContactJob::dispatch($payload);
} catch (\Throwable $e) {
Log::error('Contact form failed: ' . $e->getMessage());
return response()->json(['message' => ], 500);
}
Ölü bir günlük ile izlemek:
Log::info(...)UnexpectedValueExceptionfırlatır – izin reddedildi.- Kontrol
catchbloğuna atlar. Log::error(...)aynı istisnayı yeniden fırlatır, bu sefercatchbloğundan.- Hiç kimse onu yakalayamaz. Laravel’in işleyicisine geçer, bu da kaydetmeye çalışır, ki bu da başarısız olur.
- Müşteri tamamen bir 500 alır, hiçbir bağlam yoktur.
Genel ders: Kayıt yapan bir blok, yalnızca günlüğün güvenilirliği kadar güvenilirdir. Kayıt yapma, bozulduğunda, hata işleme durumunu artırır ve kanıtı yok eder.
Diagnosing it when the app cannot tell you anything
Alışılmış refleks php artisan tinker kullanmaktır. Ancak burada, anlatıcı PHP ikilisinin sunucuda Redis uzantısı yüklenmediği için bu işe yaramadı. CLI PHP ikilisi, web SAPI’si yüklenmişti. CLI’den bir istek başlatmaya çalıştığınızda, oturum/kasa sürücüsü üzerindeki hata, gerçek hataya ulaşmadan önce fırladı.
İki ortam, iki uzantı seti, iki kullanıcı kimliği. CLI, bir web isteğini kopyalayamadı.
Çalışan bir web bağlamında çalışmaya devam etmek çalıştı — genel dizinde, çekirdeği başlangıçta çalıştıran, hedef rotayı yöneten ve doğrudan istisna nesnesini yazdıran geçici bir tanısal betik:
// public/diag-temp.php — KULLANIMDAN SONRA HEMEN SİLİN
require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . ;
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$request = Illuminate\Http\Request::create(, , [
=> , => , ,
]);
$response = $kernel->handle($request);
var_dump($response->exception); // günlüğe kaydedilmeyi başaramayan şey
Bir curl ile hit yapın, gerçek istisnayı okuyun, dosyayı silin. İzin hatası ilk çalıştırmada göründü.
Dikkatli kullanın: Bu tür bir betik normal yönlendirme ve ara yazılım giriş noktalarınızı atlar. Ona tahmin edilemez bir dosya adı verin, bir kez çalıştırın ve aynı oturumda kaldırın. Üzerinde bırakmayın.
The fix
Üç değişiklik, en küçüğünden başlıyoruz:
1. Zehirli dosyayı döndürün ve yazılabilir yeni bir tane oluşturun. Eski günlük 280 MB’a çıkmıştı, bu da ayrı bir problem:
mv storage/logs/laravel.log storage/logs/laravel.log.backup-$(date +%F)
touch storage/logs/laravel.log
chmod 666 storage/logs/laravel.log
2. Monolog’un gelecekteki dosyaları doğru modda oluşturmasını sağlayın. Laravel’in log kanalları bir permission anahtarı kabul eder ve bunun olmaması, etkide bulunduran umask’ı miras alır:
// config/logging.php
=> [
=> ,
=> storage_path(),
=> env(, ),
0666, // log döngüsü ve CLI tarafından oluşturulan dosyalar için geçerlidir
],
3. Yakalama bloğunun fırlatmasını durdurun. Logger başarısız olursa, işleyici artmaktan ziyade degradeli olmalıdır:
} catch (\Throwable $e) {
try {
Log::error(, [=> $e]);
} catch (\Throwable $loggingFailure) {
error_log(. $e->getMessage()); // syslog düşüşü
}
return response()->json([ ], 500);
}
error_log() SAPI’nin kendi hata günlüğüne yazar, bu da web sunucusu tarafından sahibidir ve dolayısıyla her zaman yazılabilir. Bu zarif olmasa da, uygulama günlüğü başarısız olduğunda hayatta kalan bir kanaldır.
The second bug, found only after logging worked
Günlük tekrar çalışmaya başladığında, bir sonraki gönderim gerçek bir yığın izini üretti – tamamen farklı bir hata, ilk hatanın arkasında gizlenmişti:
ErrorException: Undefined array key "url"
Bir kuyruk dinleyicisi, yüklemeden $data['url'] değerini okudu. O alan isteğe bağlıydı ve bazı formlar boş bırakılıyordu. Bir ?? null bunu düzeltti.
Bu kısmı içselleştirmek önemlidir: bozuk bir günlük bir hatayı gizlemez, tüm hataları gizler. On bir gün boyunca yaşanan her hata aynı anonim 500’e dönüştü.
Two operational notes that cost extra time
- Kuyruk işlemleri kodunuzu önbelleğe alır. Bir dinleyici düzenlendiğinde, çalışan işçi eski sınıfı yürütmeye devam eder. O işçileri öldürün ve denetleyici veya cron’un yeniden başlatmasına izin verin, ardından başarısız işler için
php artisan queue:retry allile yeniden çalıştırın. - Web sürecinin yazdığı her şey, web sürecine yazılabilir olmalıdır. Günlükler, önbellek dosyaları, derlenmiş görünümler, yüklenmiş medyalar. Bir dağıtım betiği veya shell oturumu herhangi birini oluşturduğunda, sahiplik ve mod belirli bir şekilde ayarlanmalıdır; aksi takdirde sonraki web isteği, erişemeyeceği bir dosya miras alır.
A checklist worth stealing
| Kontrol | Komut | Görmek istediğiniz şey |
|---|---|---|
| Web PHP hangi kullanıcı altında çalışıyor? | Yazılabilir yolların beklediği kimlik | |
| Günlük gerçekten yazılıyor mu? | ls -l –time-style=full-iso storage/logs/ | Şimdi, birkaç dakika değil; haftalar değil. |
| Web kullanıcısı ekleyebilir mi? | stat -c ‘%U %G %a’ storage/logs/laravel.log | Web sürecini içeren mod |
| CLI web ile eşleşiyor mu? | php -m vs a web phpinfo() | Eşleşen uzantı setleri, yoksa kopyalayamazsınız. |
| Günlük sınırlandırılmamış mı? | du -sh storage/logs/ | Rotasyon yapılandırılmış; yüzlerce MB değil. |
Add the monitor you wish you had
Bu tür bir hatayı tespit etmenin en ucuz yolu, günlük dosyasının hâlâ güncellenip güncellenmediğini kontrol eden zamanlanmış bir kontrol olmaktadır. Uygulamanız normal bir günde herhangi bir günlüğe yazıyorsa, birkaç saatlik bir mtime, bir olaydır:
# laravel.log 6 saat içinde yazılmamışsa uyar
find /path/to/storage/logs/laravel.log -mmin +360 \
-exec echo "WARNING: laravel.log stale" \;
Bunu hangi sayfalarınıza bağlıyorsanız, bağlayın. On bir gün süren sessiz bir hata, bir günlüğü yazma problemi değildir – bu bir izleme problemidir, ki bu da bir günlük problemi ortaya çıkmıştır.
Takeaways
- Bir kesinti sırasında boş bir günlük, yazıcının başarısız olduğunu gösterir.
- CLI PHP ve web PHP genellikle farklı kullanıcılar ve farklı uzantılarla çalışır. Hataları yaşandıkları bağlamda kopyalayın.
- Asla
catchbloğunun fırlatmasına izin vermeyin. Kayıt çağrılarını sarın veyaerror_log()ile geri çekilin. - Dosya tabanlı günlük kanallarında
permission‘ı açık bir şekilde ayarlayın. - Günlük dosyanızın tazeliğini izleyin, sadece içeriğini değil.
FAQ
Laravel neden hiçbir günlük girişi olmadan genel 500 döndürüyor?
Genellikle, çünkü yatak yazma yapamaz. Monolog, günlük dosyasını ekleme modunda açamazsa fırlatır ve eğer yakalama bloğunuz da Log::error() çağırıyorsa, bu ikinci kez bir yakalamadan fırlatılır ve kaydedilmiş bir iz yoktur. İlk önce storage/logs/laravel.log üzerindeki değiştirme zaman damgasını kontrol edin — aktif bir olay sırasında donmuş bir zaman damgası dosya izinlerine doğrudan işaret eder.
Web isteğim neden php artisan tinker’dan farklı davranıyor?
Web PHP ve CLI PHP genellikle ayrı SAPIlardır. Farklı işletim sistemi kullanıcıları olan, farklı uzantı setleri yükleyen ve farklı php.ini dosyalarını okuyan olabilirler. Birçok cPanel ve paylaşımlı sunucuda web süreci, yani nobody altında çalışırken, CLI, sitenin kendi kullanıcısı olarak çalışır. Bu, CLI’nın her zaman yalnızca webe özgü bir hatayı tekrarlayamaması anlamına gelir ve CLI tarafından oluşturulan bir dosya, web işlemi tarafından yazılamaz hale gelebilir.
storage/logs/laravel.log için hangi izinler olmalı?
Web sürecinin ona ekleme yapabilmesi gerekir. Web PHP, dağıtım kullanıcısından farklı bir kullanıcı olarak çalışıyor olduğunda, bu pratikte dosya için 666 ve dizin için 777 moduna sahip olmalıdır ya da sahipliği ayarlamak gerekir ki web kullanıcısının grubu yazma erişimi olsun. Ayrıca, log kanalında permission anahtarını config/logging.php içinde ayarlayın ki Monolog, döngüden sonra dosyayı doğru modda yeniden oluştursun.
Günlük bozulduğunda Laravel 500 hatasını nasıl ayıklayabilirim?
Simüle etmeye çalışmak yerine web bağlamında çalışın. Genel dizininiz içine geçici bir PHP dosyası koyun, çekirdeği başlatın, hedef isteği yönetin ve $response->exception nesnesini dökün, ardından onu curl ile test edin ve hemen silin. Onu tahmin edilemeyen bir adla yaptığınızdan emin olun ve sunucuda bırakmayın.
Kullanıcılar bunu bildirmeden önce bu hatayı nasıl tespit edebilirim?
Günlük dosyasının tazeliğini, yalnızca içeriğini değil, izleyin. Günlük dosyası, birkaç saat içinde değişmediyse bülten gönderen bir zamanlanmış iş, sessiz günlüğü hatalarını yakalayabilir. Bunu, bozuk bir yazma yolunu bir uyarı olarak yüzeye çıkartmak için gerçek bir form gönderen dış bir çalışma süresi kontrolü ile eşleştirin.
Bu makale ilk olarak web-pioneer.com‘da yayımlandı.
Kaynak: Orijinal Makale
- The symptom that misleads you
- Root cause: two different users, one log file
- Why the exception was completely silent
- Diagnosing it when the app cannot tell you anything
- The fix
- The second bug, found only after logging worked
- Two operational notes that cost extra time
- A checklist worth stealing
- Add the monitor you wish you had
- Takeaways
- FAQ


