ahmedmerza / logscope
A fast, database-backed log viewer for Laravel applications
Requires
- php: ^8.2
- illuminate/contracts: >=10.0
- illuminate/database: >=10.0
- illuminate/support: >=10.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
- dev-master
- v1.7.1
- v1.7.0
- v1.6.1
- v1.6.0
- v1.5.9
- v1.5.8
- v1.5.7
- v1.5.6
- v1.5.5
- v1.5.4
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5.0
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.1
- v1.3.0
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.0
- v0.6.1
- v0.6.0
- v0.5.2
- v0.5.1
- v0.5.0
- v0.4.0
- v0.3.2
- v0.3.1
- v0.3.0
- v0.2.2
- v0.2.1
- v0.2.0
- v0.1.0
- dev-fix/search-not-complement-issue-24
This package is auto-updated.
Last update: 2026-05-20 05:52:03 UTC
README
A beautiful, database-backed log viewer for Laravel applications. Production-ready.
Quick Start
composer require ahmedmerza/logscope php artisan logscope:install php artisan migrate
Visit /logscope in your browser. That's it!
What's New
Latest: v1.7.1 β Search NOT toggle is now a true boolean complement (#24): include + exclude == total for any input. A stray colon in a phrase no longer fragments your search, and rows with NULL columns no longer disappear from both views.
See CHANGELOG.md for full release history and behavior-change notes.
Table of Contents
- Features
- Requirements
- When to Use LogScope
- Installation
- Configuration
- Usage
- Production Deployment
- Customization
- Contributing
- License
β¨ Features
| Feature | Description |
|---|---|
| Zero-Config Capture | Automatically captures ALL logs from ALL channels |
| Request Context | Trace ID, user ID, IP, URL, and user agent for every log |
| Advanced Search | Search syntax (field:value), regex support, NOT toggle |
| Smart Filters | Include/exclude by level, channel, HTTP method, date range |
| Active Filters Bar | See all active filters at a glance, clear individually |
| Channel Search | Search and filter channels when you have many |
| JSON Viewer | Syntax-highlighted, collapsible JSON with copy support |
| Smart Context | Auto-expand Request/Model objects, redact sensitive data |
| Status Workflow | Track logs as open, investigating, resolved, or ignored |
| Log Notes | Add investigation notes to any log entry |
| Quick Filters | One-click filters for common queries |
| Keyboard Shortcuts | 14 shortcuts for navigation, status changes, and actions |
| Dark Mode | Full dark mode support with persistence |
| Shareable URLs | Current filters reflected in URL for sharing |
| Deep Linking | Link directly to specific log entries |
| Performance | Keyset pagination, batch writes, query optimization, proper indexing |
π Requirements
- PHP 8.2+
- Laravel 10+
- SQLite, MySQL, or PostgreSQL
π€ When to Use LogScope
LogScope stores logs in your database - a deliberate choice that works great for most Laravel apps.
Great fit if you:
- Want log visibility without external services
- Have a typical Laravel app (up to ~100K requests/day)
- Need rich search and filtering
- Prefer simplicity over infrastructure complexity
Consider alternatives if you:
- Process millions of requests daily
- Need months/years of log retention
- Already use centralized logging (Datadog, CloudWatch, ELK)
How LogScope handles common concerns:
| Concern | Solution |
|---|---|
| Database bloat | Retention policies with scheduled pruning (default: 30 days) |
| Performance | Batch mode writes logs after response is sent |
| Query speed | Proper indexes on common filter combinations |
β οΈ Known Limitations
LogScope captures everything that flows through Laravel's logger (Illuminate\Log\Logger). A few categories of logs structurally bypass that path and cannot be auto-captured. These are PHP / framework limitations, not bugs we can fix from a Composer package.
1. PHP's native error_log() is not interceptable
error_log("...") is a built-in PHP function that writes directly to the destination configured in php.ini's error_log directive (file, syslog, or stderr). It calls into PHP's C-level logging, never invokes set_error_handler, and there's no userland API to redirect it. Workarounds (the uopz extension, php.ini overrides, stderr capture) all live outside the PHP application β out of scope for a package.
If your app or a dependency calls error_log(), those messages land in your php-fpm/server log, not in LogScope. Search both places when investigating.
2. trigger_error(E_USER_*) should work, but depends on Laravel's exception handler
PHP routes trigger_error() through the registered set_error_handler, and Laravel's HandleExceptions bootstrapper installs one that converts these to ErrorException and reports them β which then fires MessageLogged and reaches LogScope. So in normal HTTP/CLI flow this works.
It can break in specific contexts where Laravel's exception handler is bypassed:
php artisan tinker(skips reporting viarunningUnitTests()-style guards in some versions)- Custom
set_error_handlercalls in user code that don't chain to the previous handler error_reportingbeing lowered to excludeE_USER_*levels
If something seems missing here, check error_reporting() and that \Illuminate\Foundation\Bootstrap\HandleExceptions::class is in your bootstrap chain (it is by default in Laravel 10/11/12).
3. Direct Monolog instances bypass capture
If a dependency (or your own code) does:
$logger = new \Monolog\Logger('foo'); $logger->error('something');
β¦that's a raw Monolog logger, not Laravel's Illuminate\Log\Logger. Only Laravel's logger fires MessageLogged. The Monolog instance has no way to know LogScope exists.
Opt-in workaround: push our handler onto the Monolog instance:
use LogScope\Logging\LogScopeHandler; $logger = new \Monolog\Logger('foo'); $logger->pushHandler(new LogScopeHandler('foo')); // 'foo' = the channel name to record $logger->error('captured by LogScope now');
Auto-instrumentation isn't possible β we'd have to patch the Monolog\Logger class itself.
4. SIGKILL / segfault / E_PARSE in batch write mode loses the buffered batch
Batch mode (LOGSCOPE_WRITE_MODE=batch, default) accumulates logs during the request and flushes them on app->terminating() or PHP shutdown. Both safety nets require a graceful shutdown:
kill -9(SIGKILL): cannot be caught by PHP β buffer is gone- OOM kill: same
E_PARSE/E_COMPILE_ERROR: PHP can't run user code at shutdown for these
If low-loss is critical, set LOGSCOPE_WRITE_MODE=sync to write every log immediately. Cost: each Log::*() call adds a synchronous DB round-trip.
5. null_channel filter β read before enabling
LOGSCOPE_IGNORE_NULL_CHANNEL=true drops logs that LogScope can't attribute to a named channel. This includes Laravel's own framework-level error reporter in some configurations, which means enabling this flag may silently drop unhandled exceptions. Only enable if you know exactly which no-channel logs flow through your app.
π¦ Installation
composer require ahmedmerza/logscope
Run the install command:
php artisan logscope:install php artisan migrate
Access the dashboard at /logscope.
βοΈ Configuration
After installation, configure LogScope in config/logscope.php or via environment variables.
Capture Mode
# 'all' (default) - Capture all logs automatically # 'channel' - Only capture logs sent to the logscope channel LOGSCOPE_CAPTURE=all
Write Mode (Performance)
# 'batch' (default) - Buffer logs, write after response # 'sync' - Write immediately (simple, but slower) # 'queue' - Queue each log entry (best for high-traffic) LOGSCOPE_WRITE_MODE=batch # Queue settings (when using 'queue' mode) LOGSCOPE_QUEUE=default LOGSCOPE_QUEUE_CONNECTION=
Testing
LogScope forces write_mode to sync whenever the app is running in the testing environment, regardless of what LOGSCOPE_WRITE_MODE or config/logscope.php says. This mirrors how Laravel ships sensible test defaults for mail (array), queue (sync), and cache (array).
Why: in batch mode the buffer is flushed on Application::terminate(). Unit tests that never dispatch a request never trigger that callback, so entries accumulate across tests and get discarded at PHP shutdown β producing both noisy stderr warnings and silent loss of the captured logs. sync writes land inside the test's transaction and roll back cleanly with RefreshDatabase / DatabaseTransactions.
If you specifically want to exercise batch behavior in a test, opt back in inside setUp():
protected function setUp(): void { parent::setUp(); config(['logscope.write_mode' => 'batch']); }
Even with that override, the shutdown discard warning is suppressed in the testing env (the loss is expected and uninteresting) β production keeps the loud notify so real data loss stays visible.
Retention
LOGSCOPE_RETENTION_ENABLED=true LOGSCOPE_RETENTION_DAYS=30 # Optional: let LogScope register the prune schedule for you (off by default). # Leave off if you already wire `logscope:prune` in your own console kernel. LOGSCOPE_RETENTION_AUTO_SCHEDULE=false LOGSCOPE_RETENTION_SCHEDULE_AT=03:00
Note: Retention requires either flipping
LOGSCOPE_RETENTION_AUTO_SCHEDULE=trueor schedulinglogscope:pruneyourself - see Schedule Pruning.
Features
LOGSCOPE_FEATURE_STATUS=true # Enable status workflow LOGSCOPE_FEATURE_NOTES=true # Add notes to logs
Noise Reduction
# Filter out noisy logs LOGSCOPE_IGNORE_DEPRECATIONS=true # Skip "is deprecated" messages (default: true) LOGSCOPE_IGNORE_NULL_CHANNEL=false # Skip logs without a channel (default: false)
Note:
null_channeldefaults tofalsebecauseLog::build()(dynamic loggers) produce logs without a channel. Setting this totruewould filter out those logs.
Cache TTL
# How long (in seconds) to cache stats and filter options (levels, channels, etc.) # Set to 0 to disable caching LOGSCOPE_CACHE_TTL=60
JSON Viewer
Configure collapsible JSON behavior in config/logscope.php:
'json_viewer' => [ 'collapse_threshold' => 5, // Auto-collapse arrays/objects larger than this 'auto_collapse_keys' => ['trace', 'stack_trace', 'stacktrace', 'backtrace'], ],
Routes
LOGSCOPE_ROUTES_ENABLED=true LOGSCOPE_ROUTE_PREFIX=logscope LOGSCOPE_DOMAIN= LOGSCOPE_FORBIDDEN_REDIRECT=/ LOGSCOPE_UNAUTHENTICATED_REDIRECT=/login
Add middleware and configure error redirects:
'routes' => [ 'middleware' => ['web', 'auth'], 'forbidden_redirect' => '/', // Where to redirect on 403 (access denied) 'unauthenticated_redirect' => '/login', // Where to redirect on 401/419 (session expired) ],
Error Handling
LogScope handles errors gracefully with toast notifications:
| Error | Behavior |
|---|---|
| 401/419 (Session expired) | Toast + redirect to unauthenticated_redirect |
| 403 (Access denied) | Toast + redirect to forbidden_redirect |
| 429 (Rate limited) | Toast only (retry later) |
| 500+ (Server error) | Toast only (temporary issue) |
| Network error | Toast only (check connection) |
Note: Redirect URLs can be relative paths (
/login) or absolute URLs (https://auth.example.com/login).
π Usage
Automatic Capture
All logs are captured automatically - no code changes needed:
Log::info('User logged in', ['user_id' => 1]); Log::channel('slack')->error('Payment failed'); Log::stack(['daily', 'slack'])->warning('Low inventory');
Keyboard Shortcuts
| Key | Action |
|---|---|
j / k |
Navigate down / up |
h / l |
Previous / next page |
r |
Refresh data |
Enter |
Open detail panel |
Esc |
Close panel |
/ |
Focus search |
y |
Copy context (yank) |
n |
Focus note field |
c |
Clear all filters |
d |
Toggle dark mode |
? |
Show keyboard help |
Status shortcuts (require Shift, context-aware β behavior change since v1.5.8):
| O | Open | I | Investigating | R | Resolved | X | Ignored |
- With a log detail panel open β change that log's status, advance to the next log. Optimistic UI: the row updates (or disappears, if hidden by current filter) immediately, no spinner. Designed for rapid triage β chain
R, R, R, Rto resolve four logs in seconds. - With no detail open β filter the list by that status (the legacy behavior).
Pre-v1.5.9, these shortcuts always filtered the list, regardless of whether a log was open. If you relied on that, the in-detail-panel behavior is what you'll notice as different. The "no detail open" path is unchanged.
If a status update fails (network, server error), the row is restored and an error toast appears.
Action shortcuts (r, h, l) and status shortcuts are configurable β see Keyboard Shortcuts.
Search Syntax
Type directly in the search box using field:value syntax:
| Syntax | Example | Description |
|---|---|---|
field:value |
message:error |
Search in specific field |
-field:value |
-level:debug |
Exclude matches |
field:"value" |
message:"user login" |
Quoted values with spaces |
text |
error |
Search in all fields |
-text |
-deprecated |
Exclude from all fields |
Searchable fields: message, source, context, level, channel, user_id, ip_address, url, trace_id, http_method
How multi-word and quoted searches behave
LogScope only switches into structured-parse mode when the input actually looks structured. Otherwise the whole input is matched as a single substring β so a stray : in a log message (e.g. failed: timeout) doesn't fragment your query.
| Input | Treated as | Matches |
|---|---|---|
payment failed: timeout |
Single substring | Logs whose message/context/source contains the contiguous phrase payment failed: timeout |
"payment failed" |
Single substring (quotes stripped) | Logs containing payment failed contiguously |
level:error message:timeout |
Structured (field:value Γ 2) |
Logs where level matches error AND message contains timeout |
payment -warning |
Structured (per-token - exclusion) |
Logs containing payment AND NOT containing warning |
level:error alone |
Structured | Logs where level matches error |
Structured mode triggers when the input contains any of: a quoted phrase ("..."), a per-token exclusion (-word or -word), or a field:value where field is one of the searchable field names listed above.
NOT toggle = true boolean complement
The UI's NOT toggle (or exclude=1 on a searches[] query-string entry) inverts the entire search expression. For any input, include_count + exclude_count == total_count β logs are never lost between the two views.
Tip: Request context filters (trace ID, user ID, IP, URL) support partial matching. Type
192.168to find all IPs starting with that prefix, or42to find user IDs containing "42".
Tip: Click on trace ID, user ID, or IP address in the detail panel to pivot your investigation β severity, channel, status, and search filters are cleared so nothing is hidden. Your date range is preserved.
Examples:
# Find errors in the API channel channel:api level:error # Find payment logs excluding debug channel:payment -level:debug # Find logs mentioning a specific user ID user_id:123 # Find logs containing "timeout" anywhere timeout # Find logs with "connection failed" in message message:"connection failed" # Find context containing a job ID context:abc123 # Exclude deprecated warnings -message:deprecated # Combine multiple conditions level:error channel:database message:timeout # Find all POST requests http_method:POST # Find logs from specific URL path url:/api/payments # Exclude health check endpoints -url:/health -url:/ping # Find logs from specific IP range ip_address:192.168 # Track a specific request by trace ID trace_id:abc-123-def
Regex mode: Click the .* button to enable regex patterns:
# Match error OR warning levels level:error|warning # Match any payment-related message message:payment.*failed # Match IP addresses starting with 192.168 ip_address:192\.168\.\d+\.\d+
Both search syntax and regex can be disabled in config if not needed.
Status Workflow
Logs have a status workflow: Open β Investigating β Resolved or Ignored.
Customize who changed the status:
// In AppServiceProvider::boot() use LogScope\LogScope; LogScope::statusChangedBy(function ($request) { return $request->user()?->name; });
Customize Statuses
Override built-in statuses or add new ones in config/logscope.php:
'statuses' => [ // Override built-in status 'investigating' => [ 'label' => 'In Progress', 'color' => 'blue', ], // Add custom statuses 'waiting' => [ 'label' => 'Waiting for Customer', 'color' => 'orange', 'closed' => false, // Shows in "Needs Attention" ], 'duplicate' => [ 'label' => 'Duplicate', 'color' => 'purple', 'closed' => true, // Hidden from "Needs Attention" ], ],
Available colors: gray, yellow, green, slate, blue, red, orange, purple
Quick Filters
Configure one-click filters in config/logscope.php:
'quick_filters' => [ ['label' => 'Today', 'icon' => 'calendar', 'from' => 'today'], ['label' => 'Recent Errors', 'icon' => 'alert', 'levels' => ['error', 'critical'], 'from' => '-24 hours'], ['label' => 'Needs Attention', 'icon' => 'filter', 'statuses' => ['open', 'investigating']], ['label' => 'Resolved Today', 'icon' => 'filter', 'statuses' => ['resolved'], 'from' => 'today'], ],
Available options: label, icon (calendar/clock/alert/filter), levels, statuses, from, to
Status Shortcuts
Each status has a default keyboard shortcut (uppercase, requires Shift). The shortcut is context-aware:
- Detail panel open: change the open log's status, advance to next (rapid triage)
- Detail panel closed: filter the list by that status
Customize in config/logscope.php:
'statuses' => [ // Disable a shortcut entirely (no triage AND no filter for this status) 'ignored' => ['shortcut' => null], // Custom status with shortcut β works in both contexts automatically 'waiting' => ['label' => 'Waiting', 'color' => 'orange', 'shortcut' => 'w'], ],
Keyboard Shortcuts
Action shortcuts (refresh, pagination) are configurable or can be disabled:
'keyboard_shortcuts' => [ 'refresh' => 'r', // Refresh logs and stats 'prev_page' => 'h', // Previous page 'next_page' => 'l', // Next page ],
Set any shortcut to null to disable it:
'keyboard_shortcuts' => [ 'prev_page' => null, // Disable previous page shortcut 'next_page' => null, ],
Authorization
LogScope uses a flexible auth system (checked in order):
1. Custom Callback:
LogScope::auth(fn ($request) => $request->user()?->isAdmin());
2. Gate:
Gate::define('viewLogScope', fn ($user) => $user->hasRole('admin'));
3. Default: Only accessible in local environment.
Custom Context
Add custom data to every log entry (e.g., API token ID, tenant ID):
// In AppServiceProvider::boot() use LogScope\LogScope; LogScope::captureContext(function ($request) { $token = $request->user()?->currentAccessToken(); return [ // Sanctum returns TransientToken (no `id`) for session-authenticated // requests and PersonalAccessToken (has `id`) for token-authenticated // requests. Type-check before accessing. 'token_id' => $token instanceof \Laravel\Sanctum\PersonalAccessToken ? $token->id : null, 'tenant_id' => $request->user()?->tenant_id, ]; });
This data is merged into the log's context field and appears in the JSON viewer.
If your callback throws (a property access on the wrong type, an unbound service, etc.), LogScope catches the exception, drops the custom context for that log entry only, and adds _logscope_callback_error to the entry's context with the exception class + message. The original log is still captured. The callback failure is also surfaced via error_log() and the in-UI failure banner so you know the callback is broken.
Artisan Commands
# Import existing log files (one-time migration) php artisan logscope:import php artisan logscope:import storage/logs/laravel.log --days=7 # Prune old logs php artisan logscope:prune php artisan logscope:prune --dry-run php artisan logscope:prune --days=14 # Diagnose configuration & wiring (CI-friendly: non-zero exit on any FAIL) php artisan logscope:doctor php artisan logscope:doctor --json # Smoke-test the capture pipeline end-to-end php artisan logscope:test php artisan logscope:test --keep # leave the test entry in the table
Note: The import command is a one-time migration for existing log files. After setup, new logs are captured automatically.
logscope:doctor checks the table, capture mode, write mode (including queue connection), middleware wiring, retention + schedule status, authorization resolution path, Octane integration, built assets, and any cached write-failure breadcrumb. Run it after install or whenever something feels off.
logscope:test emits a uniquely-tagged log through the configured capture path, forces a sync write for the duration of the test, and verifies the entry lands in log_entries. Useful as a one-shot post-install sanity check.
π Production Deployment
Recommended Settings
LOGSCOPE_WRITE_MODE=batch LOGSCOPE_RETENTION_DAYS=14 LOG_LEVEL=info
Schedule Pruning
You have two options:
Option 1 β Let LogScope do it (opt-in, off by default). Set LOGSCOPE_RETENTION_AUTO_SCHEDULE=true and LogScope registers logscope:prune on Laravel's scheduler at the time configured by LOGSCOPE_RETENTION_SCHEDULE_AT (defaults to 03:00), with ->onOneServer() for safe multi-server deploys.
Option 2 β Wire it yourself. Leave LOGSCOPE_RETENTION_AUTO_SCHEDULE off and add the schedule entry where you keep the rest of your scheduled tasks:
// Laravel 11+ (routes/console.php) Schedule::command('logscope:prune')->daily(); // Laravel 10 (app/Console/Kernel.php) $schedule->command('logscope:prune')->daily();
Don't enable both β auto-schedule plus a manual entry will run prune twice per night.
High-Traffic Apps
For thousands of requests/day:
-
Use queue mode with a dedicated queue:
LOGSCOPE_WRITE_MODE=queue LOGSCOPE_QUEUE=logs
-
Run a separate queue worker:
php artisan queue:work --queue=logs
-
Consider shorter retention (7 days).
π¨ Customization
Theme
Customize the appearance in config/logscope.php:
'theme' => [ // Primary accent color (buttons, links, selections) 'primary' => '#10b981', // Default to dark mode for new users (users can toggle and preference is saved) 'dark_mode_default' => true, // Google Fonts (set to false to use system fonts) 'fonts' => [ 'sans' => 'Outfit', // UI text 'mono' => 'JetBrains Mono', // Code/logs ], // Log level badge colors 'levels' => [ 'error' => ['bg' => '#dc2626', 'text' => '#ffffff'], 'warning' => ['bg' => '#f59e0b', 'text' => '#1f2937'], // ... other levels ], ],
Disable external fonts (use system fonts instead):
'fonts' => [ 'sans' => false, 'mono' => false, ],
Context Sanitization
LogScope automatically expands objects in your log context for better debugging:
// Request objects show useful data Log::info('API request', ['request' => $request]); // Context: { "request": { "_type": "request", "method": "POST", "url": "...", "input": {...} } } // Models and Arrayable objects are converted Log::info('User action', ['user' => $user]); // Context: { "user": { "name": "John", "email": "..." } }
Sensitive data is automatically redacted (password, token, api_key, credit_card, etc.):
Log::info('Login', ['request' => $request]); // Input: { "email": "john@example.com", "password": "[REDACTED]" }
Configure in config/logscope.php:
'context' => [ 'expand_objects' => true, // Set false to show [Object: ClassName] 'redact_sensitive' => true, // Set false to disable redaction (not recommended) 'sensitive_keys' => [], // Empty = use defaults, or provide your own list 'sensitive_headers' => [], // Empty = use defaults, or provide your own list ],
Publishing Assets
php artisan vendor:publish --tag=logscope-config php artisan vendor:publish --tag=logscope-migrations php artisan vendor:publish --tag=logscope-views
All Environment Variables
# Capture & Performance LOGSCOPE_CAPTURE=all LOGSCOPE_WRITE_MODE=batch LOGSCOPE_QUEUE=default LOGSCOPE_QUEUE_CONNECTION= # Features LOGSCOPE_FEATURE_STATUS=true LOGSCOPE_FEATURE_NOTES=true LOGSCOPE_FEATURE_SEARCH_SYNTAX=true LOGSCOPE_FEATURE_REGEX=true LOGSCOPE_IGNORE_DEPRECATIONS=true LOGSCOPE_IGNORE_NULL_CHANNEL=false # Database & Retention LOGSCOPE_TABLE=log_entries LOGSCOPE_RETENTION_ENABLED=true LOGSCOPE_RETENTION_DAYS=30 LOGSCOPE_MIGRATIONS_ENABLED=true # Routes LOGSCOPE_ROUTES_ENABLED=true LOGSCOPE_ROUTE_PREFIX=logscope LOGSCOPE_DOMAIN= LOGSCOPE_MIDDLEWARE_ENABLED=true LOGSCOPE_FORBIDDEN_REDIRECT=/ LOGSCOPE_UNAUTHENTICATED_REDIRECT=/login # Search LOGSCOPE_SEARCH_DRIVER=database # Cache LOGSCOPE_CACHE_TTL=60 # Context Sanitization LOGSCOPE_EXPAND_OBJECTS=true LOGSCOPE_REDACT_SENSITIVE=true
π‘οΈ Extensions
Watchtower
Block malicious IPs directly from the LogScope UI and sync the blacklist across all your environments automatically. (Previously named ahmedmerza/logscope-guard.)
composer require ahmedmerza/laravel-watchtower php artisan watchtower:install
π€ Contributing
Contributions are welcome! Please open an issue or submit a pull request.
π License
MIT License. See LICENSE for details.
