Open source

watchtower-laravel ระบบติดตาม error แบบ Sentry ที่อยู่ในแอป Laravel ของคุณเอง

แพ็กเกจที่ทำให้แอป Laravel เก็บ error ของตัวเองได้ทั้งฝั่ง server และเบราว์เซอร์ มีหน้าดู issue อีเมลแจ้งเตือน และ MCP ให้ Claude ช่วยไล่ปัญหา โดยไม่ต้องมี server แยก

เผยแพร่
เวลาอ่าน
7 นาที

ปัญหาที่เจอ

แอปที่ใช้งานจริงต้องรู้ว่าเกิด error อะไรขึ้นบ้าง Sentry ทำเรื่องนี้ได้ดีมาก แต่บางงานส่งข้อมูล error ออกไปนอกเซิร์ฟเวอร์ไม่ได้ เพราะ stack trace และ request มักมีข้อมูลของลูกค้าติดไปด้วย ส่วนการตั้ง Sentry แบบ self-hosted เองก็ต้องดูแลระบบใหญ่อีกชุด ซึ่งเกินความจำเป็นสำหรับแอปเดียว

ผมเลยทำ Watchtower ให้รับข้อมูลในรูปแบบเดียวกับ Sentry แอปจึงใช้ Sentry SDK ตัวจริงต่อได้เลย และในโหมด standalone ทุกอย่างอยู่ในแอป Laravel ตัวเดียว ทั้งตารางเก็บ error หน้าดู issue และ MCP ไม่ต้องมีเซิร์ฟเวอร์เพิ่ม

ทำงานอย่างไร

flowchart LR
  B[เบราว์เซอร์] -->|/api/watchtower-relay| I[Ingest]
  L[Exception ใน Laravel] -->|self-capture| I
  O[แอปอื่นที่ใช้ Sentry SDK] -->|DSN ของโปรเจกต์| I
  I -->|scrub · ตัดขนาด · rate limit| Q[(queue)]
  Q --> D[(ตาราง watchtower_*)]
  D --> U[หน้า /watchtower]
  D --> A[อีเมลแจ้งเตือน]
  D --> M[MCP ให้ Claude]

error จากทุกทางเข้าผ่านขั้นตอนเดียวกันก่อนลงฐานข้อมูลของแอป

  • ฝั่ง server ใช้ sentry/sentry-laravel ตามปกติ แต่ในโหมด standalone แพ็กเกจเปลี่ยน transport ของ SDK ให้ส่ง event เข้า ingest ในโปรเซสเดียวกันเลย ไม่ต้องยิง HTTP กลับเข้าแอปตัวเอง
  • ฝั่งเบราว์เซอร์ ใช้ @sentry/browser ส่งเข้า /api/watchtower-relay ซึ่งอยู่ใน origin เดียวกับแอป ad blocker จึงไม่รู้ว่าเป็น traffic ของ Sentry และไม่บล็อก
  • ก่อนลงฐานข้อมูล ทุก event ถูกลบข้อมูลลับ (รวมถึงค่าของแถวใน SQL error) ตัดขนาดไม่ให้เกิน 200 KB และนับ rate limit ต่อโปรเจกต์และต่อ issue

ติดตั้งแบบ standalone

composer require phattarachai/watchtower-laravel
php artisan watchtower:install --standalone
npm run build
php artisan watchtower:doctor

watchtower:install --standalone ทำให้ครบ

  1. migrate ตาราง watchtower_* 6 ตาราง แล้วสร้างโปรเจกต์แรกตามชื่อ APP_NAME
  2. เขียน env และ patch bootstrap/app.php ให้ Sentry จับ exception ระหว่างนี้จะถามว่าจะส่งข้อมูลส่วนตัว (PII) ไปกับ event ไหม ค่าเริ่มต้นคือไม่ส่ง
  3. ถ้าแอปใช้ Inertia จะ publish หน้า resources/js/pages/Watchtower.jsx เพิ่ม alias @watchtower ใน Vite และสร้าง resources/css/watchtower.css ซึ่งเป็น stylesheet แยกของหน้า UI คลาสทุกตัวมี prefix tw: จึงไม่ชนกับ CSS ของแอป
  4. ถ้าเครื่องมีคำสั่ง claude จะลงทะเบียน MCP server ให้ Claude Code ใน .mcp.json ของโปรเจกต์

คำสั่งนี้รันซ้ำได้ไม่มีผลเสีย และใส่ --dry-run เพื่อดูก่อนว่าจะแก้อะไรบ้าง

ให้สิทธิ์เข้าหน้า /watchtower

สิ่งเดียวที่ต้องทำเองคือกำหนดว่าใครเข้าหน้า issue ได้ บนเครื่อง local เปิดให้ทุกคน ส่วน environment อื่นตอบ 403 จนกว่าจะกำหนด

