ลูกค้าโทรมาบอกว่ากดสั่งซื้อแล้วขึ้นหน้า Server Error สิ่งแรกที่เราอยากเห็นคือ log ของแอปช่วงเวลานั้น ที่ผ่านมาเราก็มัก SSH เข้า server แล้วพิมพ์ tail -f storage/logs/laravel.log หรือ grep หาคำที่น่าจะเกี่ยว ใช้ได้นะ แต่ต้องมีสิทธิ์เข้า server และต้องจำคำสั่งให้ได้ตอนกำลังรีบ
ถ้าเว็บอยู่บน shared hosting ที่ไม่มี SSH ก็ยิ่งลำบาก ต้องเข้า File Manager ของ cPanel หรือ DirectAdmin ไปที่ storage/logs แล้วดาวน์โหลด laravel.log มาเปิดในเครื่อง ไฟล์นี้ใหญ่ขึ้นทุกวัน พอถึงหลายร้อย MB โปรแกรมแก้ไขข้อความก็เริ่มเปิดไม่ไหว
โพสต์นี้แก้สองเรื่อง เรื่องแรกคือตั้งค่าของ Laravel เองให้แยกไฟล์ log ให้เล็กลง เรื่องที่สองคือติดตั้ง Log Viewer ให้อ่าน log ผ่านเบราว์เซอร์หลัง login ของแอป เว็บนี้เองผมก็ใช้ Log Viewer อยู่ ลิงก์ไว้ในหลังบ้านคู่กับ Horizon และ Pulse
แยก log เป็นไฟล์รายวันก่อน
Laravel ตั้งต้นมาให้เขียน log ลงไฟล์เดียว เพราะ .env.example ของโปรเจกต์ใหม่ตั้ง LOG_STACK=single ไว้ ไฟล์ laravel.log จึงโตไปเรื่อย ๆ ไม่มีวันหยุด เปลี่ยนเป็น daily ใน .env
LOG_STACK=daily
LOG_DAILY_DAYS=14
ทีนี้ Laravel จะเขียนลงไฟล์ใหม่ทุกวัน ชื่อตามวันที่ เช่น laravel-2026-10-11.log และลบไฟล์ที่เก่ากว่า LOG_DAILY_DAYS วันออกให้เอง ถ้าไม่ตั้งค่านี้ Laravel ใช้ 14 วัน ตามค่า max_files ของ channel daily ใน config/logging.php (เอกสาร Laravel)
ไฟล์ laravel.log ตัวเดิมยังอยู่ที่เดิม Laravel ไม่ลบให้ ถ้าไม่ต้องใช้แล้วก็ลบทิ้งได้ ถ้าดิสก์ของ server เริ่มเต็มเพราะ log ดูวิธีไล่หาว่าโฟลเดอร์ไหนกินพื้นที่ได้ใน ดูขนาดโฟลเดอร์บน Linux ยังไง
แค่ขั้นนี้ก็ช่วยได้เยอะแล้ว เวลาลูกค้าแจ้งปัญหาของเมื่อวาน เราเปิดหรือดาวน์โหลดแค่ไฟล์ของวันนั้น ไม่ต้องลากไฟล์ทั้งปีมา
ติดตั้ง Log Viewer
Log Viewer ของ Arunas Skirius เป็นแพ็กเกจ open source (MIT) ที่เพิ่มหน้าอ่าน log เข้าไปในแอป Laravel ของเราเอง ตอนเขียนโพสต์นี้เวอร์ชันล่าสุดคือ 3.24.2 (มิถุนายน 2026) รองรับ PHP 8.0 ขึ้นไป และ Laravel 8 ถึง 13
composer require opcodesio/log-viewer
แล้วเปิด /log-viewer ในเบราว์เซอร์ เช่น https://shop.test/log-viewer ได้เลย
หน้าจอเขียนด้วย Vue และมากับแพ็กเกจแบบ build เสร็จแล้ว แอปของเราไม่ต้องมี Livewire และไม่ต้องรัน npm เพิ่ม ส่วนคำสั่ง php artisan log-viewer:publish ที่ README กับหน้าติดตั้งยังบอกให้รัน ในเวอร์ชันนี้ไม่ต้องแล้ว ไฟล์ config ของแพ็กเกจเขียนไว้ว่าแพ็กเกจส่ง asset จากโฟลเดอร์ vendor ให้เอง ถ้าเคย publish ไว้ ทุกครั้งที่อัปเดตแพ็กเกจก็ต้อง publish ซ้ำ ไม่อย่างนั้นหน้าจอจะเก่ากว่าโค้ด
Log Viewer อ่านไฟล์ log ด้วย PHP ธรรมดา ไม่ต้องลงโปรแกรมอื่นบน server จึงใช้บน shared hosting ได้เหมือน VPS
บน production ต้องบอกเองว่าใครเข้าได้
ตอน APP_ENV=local ใครเปิด /log-viewer ก็เห็น log ทั้งหมด พอขึ้น production ถ้ายังไม่ได้ตั้งอะไร ทุกคนจะได้ 403 รวมถึงเราด้วย ให้ตั้งกฎใน boot() ของ AppServiceProvider แบบใดแบบหนึ่ง
use Illuminate\Http\Request;
use Opcodes\LogViewer\Facades\LogViewer;
public function boot(): void
{
LogViewer::auth(fn (Request $request): bool => in_array(
$request->user()?->email,
['[email protected]'],
true,
));
}
use App\Models\User;
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('viewLogViewer', fn (?User $user): bool => in_array(
$user?->email,
['[email protected]'],
true,
));
}
ทั้งสองแบบให้ผลเหมือนกัน ถ้าตั้งไว้ทั้งคู่ Log Viewer ใช้ LogViewer::auth() ก่อน เลือกแบบที่เข้ากับแอป ถ้าแอปใช้ Gate อยู่แล้ว เช่น viewPulse หรือ viewHorizon ก็ใช้ Gate ให้เหมือนกัน
คนที่ยังไม่ login จะได้ 403 เลย Log Viewer ไม่พาไปหน้า login ให้ ถ้าอยากให้เด้งไปหน้า login ก่อน ให้ publish config แล้วเพิ่ม auth ใน middleware (วิธี publish อยู่ในหัวข้อตั้งค่าที่ใช้บ่อยด้านล่าง)
'middleware' => [
'web',
'auth',
\Opcodes\LogViewer\Http\Middleware\AuthorizeLogViewer::class,
],
ใครเปิดได้ ก็ลบไฟล์ log ได้
แพ็กเกจตั้ง gate deleteLogFile, deleteLogFolder, downloadLogFile และ downloadLogFolder ไว้ให้ทุกคนที่เข้าหน้านี้ได้ (โค้ดของ service provider) ถ้าเปิดให้คนในทีมดูด้วย แต่ไม่อยากให้ใครเผลอลบ log ทิ้ง ให้ปิด gate สองตัวแรก (เอกสาร)
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Opcodes\LogViewer\LogFile;
use Opcodes\LogViewer\LogFolder;
Gate::define('deleteLogFile', fn (?User $user, LogFile $file): bool => false);
Gate::define('deleteLogFolder', fn (?User $user, LogFolder $folder): bool => false);
ปุ่มลบจะหายไปจากหน้าจอ และ API ลบไฟล์ก็ตอบ 403
หน้าจอ Log Viewer
เลือกไฟล์ทางซ้าย อ่าน log ทางขวา

