ahmedmerza/logscope

A fast, database-backed log viewer for Laravel applications

Maintainers

Package info

github.com/AhmedMerza/laravel-logscope

pkg:composer/ahmedmerza/logscope

Statistics

Installs: 125

Dependents: 1

Suggesters: 0

Stars: 2

Open Issues: 1

v1.7.1 2026-05-20 05:51 UTC

README

Latest Version License PHP Version

A beautiful, database-backed log viewer for Laravel applications. Production-ready.

LogScope Screenshot

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

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 via runningUnitTests()-style guards in some versions)
  • Custom set_error_handler calls in user code that don't chain to the previous handler
  • error_reporting being lowered to exclude E_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=true or scheduling logscope:prune yourself - 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_channel defaults to false because Log::build() (dynamic loggers) produce logs without a channel. Setting this to true would 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, R to 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.168 to find all IPs starting with that prefix, or 42 to 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:

  1. Use queue mode with a dedicated queue:

    LOGSCOPE_WRITE_MODE=queue
    LOGSCOPE_QUEUE=logs
  2. Run a separate queue worker:

    php artisan queue:work --queue=logs
  3. 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

View Watchtower β†’

🀝 Contributing

Contributions are welcome! Please open an issue or submit a pull request.

πŸ“„ License

MIT License. See LICENSE for details.