Search by

salioudiabate / log-viewer

salioudiabate

A multi-source Laravel log file viewer for Livewire, built on top of livewire-datatable — real search/sort/pagination/export instead of hand-rolled pagination, per-entry detail in a Modal, redaction of sensitive data, and a purge action that's actually permission-gated and backed up.

Package info

github.com/salioudiabate/log-viewer

pkg:composer/salioudiabate/log-viewer

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.0 2026-09-28 15:15 UTC

This package is auto-updated.

Last update: 2026-09-28 15:16:23 UTC


README

Tests Latest Version Total Downloads License

A multi-source log file viewer for Livewire, built on top of livewire-datatable — real search/sort/pagination/export instead of hand-rolled pagination, per-entry detail in a Modal, redaction of sensitive data, and a purge action that's actually permission-gated, backed up, and — unlike the file it's clearing — never itself invisible.

<livewire:log-viewer.log-table />

That's the whole integration. One route, one component, tabs for every configured source (Laravel, Horizon, a custom app log, a Go/Node service's JSON logs...), daily-file switching, level/search filtering, CSV export, and a "view details" modal with the full stack trace — all for free from salioudiabate/livewire-datatable's own engine.

Why this exists

Reviewing a real production app's own "Journaux" page turned up a fully hand-rolled log-file viewer: a bespoke in-memory pagination loop reimplemented from scratch in the Blade view, a single JOURNAUX VOIR permission gating both viewing and destructively truncating the file, a "Vider" (clear) button wired to a plain wire:confirm with no backup and no record of who cleared what, and level-filtering hardcoded as a plain <select> duplicating the same 8-level list already implicit in the log format's own parser.