use Phattarachai\WatchtowerLaravel\Watchtower;

public function boot(): void
{
    Watchtower::auth(fn ($request): bool => $request->user()?->isAdmin() === true);
}

หรือจะประกาศ gate ชื่อ viewWatchtower แทนก็ได้

ตรวจว่าครบ

watchtower:doctor ไล่ตรวจทุกอย่างที่แอปต้องมี ได้แก่ ตาราง, route, หน้า UI, alias ของ Vite, บรรทัด Tailwind, โปรเจกต์ที่ active, mailer, queue, MCP และ self-capture แล้วบอกว่าข้อไหนยังขาด

หน้า /watchtower

รายการ issue

หน้ารายการ issue ของ Watchtower error ที่มีต้นเหตุเดียวกันถูกรวมเป็น issue เดียว พร้อมจำนวน event และจำนวนผู้ใช้ที่เจอ

  • แท็บด้านบนแยก issue ตามสถานะ คือ unresolved, resolved, ignored และ snoozed
  • ค้นจากชื่อ issue และกรองตามโปรเจกต์ ระดับ (debug ถึง fatal) และ environment ได้
  • แต่ละแถวบอกว่าเป็น error ฝั่ง php หรือ javascript เกิดครั้งแรกและล่าสุดเมื่อไหร่ เกิดไปกี่ครั้ง กับผู้ใช้กี่คน
  • ชื่อ issue ถูกแปลงตัวเลขยาว ๆ เป็น <N> วันที่เป็น <DATE> และข้อความในเครื่องหมายคำพูดเป็น <STR> เพื่อให้ error แบบเดียวกันที่ต่างกันแค่ค่า เช่นเลขออเดอร์ รวมเป็น issue เดียว ข้อความเต็มดูได้ในหน้ารายละเอียด

จัดการหลาย issue พร้อมกัน

เลือกหลาย issue แล้วสั่ง snooze ติ๊กหลายแถวแล้วสั่ง Resolve, Ignore, Snooze หรือ Delete ได้ในครั้งเดียว

Snooze คือซ่อน issue ไว้ชั่วคราว 1 ชั่วโมงถึง 30 วัน เหมาะกับ error ที่รู้สาเหตุแล้วแต่ยังแก้ไม่ได้ทันที เช่น API ของผู้ให้บริการภายนอกล่ม

รายละเอียด issue

หน้ารายละเอียด issue พร้อม stack trace stack trace เปิดเฟรมแรกที่เป็นโค้ดของแอปไว้ให้ ไฮไลต์บรรทัดที่เกิด error และแสดง exception ต้นเหตุต่อท้าย

  • แถบ event บอก event id, environment, release ([email protected]) และ route ที่เกิด error กด Newer / Older เพื่อดู event อื่นของ issue เดียวกัน หรือเลือกจาก Recent events ด้านขวา
  • Stacktrace แสดง exception ที่ถูก throw ก่อน แล้วตามด้วยส่วน Caused by ของ exception ที่เป็นต้นเหตุ เช่น PDOException ที่อยู่ใต้ error ของ Laravel ในแต่ละส่วน เฟรมของโค้ดแอปแสดงก่อน ส่วนเฟรมของ vendor ซ่อนไว้ใต้ "Show vendor frames"
  • Copy Markdown คัดลอกรายละเอียดของ issue เป็น Markdown ที่วางให้ Claude อ่านต่อได้ทันที
  • Resolve, Ignore และ Snooze 24h อยู่มุมขวาบน ถ้า issue ที่ resolve หรือ ignore ไว้กลับมาเกิดอีก จะนับเป็น regression และกลับไปอยู่ในแท็บ unresolved

แท็บอื่นของ event เดียวกันช่วยให้รู้ว่าเกิดอะไรขึ้นก่อน error

แท็บ Request ของ event Request: URL, method, header และข้อมูลที่ส่งมา cookie ถูกเปลี่ยนเป็น [Filtered] ก่อนบันทึก

แท็บ Breadcrumbs ของ event Breadcrumbs: cache และ SQL ที่รันก่อน error ค่าในคำสั่ง SQL เป็น ? ทั้งหมด จึงไม่มีข้อมูลลูกค้าติดมา

ส่วนแท็บ User บอกว่า error เกิดกับผู้ใช้คนไหน (id, email, name จาก @watchtowerUser) และแท็บ Tags แสดง tag ที่ SDK ส่งมา เช่น locale และ route

error จากเบราว์เซอร์

issue ที่มาจาก JavaScript ในเบราว์เซอร์ error ฝั่ง JavaScript อยู่ในรายการเดียวกับ error ฝั่ง PHP แต่ละเฟรมบอกไฟล์ที่ Vite build ออกมา พร้อมบรรทัดและคอลัมน์

