abdian/loglens

Production-scale log viewer for Laravel: persistent on-disk index, instant multi-GB opening, full-text search, browser live tail, error grouping, RTL. Laravel 8โ€“13, PHP 8.0+, zero required extensions.

Maintainers

Package info

github.com/abdian/loglens

pkg:composer/abdian/loglens

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-20 12:52 UTC

This package is auto-updated.

Last update: 2026-06-20 21:24:21 UTC


README

๐Ÿ” LogLens

A production-scale log viewer for Laravel.

Open a 5 GB laravel.log in under 100 ms ยท search it in under 200 ms ยท never hang the UI on a cache miss again.

Packagist Version Tests PHP Version Laravel Downloads License

Installation ยท Configuration ยท Query language ยท Security ยท CLI ยท JSON API


LogLens โ€” three-pane log viewer: facet sidebar, entry list with volume histogram, and live tail
composer require abdian/loglens

Then visit /loglens โ€” no publish step, no npm, no migrations, no configuration required.

The one architectural decision that matters

Most Laravel log viewers keep their index in the Laravel cache. That choice is the root of two failure modes you eventually hit at scale: the UI hangs while a large file is (re-)scanned, and searches return zero results after the cache is evicted or cleared. Because the cache is load-bearing, neither can be solved without changing the storage model.

LogLens stores a persistent on-disk SQLite sidecar index per log file under storage/loglens/. That index survives cache clears, deploys, load-balancer key skew, and Octane restarts. The entire bug class is structurally impossible.

Headline differentiators

The column on the right describes the common design of a cache-backed Laravel log viewer โ€” the pattern LogLens was built to move past. It is a description of an architecture, not a benchmark of any one package.

LogLens Typical cache-backed log viewer
Index storage Per-file SQLite sidecar on local disk Laravel cache (evictable)
Multi-GB file open Instant tail-first read (3.6 ms on 500 MB, no index needed) Full-file scan on every open; degrades on large files
Search FTS5 + trigram โ€” index once, search many Re-scans the raw file on every query
Query language level:error after:-1h "payment failed" -channel:horizon Raw regex only
Live tail in the browser SSE + polling fallback, works on Windows and shared hosting Manual refresh
JSON/NDJSON logs Auto-detected, no class required Often needs a custom log class
.gz reading Transparent, indexed Usually unsupported
Error grouping Sentry-style Issues view โ€” deterministic fingerprints, sparklines Typically none
RTL / Farsi locale Supported via CSS logical properties Rarely supported
PHP/Laravel support Laravel 8โ€“13, PHP 8.0โ€“8.5, every combination CI-tested Varies
Required extensions None (pdo_sqlite + zlib used opportunistically) Varies
Install composer require โ†’ visit /loglens Often requires a config publish

Feature overview

Persistent index engine

One SQLite file per log file lives under storage/loglens/. It records byte offsets, timestamps, levels, fingerprint hashes, and pre-aggregated per-hour/level counts. Indexing is incremental โ€” only appended bytes are re-scanned. Rotation and truncation are detected via file size, inode, and a fingerprint of the first 4 KB.

When pdo_sqlite is unavailable, LogLens falls back to a packed binary sidecar that supports all browsing operations except full-text search.

Instant first paint

Opening any file immediately serves the newest page via backward chunked reads โ€” no index is required. The response carries indexState: none|building|ready so the UI can progressively enable index-backed features (full-text search, histogram, jump-to-date) as they become available. You see logs in under 100 ms on a 5 GB file.

Format support

Built-in parsers: Laravel/Monolog LineFormatter (multi-line stack traces, dual JSON context tails from Laravel 11+), NDJSON/JsonFormatter (auto-detected), Horizon (both formats), Apache/Nginx access logs, Apache/Nginx error logs, PHP-FPM, PostgreSQL, Redis, and Supervisor.

Custom formats can be declared in config/loglens.php with a named-capture PCRE โ€” no PHP class required. A class-based API and an adapter for existing opcodesio Log subclasses are also available.

Compressed .gz files are read transparently through zlib stream wrappers.

FTS5 search with a query language

level:error after:-1h "payment failed" -channel:horizon
level:>=warning after:2026-06-01 before:2026-06-30
context.user_id:42 OR context.user_id:43
/TimeoutException|ConnectionException/i

The query language supports bare terms (implicit AND), quoted phrases, field:value filters, level comparisons, absolute and relative time filters, negation, OR with parentheses, prefix wildcards (error*), and opt-in regex. The same grammar drives the web UI, the JSON API, loglens:search, loglens:tail, and live-tail server-side filtering.

