hocvt / log-viewer-remote
Mở rộng opcodesio/log-viewer: auth shared-secret giữa các host, tải file log của host ở xa qua host đang xem, và log query chậm / N+1.
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- opcodesio/log-viewer: ^3.0
Requires (Dev)
- livewire/livewire: ^4.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Mở rộng opcodesio/log-viewer cho mô hình "một Log Viewer xem log của nhiều host":
- Auth thống nhất. Request server-to-server giữa các host đi bằng
Authorization: Bearer <shared secret>. Người dùng thật thì hỏi GateviewLogViewer. - Tải file log của host ở xa. Package gốc không tải được: nút Download gọi XHR sang
origin khác (CORS chặn), và route download bên đó vẫn có
AuthorizeLogViewernên trình duyệt không có session cũng 403. Package viết lạidownload_urlvề origin hiện tại rồi tải hộ server-to-server, stream về client. - Bớt cấu hình tay. Hosts khai bằng env,
api_stateful_domainsmặc định theoAPP_URL. - Slow query log. Log query chậm và cảnh báo request/job có quá nhiều query (N+1), che sạch bí mật trong SQL lẫn URL, truy vết về đúng file Blade / component Livewire. Mặc định tắt — xem Slow query log.
- Agent phân tích log từ xa. Lệnh
log-viewer-remote:aggregate/:entriesgom số liệu (lỗi, trang chậm, N+1, job tốn DB…) ngay trên host có file, không tải file về; token chỉ đọc, giới hạn theo channel — xem Agent.
Cài
Trên mọi host (host xem và host bị xem):
composer require hocvt/log-viewer-remote
php artisan log-viewer-remote:secret # sinh LOG_VIEWER_SHARED_SECRET, copy sang các host khác
Trên host xem log, thêm vào .env:
LOG_VIEWER_HOSTS="web=https://app.example.com/log-viewer,api=https://api.example.com/log-viewer"
Trên host chạy production, mở quyền xem trong AppServiceProvider::boot():
Gate::define('viewLogViewer', fn (?User $user): bool => (bool) $user?->isAdmin());
Mặc định của package: local cho qua, môi trường khác chặn hết.
App không dùng Gate được
Có app mà mọi lần gọi Gate với user đã đăng nhập đều ném TypeError: cài
spatie/laravel-permission (nó đăng ký một Gate::before nhận
Illuminate\Contracts\Auth\Access\Authorizable) trong khi model User không
implement contract đó. Thường gặp ở codebase cũ tự làm tầng phân quyền riêng và đã có
sẵn User::can() với ngữ nghĩa khác, nên không thể implement Authorizable mà không
làm gãy chỗ khác.
Những app đó bỏ qua tầng Gate bằng cách cắm callback riêng, đường shared secret giữa các host vẫn giữ nguyên:
use HocVT\LogViewerRemote\LogViewerRemote; LogViewerRemote::authorizeUsing( fn (Request $request): bool => app()->isLocal() || access()->hasRoles(['Administrator']) );
Cắm callback thì Gate viewLogViewer không còn được hỏi tới.
Kiểm tra kết nối:
php artisan log-viewer-remote:check
Đổi prefix mặc định
Nên đổi. Mặc định Log Viewer nằm ở /log-viewer — ai cũng đoán được, và trang này
phơi toàn bộ log ứng dụng. Gate chặn người lạ, nhưng một URL không đoán được sẽ cắt
luôn lượt dò của bot trước khi chạm tới tầng xác thực. Publish config rồi đổi:
php artisan vendor:publish --tag=log-viewer-config
// config/log-viewer.php 'route_path' => 'sys/x9f2logs', // nhiều đoạn cũng được
route_path không đọc env, nên bắt buộc publish config mới đổi được. Đổi xong nhớ
php artisan optimize:clear (hoặc route:cache lại nếu đang cache route).
Toàn bộ route đi theo prefix mới, kể cả hai route của package này, và thứ tự vẫn đúng
— chúng vẫn đứng trước route bắt-tất {prefix}/{view?}.
Ba điều cần nhớ khi đã đổi:
-
Prefix của mỗi host là độc lập. Host A xem log host B thì chỉ cần URL trong
LOG_VIEWER_HOSTStrỏ đúng gốc Log Viewer của B. Hai bên đặt prefix khác nhau vẫn chạy. -
Khai host phải kèm prefix. Chỉ URL trần mới được tự bù, và giá trị bù là
route_pathcủa chính host đang khai — tức đoán cả cụm dùng chung quy ước:LOG_VIEWER_HOSTS="a=https://a.example.com/sys/x9f2logs,b=https://b.example.com"bsẽ thànhhttps://b.example.com/<route_path của host này>. Nếubđặt prefix khác, phải viết đủ. Sai prefix thìlog-viewer-remote:checkbáo404 — sai route_path. -
Đừng hardcode
/log-viewertrong test hay link. Dùngconfig('log-viewer.route_path'), không thì đổi prefix là gãy hàng loạt.
Cấu hình
Cả package dùng một file config/log-viewer-ext.php (key log-viewer-ext): auth / host
xa ở gốc, nhóm agent, nhóm slow_log. Publish: php artisan vendor:publish --tag=log-viewer-ext-config.
Provider gộp đệ quy với file của app: app chỉ ghi key mình đổi, kể cả trong nhóm lồng
nhau; danh sách thì app thay hẳn.
Bản ≤ 1.2 dùng hai file config/log-viewer-remote.php + config/slow-log.php: app còn giữ thì
vẫn được đọc (xếp dưới file mới), log-viewer-remote:check nhắc chuyển. Env giữ nguyên tên.
Các key ở gốc:
| Key | Env | Ý nghĩa |
|---|---|---|
shared_secret |
LOG_VIEWER_SHARED_SECRET (fallback LOG_VIEWER_PRODUCTION_TOKEN) |
Bearer token giữa các host |
hosts |
LOG_VIEWER_HOSTS |
id=url,id2=url2; merge vào log-viewer.hosts, không ghi đè host đã khai tay |
timeout.request / timeout.download |
— | Giây, gọi sang host xa |
Host cần tên đẹp, basic auth hay header riêng thì khai trong config/log-viewer.php
như bình thường; package chỉ điền những id chưa có.
Slow query log
SLOW_LOG_ENABLED=true
Tuỳ chọn log thêm request xử lý quá lâu và response quá lớn theo prefix path
(SLOW_LOG_REQUEST_TIME_PREFIXES, SLOW_LOG_RESPONSE_SIZE_PREFIXES, mặc định tắt).
Không cần đăng ký thêm provider. Mặc định ghi ra channel riêng slow-log
(storage/logs/slow-log-YYYY-MM-DD.log). Truy vết về ⚡component Livewire 4 và bản vá
Debugbar mặc định tắt — app Livewire bật bằng SLOW_LOG_TRACE_LIVEWIRE=true,
SLOW_LOG_DEBUGBAR_LIVEWIRE=true. Config ở nhóm slow_log của log-viewer-ext.
Tài liệu đầy đủ (tích hợp, ngưỡng, cách che dữ liệu, đọc log): docs/slow-log.md.
Agent phân tích log từ xa
php artisan log-viewer-remote:secret --agent --show # một lần / project, dán LOG_VIEWER_AGENT_TOKEN vào .env mọi host php artisan log-viewer-remote:check # cột Agent: phiên bản + channel cho phép của từng host php artisan log-viewer-remote:aggregate --host=m1 --channel=slow-log --date=2026-10-05 php artisan log-viewer-remote:entries --host=m1 --at=slow-log-2026-10-05.log@1124892
Host bị đọc khai channel agent token được đọc (LOG_VIEWER_AGENT_CHANNELS=slow-log,daily;
mặc định slow-log; shared secret không bị giới hạn). Agent token chỉ gọi được api/agent/* —
không tải / xoá file, không vào UI. levels / messages đọc mọi log, 4 bảng còn lại chỉ đọc
slow log; câu hỏi riêng thì tự gửi --match / --group (regex). Đi vòng qua host đang xem:
--via=web --host=m1. Mỗi project một token, máy chạy agent không giữ shared secret của
production — đủ khuyến nghị ở docs/agent.md. Skill cho
Claude Code: php artisan vendor:publish --tag=log-viewer-remote-skill.
Tài liệu đầy đủ (bảy thao tác hay làm, cách đọc số, HTTP API, cấu hình): docs/agent.md.
Những chỗ dễ làm hỏng khi sửa
-
Host trả ra ngoài không có credential. Vendor đưa nguyên
LogViewer::getHosts()vàowindow.LogViewervà trả cảauthở/api/hosts— ai xem được Log Viewer cũng đọc được shared secret trong mã nguồn trang. Package bỏauth/headersquaLogViewer::resolveHostsUsing(); credential thật chỉ đọc quaSupport\HostCredentials(thẳng từconfig('log-viewer.hosts')), gắn vào request quaSupport\RemoteHttp. Vendor chỉ giữ một resolver: app tự gọiresolveHostsUsing()là ghi đè mất bản vá này. -
Forward tự làm, không gọi vendor.
ForwardRequestToHostkhông gọiparent::handle()vì bản vendor đọc credential từ$host->auth(đã rỗng). Đường forward giữ như vendor (URL, headerX-Forwarded-*, không gửi body), thêm timeouttimeout.forwardvà chépRetry-After. -
Route nạp ở
register(), không ởboot(). Package gốc đăng ký route bắt-tấtlog-viewer/{view?}(where '.*') trongboot(); mọi route dưới/log-viewerkhai sau nó đều bị nuốt. -
Thay middleware vendor bằng container binding.
ForwardRequestToHostMiddlewaređược gắn cứng trongroutes/api.phpcủa package gốc; Pipeline resolve middleware bằngcontainer->make()nênbind()là đủ. -
Không gửi
X-Forwarded-Hostkhi xin link ký. Host xa phải ký URL bằng domain của chính nó, nếu không chữ ký hỏng khi gọi lại. -
Stream theo chunk, không
->body(). Log production có thể vài trăm MB. -
Gate mặc định định nghĩa trong
boot()của package. App provider boot sau nênGate::definetrongAppServiceProviderghi đè được.
Gỡ rối
File tải về bị chèn rác ở đầu (vài byte khoảng trắng), md5 không khớp.
App có output lọt ra trước response — thường là file PHP có khoảng trắng trước <?php
hoặc sau ?>, và file đó được nạp ở mọi request (route file, config, helper). Rác đó
chui vào đầu MỌI response tải file của app, không riêng Log Viewer. Tìm bằng:
php -r '$b=file_get_contents($argv[1]); exit(str_starts_with($b,"<?php")?0:1);' <file>
Đi qua proxy thì rác nhân đôi, vì cả host xem lẫn host xa đều thêm phần của mình.
UI 403 toàn bộ API. EnsureFrontendRequestsAreStateful chưa nhận domain hiện tại.
Package tự điền theo APP_URL; nếu truy cập bằng domain khác thì khai
LOG_VIEWER_API_STATEFUL_DOMAINS.
log-viewer-remote:check báo 403. Token hai bên lệch, hoặc host xa chưa cài package.
Identifier file khác nhau giữa CLI và web. Identifier là hash của đường dẫn tuyệt
đối, mà CLI và php-fpm có thể resolve storage_path() khác nhau. Luôn lấy identifier
từ cùng tiến trình sẽ tải — luồng UI vốn đã đúng, chỉ sai khi copy tay từ tinker.
Mặc định package gốc quét cả /var/log/nginx/* và vài đường dẫn hệ thống. Nếu không
muốn, publish config/log-viewer.php rồi thu hẹp include_files.
Test
composer install && composer test # ~134 test, vài giây, không cần DB / mạng
CI (.github/workflows/tests.yml) chạy bộ test trên PHP 8.3 và 8.5, với Laravel 11 / 12 / 13.
Riêng cặp Laravel 11 × PHP 8.5 bị loại, vì Laravel 11 chỉ hỗ trợ tới PHP 8.4. Code của package
gây deprecation (gọi hàm PHP hoặc API vendor đã deprecate) thì test đỏ. Laravel nuốt
deprecation vào kênh log, nên tests/Feature/TestCase có lớp chặn riêng cho file trong src/.
tests/Unit— phần PHP thuần (bộ đọc log của agent,ChannelFiles). Chạy được cả bằng phpunit của app đang dùng package:vendor/bin/phpunit -c <package>/phpunit.xml --testsuite Unit.tests/Feature— phần cần Laravel trên orchestra/testbench: endpoint agent (token, channel, cursor, forward, link UI, loại log), lệnh, vá lộ secret, tải file host xa, SqlLogger, RequestLogger, gộp config. File log ở thư mục tạm, host xa làHttp::fake, người xem là user không lưu DB.
Project dùng package chỉ cần test những gì nó tự quyết (Gate viewLogViewer, giá trị trong
config/log-viewer-ext.php) — không chép lại test của package.