This package is that same idea — multi-source, multi-format log tailing — rebuilt as a proper DataTableComponent subclass (so pagination/search/sort/export come from livewire-datatable's own tested engine instead of a hand-rolled loop), with viewing and purging as two separately configurable permissions, a mandatory backup before any truncation, and the purge itself always recorded via Log::warning() — the one action a log viewer can least afford to leave unaccounted for.

Requirements

Installation

composer require salioudiabate/log-viewer

Publish the config file — you'll want to at least review sources and authorize/authorize_clear:

php artisan vendor:publish --tag=log-viewer-config

Add a route:

use Salioudiabate\LogViewer\Livewire\LogTable;

Route::get('/logs', LogTable::class)
    ->middleware(['web', 'auth']) // and/or `can:...` — see Authorization below
    ->name('logs.index');

No npm install, no Vite entry — the one small CSS file this package ships (level badges, the source/file picker, the detail modal) is served directly by the package and linked by the component itself, so there is nothing to add to your layout. The table itself is styled by whatever livewire-datatable already does in your app.

Sources

// config/log-viewer.php
'sources' => [
    'laravel' => [
        'label' => 'Laravel',
        'dir' => storage_path('logs'),
        'pattern' => 'laravel*.log',
        'format' => 'laravel',
    ],
    'horizon' => [
        'label' => 'Horizon',
        'path' => '/var/log/myapp/horizon.log', // a single file instead of a dir+pattern
        'format' => 'laravel',
    ],
    'api-service' => [
        'label' => 'API Service',
        'path' => '/var/log/myapp/api.log',
        'format' => 'json', // ndjson — Go's zap/zerolog, Node's pino, Python's structlog, ...
    ],
],

A source only shows up as a tab once its file(s) actually exist on disk. Three formats ship out of the box:

  • laravel — the default [timestamp] channel.LEVEL: message format, with multi-line stack traces folded into the entry they belong to.
  • plain — no structure assumed; every non-empty line is its own entry.
  • json — one JSON object per line. Common field aliases are tried for timestamp (ts/time/timestamp), level (level/severity), and message (msg/message); config('log-viewer.level_map') normalizes whatever level vocabulary the source logger uses (Go's warn/dpanic/panic/fatal, ...) onto the 8 PSR-3 levels used everywhere else in this package.

dir + pattern sources get a daily-file picker (newest first, size and date shown) rendered above the table automatically.

Filtering, search, export

All inherited from DataTableComponent — search covers the message and channel columns, the level filter is a plain SelectFilter, sorting works on every column, and CSV export is already wired up (see livewire-datatable's own README for ->exportUsing()/ExcelExporter/PdfExporter if you want to customize it further). None of this is reimplemented here.

Entry detail

Click a row's "Details" action to open its full context in a Modal — level, channel, the complete message, structured context (for JSON sources), and the full stack trace, each in its own collapsible <pre> block. Nothing here needs any custom JS of its own.

Redaction

// config/log-viewer.php
'redact' => [
    '/Bearer\s+[A-Za-z0-9\-_.]+/i' => 'Bearer [REDACTED]',
    '/"password"\s*:\s*"[^"]*"/i' => '"password":"[REDACTED]"',
    '/(api[_-]?key|secret|token)=([^&\s]+)/i' => '$1=[REDACTED]',
],

Applied to every text field of an entry — message, context, stack, and the raw line — before it's ever rendered, exported, or handed to the detail modal. A stack trace can easily contain a bearer token or a password lifted straight from a query string; this doesn't rely on your application never having logged one by accident. It's a display-time mitigation, not a guarantee — see SECURITY.md.

Authorization

// config/log-viewer.php
'authorize' => 'log-viewer.view',
'authorize_clear' => 'log-viewer.clear', // deliberately separate — see below
// AppServiceProvider::boot()
Gate::define('log-viewer.view', fn (User $user) => $user->hasPermission('view logs'));
Gate::define('log-viewer.clear', fn (User $user) => $user->hasPermission('purge logs'));

Both default to null — no gate enforced by the package itself, on the assumption the route is already restricted (->middleware('can:...') or similar). Livewire re-applies that route's middleware to every subsequent action call, so the component's own actions stay covered by it. When set, authorize is re-checked on every request (not only when the page first loads), and authorize_clear is enforced on the clear() action itself, not just on the toolbar button. authorize_clear falls back to authorize when left unset, but the two are intentionally independent options: viewing and destructively truncating a log file are very different levels of trust, and defaulting them to the same ability — as the hand-rolled viewer this package replaces did — means anyone who can see logs can also destroy them.

The download route

The "Download" button streams the raw file through a route the package registers itself (/log-viewer/download), so it can't inherit the middleware of the page you mounted the component on. Instead, it only accepts a short-lived signed URL, which the component generates at click time — meaning only someone who can already see the component can download, whatever protects that page (a gate, your own admin middleware, a Spatie role...). No configuration needed.

For an extra layer on top of the signature, set the middleware applied to that route:

// config/log-viewer.php
'middleware' => ['web', 'auth'],
'download_url_ttl' => 60, // seconds a generated link stays valid

Note that downloads are the raw, unredacted file — redact only applies to what's rendered in the table and modal.

Clearing a file

The "Clear" toolbar button:

  1. copies the file to config('log-viewer.backup_path') first, unless backup_on_clear is false;
  2. truncates it;
  3. records the purge itself via Log::warning('[log-viewer] Log file cleared', [...]) — who, which source, which file.

Prune old backups on a schedule:

// routes/console.php or a scheduler service provider
$schedule->command('log-viewer:prune-backups')->daily();

removes anything past config('log-viewer.backup_retention_days') (30 by default).

Translations and date format

All user-facing text follows your app's locale — English and French ship with the package. Override or add a language by publishing the translation files:

php artisan vendor:publish --tag=log-viewer-translations

Timestamps use config('log-viewer.date_format') (Y-m-d H:i:s by default).

Customization

Any piece of markup — the source/file picker header, the entry-detail modal, the level-badge column, the empty state:

php artisan vendor:publish --tag=log-viewer-views

Colors — --log-viewer-* CSS custom properties in log-viewer.css, same design language as Notify/Modal/Select:

:root {
  --log-viewer-danger-fg: #B42318;
  --log-viewer-r-md: 8px;
}

Light or dark — follows the OS preference by default. When your app forces a scheme (e.g. modal/notify/select set to light), force the same one here, or the modal gets dark-mode text on a light surface:

// config/log-viewer.php
'color_scheme' => 'light', // null (follow the OS), 'light' or 'dark'
'modal_max_width' => '4xl', // entry-detail modal: a modal width token or any CSS length

A different table shape entirely. Livewire\LogTable is a plain, small DataTableComponent subclass — extend it yourself and override columns()/filters()/rowActions() if you want a different set of columns or extra row actions, the same way you'd customize any other livewire-datatable table.

Testing

use Livewire\Livewire;
use Salioudiabate\LogViewer\Livewire\LogTable;

Livewire::test(LogTable::class)
    ->assertSee('error')
    ->set('search', 'timeout')
    ->assertSee('Connection timeout');

Security

See SECURITY.md. Report vulnerabilities privately rather than via a public issue.

License

MIT. See LICENSE.md.