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: 62

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 1

v1.5.1 2026-02-27 12:29 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

v1.5.0 — Performance Overhaul

To understand how LogScope holds up at scale, we stress-tested against a SQLite database with 1,000,052 log entries — significantly more than a typical production deployment (weekly pruning keeps most installs around 400k). The results exposed three fixable bottlenecks.

Scenario Before After Improvement
First page load 20.6 ms 0.6 ms 34× faster
Page 100 29.4 ms 1.2 ms 25× faster
Page 1,000 95.3 ms 6.4 ms 15× faster
Filtered query 461.4 ms 0.7 ms 659× faster
Response payload 47.7 KB 29.4 KB 38% smaller

What changed and why:

1. Narrow SELECT on the list query The list was running SELECT *, fetching full message and context columns (up to 48 KB per row × 50 rows) that the list view never renders — it only shows short previews. Full content still loads, but only when you open a specific entry.

2. Keyset (cursor) pagination instead of OFFSET LIMIT 50 OFFSET 49950 tells the database to scan and throw away 49,950 rows before returning your 50. Cursor pagination stores a (occurred_at, id) bookmark and jumps directly — O(1) regardless of depth. In practice most users find what they need on page 1, but it means deep pagination no longer degrades.

3. No COUNT on filtered queries Laravel's paginator ran a COUNT(*) with all active filters on every page change to compute total pages. On large filtered result sets this was the dominant cost — most of the 461 ms on filtered queries.

The trade-off: when filters are active, the count is capped at 1,000 and shows "1,000+" beyond that. The real unfiltered total is still shown with no extra cost (it comes from the cached stats). When you're actively filtering you're in find mode — "1,000+ errors in the last hour" tells you everything actionable. Knowing the exact number beyond that doesn't change what you do next.

v1.4.2 — Responsive Design & Stability

Full responsive layout for mobile, tablet, and desktop. The sidebar, log table, detail panel, and filters all adapt to screen size. LogScope is now usable from a phone.

Also in v1.4.x: fixed a filter race condition with debounce + AbortController, pivot filtering from the detail panel (trace ID / user ID / IP), and several stability fixes.

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, 11, or 12
  • 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

📦 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=

Retention

LOGSCOPE_RETENTION_ENABLED=true
LOGSCOPE_RETENTION_DAYS=30

Note: Retention requires scheduling logscope:prune - 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, filter by status): | O | Open | I | Investigating | R | Resolved | X | Ignored |

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

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: OpenInvestigatingResolved 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). Customize in config/logscope.php:

'statuses' => [
    // Disable a shortcut
    'ignored' => ['shortcut' => null],
    // Custom status with shortcut
    '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) {
    return [
        'token_id' => $request->user()?->currentAccessToken()?->id,
        'tenant_id' => $request->user()?->tenant_id,
    ];
});

This data is merged into the log's context field and appears in the JSON viewer.

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

Note: The import command is a one-time migration for existing log files. After setup, new logs are captured automatically.

🏭 Production Deployment

Recommended Settings

LOGSCOPE_WRITE_MODE=batch
LOGSCOPE_RETENTION_DAYS=14
LOG_LEVEL=info

Schedule Pruning

// Laravel 11+ (routes/console.php)
Schedule::command('logscope:prune')->daily();

// Laravel 10 (app/Console/Kernel.php)
$schedule->command('logscope:prune')->daily();

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

🤝 Contributing

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

📄 License

MIT License. See LICENSE for details.