แจ้งเตือนทางอีเมล

หน้า Alert rules ตั้งได้หลายกฎ แต่ละกฎเลือกโปรเจกต์ ระดับขั้นต่ำ environment และผู้รับ

กฎมี 4 แบบ

แบบ ส่งอีเมลเมื่อ
New issue มี issue ใหม่ที่ไม่เคยเกิดมาก่อน
Regression issue ที่ resolve หรือ ignore ไว้ หรือหมดเวลา snooze แล้ว กลับมาเกิดอีก
Threshold spike เกิดเกินจำนวนที่ตั้งภายในช่วงเวลา เช่น 20 ครั้งใน 300 วินาที
Milestone issue หนึ่งเกิดครบจำนวนที่ตั้ง เช่น 500 ครั้ง

Cooldown กันไม่ให้กฎเดิมส่งอีเมลซ้ำถี่เกินไป ปุ่ม Send test ส่งอีเมลทดสอบให้ผู้รับได้ทันที และปิดกฎชั่วคราวได้โดยไม่ต้องลบ (กฎที่ปิดอยู่ขึ้นป้าย paused)

โปรเจกต์และ DSN

หน้า Settings → Projects แต่ละโปรเจกต์มี DSN ของตัวเอง พร้อมตัวอย่างการตั้งค่าสำหรับ Laravel, JavaScript, Next.js และ WordPress

key ถูกซ่อนไว้ทุกที่ทั้งใน DSN และตัวอย่างการตั้งค่า กด Reveal key เมื่อต้องการดู ส่วนปุ่ม Copy คัดลอกค่าจริงให้เสมอ นอกจากนี้ยังสร้างโปรเจกต์ใหม่ เปลี่ยน key (Rotate key) หรือปิดรับ event ของโปรเจกต์ได้จากหน้านี้ ทำแบบเดียวกันจาก command line ได้ด้วย watchtower:project (ดูหัวข้อ "รับ error จากแอปอื่นด้วย")

ฝั่งเบราว์เซอร์

ตอนติดตั้ง แพ็กเกจจะวาง helper ไว้ที่ resources/js/vendor/watchtower.js ซึ่งตั้งค่า Sentry ไว้ให้แล้ว ทั้ง tunnel, ไม่เก็บข้อมูลส่วนตัว และไม่รับ error จาก browser extension ใน entry ของ Vite เรียกแค่นี้

import { initWatchtower } from './vendor/watchtower.js';

initWatchtower();

ใส่ directive นี้ใน <head> ของ layout หลัก error จากเบราว์เซอร์จะได้รู้ว่าเกิดกับผู้ใช้คนไหน

<head>
    @watchtowerUser
</head>

@watchtowerUser สร้าง meta tag 3 ตัว คือ id, email และ name ของผู้ใช้ที่ล็อกอินอยู่ ถ้าเป็น Filament panel ซึ่งไม่ได้ใช้ layout หลัก ให้ใส่ผ่าน render hook

$panel->renderHook(
    PanelsRenderHook::HEAD_END,
    fn (): string => Blade::render('@watchtowerUser'),
);

ให้ Claude ช่วยไล่ issue ผ่าน MCP

ถ้าติดตั้ง laravel/mcp ไว้ แอปจะมี MCP server ที่ /watchtower/mcp ยืนยันตัวด้วย public key ของโปรเจกต์ และ watchtower:install ลงทะเบียนให้ Claude Code อัตโนมัติ

composer require laravel/mcp

จากนั้นถาม Claude ได้เลย เช่น "มี issue อะไรใหม่ตั้งแต่ deploy เมื่อวาน" Claude จะเรียก tool อย่าง list_issues, get_issue และ list_events เพื่ออ่าน stack trace จริง ไล่หาสาเหตุในโค้ด แล้วใช้ resolve_issue, ignore_issue หรือ snooze_issue ปิดงานให้ เมื่อแก้เสร็จ

รับ error จากแอปอื่นด้วย

แอปที่ติดตั้งแบบ standalone เป็นที่รับ error ให้ระบบอื่นที่ใช้ Sentry SDK ได้ด้วย เช่น เว็บ WordPress หรือ frontend Next.js ที่ทำงานคู่กัน สร้างโปรเจกต์ใหม่แล้วเอา DSN ไปใส่

php artisan watchtower:project create "Storefront"
php artisan watchtower:project list

DSN มีรูปแบบ https://{public_key}@{host}/watchtower/{project_id} คือมี prefix watchtower อยู่หน้าเลขโปรเจกต์ และไม่มี api เพราะ SDK เติม /api/{project_id}/envelope/ เอง ถ้าใส่ api เองหรือไม่ใส่ prefix event จะได้ 404 แบบเงียบ ๆ