ไฟล์รายวันเรียงจากใหม่ไปเก่าพร้อมขนาด กดไฟล์ไหนก็เห็น log ของไฟล์นั้นทางขวา ใหม่สุดอยู่บน หน้าละ 25 รายการ
แต่ละแถวคือ log หนึ่งรายการ บอกระดับ เวลา environment และข้อความ ตัวเลขท้ายแถวคือลำดับของ log ในไฟล์ กดไอคอนลิงก์ข้าง ๆ จะได้ URL ที่ชี้มาที่ log รายการนั้นตรง ๆ ส่งให้เพื่อนในทีมเปิดดูต่อได้
เปิดดู error ทั้งก้อน พร้อม stack trace

กดที่แถวแล้วจะเห็น class ของ exception ข้อความเต็ม และ stack trace แยกทีละบรรทัด ส่วนแท็บ Raw แสดงข้อความกับ context ทั้งก้อนเป็น JSON
stack trace ยาว ๆ อ่านง่ายขึ้นมากเมื่อแยกบรรทัดแบบนี้ เราไล่หาบรรทัดแรกที่เป็นโค้ดของเราเอง (ในภาพคือ app/Services/PaymentGateway.php:64) ได้เร็วกว่าอ่านในไฟล์ ถ้าไม่อยากเห็นบรรทัดของ vendor เปิด Shorter stack traces ที่ปุ่มฟันเฟืองมุมขวาบน
กรองตาม level