Search executes against the SQLite FTS5 index. A capability ladder degrades gracefully: FTS5+trigram โ†’ FTS5 unicode61 โ†’ SQL LIKE โ†’ index-assisted streamed PCRE scan. The active tier is visible in the diagnostics panel.

<img src="art/screenshot-search.webp" alt="Search results for level:error after:-24h "payment" with matched terms highlighted and the active search tier shown" width="900">

Live tail in the browser

Server-Sent Events over a raw StreamedResponse (compatible with Laravel 8+). Short 45-second windows with automatic reconnect ensure compatibility with common proxy read timeouts. A client-side watchdog switches transparently to offset-based polling when a buffering middlebox is detected, or when running under Octane/Swoole where streaming responses buffer by design.

Multi-file tailing multiplexes over a single connection using per-file SSE event names.

Error grouping (Issues view)

Every entry is fingerprinted at index time. Exception entries get two deterministic hashes: one on exception_class|top_app_frame_file|top_app_frame_line and one on exception_class|throw_site. Non-exception entries are normalized by masking UUIDs, dates, IPs, numbers, and SQL bindings, producing a stable group title.

The Issues view shows each group with its occurrence count, first/last seen, level, and a sparkline โ€” collapsing thousands of identical errors into one actionable row. A "new since" diff reports fingerprints that first appeared after a given timestamp (useful after a deploy).

Issues view โ€” log entries grouped by fingerprint with occurrence counts

Analytics

A level-stacked volume histogram above the entry list is answered entirely from pre-aggregated statistics โ€” no entry scans, under 100 ms on any indexed file. Dragging a range on the histogram zooms the entry list to that window.

File management done right

  • Clear uses ftruncate with an exclusive lock, preserving the inode so long-running writers (queue workers, Horizon) continue appending to the same file without interruption.
  • Delete removes the file and writes a tombstone so a same-path replacement starts a fresh index.
  • Delete a single entry soft-deletes one row in the index (the raw file is never rewritten); it disappears from listings, search, stats, and groups, and the deletion survives a full re-index.
  • Download uses short-TTL signed URLs bound to the authenticated user. Streaming zip downloads never materialize the full archive on disk. Partial downloads ("last 50 MB") are supported for large files.
  • Prune (loglens:prune) automates retention by age and total size, with optional compression to .gz and a --dry-run mode.

Web UI

Vue 3 + Pinia + Tailwind SPA served from the vendor directory โ€” no publish step, no npm for users. Three-pane IDE layout: facet sidebar, virtualized entry list with level/time histogram, and a detail drawer with a Flare-style stack trace pane including editor deep links (PhpStorm, VS Code, Cursor, and more) with configurable remote-to-local path mapping.

Keyboard-first: j/k row navigation, / focuses search, Cmd+K opens the command palette, e/E jumps between errors. Dark mode by default. Full RTL support for Persian (fa) locale via CSS logical properties.

Detail drawer โ€” Flare-style stack trace with app frames highlighted, vendor frames collapsed, and editor deep links

JSON API and api_only mode

Every viewer operation is available through a versioned JSON API under {prefix}/api. Setting api_only = true disables UI and asset routes while keeping the full API operational โ€” useful for headless dashboards or custom frontends.

CLI companion

php artisan loglens:index          # build/update all indexes
php artisan loglens:index --watch  # continuous incremental indexing
php artisan loglens:search "level:error after:-24h" --json
php artisan loglens:tail --query="level:>=warning"
php artisan loglens:stats --json
php artisan loglens:prune --days=30 --max-total-size=10G --dry-run

All commands share the same index and query language as the web UI.

Security

LogLens is production default-deny. In any non-local environment, all routes return 403 until you define the viewLogLens gate in your application. The identical authorization middleware stack protects web, API, and SSE routes.

Display-time secret redaction is enabled by default: Authorization headers, APP_KEY, AWS keys, Stripe keys, JWTs, and password fields are replaced with [redacted] before any content reaches the browser.

See docs/security.md for the full hardening guide.

Requirements

  • PHP 8.0โ€“8.5
  • Laravel 8โ€“13
  • No required PHP extensions (pdo_sqlite and zlib are used opportunistically with graceful fallbacks)

Quick start

composer require abdian/loglens

Visit https://your-app.test/loglens. In a local environment the UI is immediately accessible. In production, define the viewLogLens gate first โ€” see docs/security.md.

To publish the config file:

php artisan vendor:publish --tag=loglens-config

Documentation

License

MIT