ก่อนขึ้น production

เพิ่ม supervisor ใน Horizon และ deploy ให้เรียบร้อยก่อน แล้วค่อยชี้ Watchtower ไปที่ queue นั้น ถ้าทำกลับกัน event จะไปกองอยู่ใน queue ที่ไม่มีใครรับ

'app-supervisor-watchtower' => [
    'connection' => 'redis',
    'queue' => ['watchtower'],
    'balance' => false,
    'maxProcesses' => 1,
    'tries' => 3,
    'timeout' => 60,
],

ทำไมต้องแยก queue และจำกัด memory: ตัวติดตาม error อยู่ในแอปเดียวกัน ใช้ queue และ Redis ชุดเดียวกับงานอื่นของแอป ถ้าวันไหน error ทะลักเข้ามาพร้อมกันจำนวนมาก งานของ Watchtower ต้องไม่ไปแย่งที่จนอีเมลหรือการชำระเงินของแอปค้าง และต้องไม่ทำให้ Redis เต็มจนทั้งแอปล่ม แพ็กเกจจึงกันไว้หลายชั้น

  • ไม่รายงาน error ของตัวเอง ถ้า job ที่ประมวลผล event ล้มเหลว error นั้นจะถูกทิ้ง ไม่ถูกส่งกลับเข้า Watchtower เป็น job ใหม่ ซึ่งจะวนซ้ำไม่จบ
  • จำกัดจำนวนที่รับ มี rate limit ต่อโปรเจกต์และต่อ issue และเมื่อมีงานค้างใน queue เกิน 5,000 ตัว event ที่เข้ามาใหม่จะถูกนับไว้แต่ไม่เพิ่มเข้า queue
  • จำกัดขนาด แต่ละ event ถูกตัดให้ไม่เกิน 200 KB ก่อนเข้า queue

เมื่อตั้ง Redis เป็น noeviction ถ้า Redis เต็มจริง ๆ ระบบจะปฏิเสธการเขียน event ที่เข้ามาตอนนั้นจะไม่ถูกเก็บ แต่ request ของผู้ใช้ยังทำงานต่อได้ตามปกติ

ค่าที่ปรับได้ใน .env

ตัวแปร ค่าเริ่มต้น ใช้ทำอะไร
WATCHTOWER_RATE_LIMIT_PER_MIN 300 event ต่อนาทีต่อโปรเจกต์
WATCHTOWER_RATE_LIMIT_PER_FINGERPRINT_PER_MIN 20 event ต่อนาทีต่อ issue ส่วนที่เกินนับไว้แต่ไม่เก็บ
WATCHTOWER_MAX_QUEUE_DEPTH 5000 งานค้างเกินนี้จะนับไว้แต่ไม่เข้า queue
WATCHTOWER_RETENTION_DAYS 90 เก็บ event กี่วัน (watchtower:prune รันทุกวันเอง)
WATCHTOWER_REDACT_SQL_VALUES true ลบค่าของแถวออกจาก SQL error

ข้อควรรู้

  • หน้า UI ต้องใช้ Inertia + React และ Tailwind v4 ผ่าน Vite แพ็กเกจไม่บังคับติดตั้ง Inertia ถ้าแอปเป็น Livewire, Filament หรือ Blade ให้ตั้ง WATCHTOWER_UI_ENABLED=false แอปจะยังรับ error, self-capture, ส่งอีเมลแจ้งเตือน และมี MCP ครบ แค่ไม่มีหน้า /watchtower ก็ใช้ Claude ไล่ issue แทน
  • หน้า UI ตามธีมของแอป ถ้าแอปใช้ dark mode ด้วยคลาส .dark หน้า /watchtower จะเป็นสีเข้มตาม ถ้าอยากล็อกไว้ ตั้ง WATCHTOWER_UI_THEME เป็น light หรือ dark
  • ไม่ต้องมี Redis ก็ใช้ได้ queue แบบ sync ก็รับ event ได้ แต่ event จะถูกประมวลผลระหว่าง request ที่รายงาน error นั้น บน production จึงควรมี queue worker
  • โหมด relay และ dual มีไว้สำหรับส่ง error ไปที่ Watchtower server กลาง ซึ่งเราใช้ภายในกับแอปหลายตัว โหมดที่ใช้งานได้ทันทีคือ standalone

เวอร์ชันที่รองรับ

  • PHP 8.4 ขึ้นไป
  • Laravel 12 และ 13
  • sentry/sentry-laravel 4.x (ติดตั้งมาพร้อมแพ็กเกจ)

GitHub: phattarachai/watchtower-laravel

อ่านต่อ

ล่าสุด

ดูทั้งหมด →