เมนู Severity บอกว่าแต่ละระดับมีกี่รายการในไฟล์นี้ ติ๊กแค่ Error ขึ้นไป รายการทางขวาก็เหลือ 8 จาก 331
เวลาลูกค้าแจ้งปัญหา เรามักเริ่มจากดูว่ามี error อะไรบ้างในวันนั้น แล้วค่อยเปิด Info กลับมาดูว่าก่อนเกิด error แอปทำอะไรอยู่
ค้นหาด้วย request ID

ค้นด้วย request ID ในไฟล์ของวันนั้น เจอ 3 รายการ คือสร้างคำสั่งซื้อ ชำระเงิน และส่งอีเมลยืนยัน เปิดรายการไหนก็เห็น context พร้อมไฮไลต์คำที่ค้น
ปุ่มทางขวาของช่องค้นหาบอกว่ากำลังค้นที่ไหน ตอนเลือกไฟล์อยู่จะค้นแค่ไฟล์นั้น ถ้ายังไม่ได้เลือกไฟล์ ปุ่มจะเป็น Search all files ค้นทุกไฟล์ และถ้าค้นในไฟล์เดียวแล้วไม่เจอ หน้าจอจะมีปุ่ม Search all files ให้กดค้นต่อในทุกไฟล์ เหมาะกับตอนที่ไม่แน่ใจว่าเรื่องเกิดวันไหน
ถ้าแอปใส่ request ID ให้ทุกบรรทัดของ log ไว้ด้วย Context ช่องค้นหานี้คือจุดที่ได้ใช้ ค้น ID เดียวก็เห็นทั้ง request และ queue job ที่ request นั้นสั่ง วิธีใส่ request ID อยู่ใน Laravel Context คืออะไร ใส่ request ID ให้ทุกบรรทัดของ log ยังไง
ช่องค้นหารับ regular expression และไม่สนตัวพิมพ์เล็กใหญ่ เช่น cURL error (7|28) จะเจอทั้ง error ต่อไม่ติดและ timeout ข้อเสียคือถ้าคำที่ค้นมีวงเล็บ ต้องใส่ \ นำหน้า เช่น charge\(
ตั้งค่าที่ใช้บ่อย
ค่าทั้งหมดอยู่ใน config/log-viewer.php ของแพ็กเกจ publish ออกมาแก้ด้วยคำสั่งนี้
php artisan vendor:publish --tag="log-viewer-config"
route_pathเปลี่ยน URL จากlog-viewerเป็นชื่ออื่นinclude_filesกับexclude_filesบอกว่าจะอ่านไฟล์ไหน pattern ที่ไม่ขึ้นต้นด้วย/นับจากstorage/logsของแอป ส่วน pattern ที่ขึ้นต้นด้วย/เป็น path เต็มบน server (เอกสาร)timezoneเวลาที่แสดงบนหน้าจอ ค่าตั้งต้นใช้ timezone ของแอป ถ้าแอปเก็บเวลาเป็น UTC แต่อยากอ่านเป็นเวลาไทย ตั้งเป็นAsia/Bangkokได้ ไฟล์ log ไม่เปลี่ยนLOG_VIEWER_ENABLED=falseใน.envปิดหน้านี้ทั้งหมด route จะหายไปLOG_VIEWER_CACHE_DRIVERLog Viewer สร้าง index ของแต่ละไฟล์เก็บไว้ใน cache ของแอป ครั้งแรกที่เปิดไฟล์ใหญ่จะช้าหน่อยเพราะต้องอ่านทั้งไฟล์ ครั้งต่อไปอ่านแค่ส่วนที่เพิ่มมา ถ้าอยากแยก index ไปไว้ที่ Redis ก็ตั้งค่านี้ (เอกสาร)
'include_files' => [
'*.log',
'**/*.log',
],
มี server หลายเครื่อง ดูจากหน้าเดียวได้
ถ้าแอปเดียวกันรันอยู่หลายเครื่อง หรืออยากดู log ของ staging จาก production ใส่เครื่องอื่นไว้ใน hosts ได้ เครื่องปลายทางต้องติดตั้ง Log Viewer ไว้ด้วย แล้วเครื่องที่เราเปิดหน้าจอจะส่งคำขอต่อไปให้ (เอกสาร)
'hosts' => [
'local' => [
'name' => ucfirst(env('APP_ENV', 'local')),
],
'staging' => [
'name' => 'Staging',
'host' => 'https://staging.shop.example.com/log-viewer',
'auth' => [
'token' => env('LOG_VIEWER_STAGING_TOKEN'),
],
],
],
ฝั่ง staging ต้องรับ token นี้ใน LogViewer::auth() ของตัวเองด้วย เพราะคำขอที่ส่งต่อมาไม่มี session ของคนที่ login อยู่ ใส่ token ค่าเดียวกันไว้ใน .env ของ staging แล้วเช็คใน callback
'log_viewer' => [
'token' => env('LOG_VIEWER_TOKEN'),
],
LogViewer::auth(function (Request $request): bool {
$token = config('services.log_viewer.token');
if (filled($token) && hash_equals($token, (string) $request->bearerToken())) {
return true;
}
return in_array($request->user()?->email, ['[email protected]'], true);
});
เช็ค filled($token) ก่อนเสมอ ถ้าลืมตั้ง token ใน .env แล้วเทียบค่าว่างกับค่าว่าง คำขอที่ไม่มี token จะผ่านไปได้
ดึง log ผ่าน API
หน้าจอของ Log Viewer อ่านข้อมูลจาก API ชุดเดียวกับที่เราเรียกเองได้ ใต้ /log-viewer/api (เอกสาร)
# รายการไฟล์ log
curl -H "Authorization: Bearer $LOG_VIEWER_TOKEN" \
https://shop.example.com/log-viewer/api/files
# ค้นหาในทุกไฟล์ ไม่เอา debug กับ info
curl -G -H "Authorization: Bearer $LOG_VIEWER_TOKEN" \
https://shop.example.com/log-viewer/api/logs \
--data-urlencode "query=ConnectionException" \
--data-urlencode "exclude_levels[]=DEBUG" \
--data-urlencode "exclude_levels[]=INFO"
API ผ่านกฎเดียวกับหน้าจอ คือ LogViewer::auth() หรือ gate viewLogViewer จะเรียกจาก script นอกเบราว์เซอร์ก็ต้องรับ token แบบในหัวข้อก่อนหน้า ส่วนพารามิเตอร์กรอง level หน้าเอกสาร API เขียนว่า levels แต่โค้ดเวอร์ชัน 3.24 อ่าน exclude_levels คือส่งระดับที่ไม่อยากได้ไปแทน
ผลที่ได้เป็น JSON แต่ละรายการมี level, datetime, message และ context แยกเป็น array ไว้ให้แล้ว เอาไปต่อกับ script ตรวจรายวันหรือส่งเข้า LINE ก็ได้ แต่ถ้าอยากได้การแจ้งเตือนจริงจัง เครื่องมือในหัวข้อถัดไปทำได้ดีกว่า
อยากได้มากกว่าอ่าน log ใช้อะไรเพิ่ม
Log Viewer ช่วยตอนที่เรารู้แล้วว่ามีปัญหา และอยากไปอ่าน log ตอนนั้น แต่ไม่ได้แจ้งเตือนเราเองเวลามี error ใหม่ เครื่องมืออื่นช่วยคนละส่วน
- Laravel Pail ดู log แบบ real time ใน terminal ด้วย
php artisan pailกรองตาม level หรือ user ได้ แต่ยังต้อง SSH และต้องมี PHP extensionpcntl - Laravel Telescope เอกสารเองบอกว่าทำมาคู่กับเครื่อง dev ส่วนบน production ค่าตั้งต้นเก็บแค่ exception, job ที่ fail, scheduled task, query ที่ช้า และ entry ที่ติด tag ที่เราเฝ้าไว้ ทั้งหมดลงฐานข้อมูลที่ต้องคอย prune
- Laravel Pulse นับและสรุปว่า request, query หรือ job ไหนช้า exception ไหนเกิดบ่อย เห็นแนวโน้ม แต่ไม่ได้เก็บ log ทีละบรรทัด
- เครื่องมือติดตาม error รวม exception ที่ซ้ำกันเป็น issue เดียวและส่งอีเมลแจ้งเตือน Sentry มีแผน Developer ที่ไม่มีค่าใช้จ่าย ใช้ได้ 1 คน 5,000 error ย้อนหลัง 30 วัน Laravel Nightwatch มีแผนฟรี 300,000 event ย้อนหลัง 14 วัน แผน Pro เดือนละ 20 ดอลลาร์ ถ้าส่งข้อมูล error ออกไปนอก server ไม่ได้ ผมทำ watchtower-laravel ไว้ รับ error ผ่าน Sentry SDK แล้วเก็บลงฐานข้อมูลของแอปเอง
ที่ผมใช้อยู่คือ Log Viewer คู่กับเครื่องมือติดตาม error ตัวหนึ่ง เครื่องมือ error บอกว่ามีอะไรพัง แล้วเปิด Log Viewer ไปดูว่าก่อนและหลังนั้นแอปเขียนอะไรไว้
GitHub: opcodesio/log-viewer






