PHPackages                             digitaldev-lx/log-hole - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Admin Panels](/categories/admin)
4. /
5. digitaldev-lx/log-hole

ActiveLibrary[Admin Panels](/categories/admin)

digitaldev-lx/log-hole
======================

A modern Laravel logging package with multi-driver database support, web dashboard, and PHP attributes for declarative logging.

v4.0.0(2mo ago)213MITPHPPHP ^8.4CI passing

Since Oct 26Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/digitaldev-lx/log-hole)[ Packagist](https://packagist.org/packages/digitaldev-lx/log-hole)[ GitHub Sponsors](https://github.com/DigitalDevLx)[ RSS](/packages/digitaldev-lx-log-hole/feed)WikiDiscussions main Synced today

READMEChangelog (7)Dependencies (36)Versions (10)Used By (0)

[![laravel-eupago-repo-banner](https://camo.githubusercontent.com/7b3701aded3239bd687b980e7399ead9975630feaf93ee7b9822fd5cc77b4ef6/68747470733a2f2f7062732e7477696d672e636f6d2f70726f66696c655f62616e6e6572732f3539333738353535382f313637313139343635372f3135303078353030)](https://camo.githubusercontent.com/7b3701aded3239bd687b980e7399ead9975630feaf93ee7b9822fd5cc77b4ef6/68747470733a2f2f7062732e7477696d672e636f6d2f70726f66696c655f62616e6e6572732f3539333738353535382f313637313139343635372f3135303078353030)

Laravel LogHole
===============

[](#laravel-loghole)

LogHole is a modern, flexible Laravel logging package with multi-driver database support. Designed for seamless integration with Laravel's Log facade, it provides a web dashboard for browsing logs, an Artisan command for CLI access, and PHP 8.4+ attributes for declarative method-level logging.

[![Latest version](https://camo.githubusercontent.com/1647e33deaef6c9f9a9700cb7e261b4fb22c94e97e9af794b45d0ce7dfd46ca6/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f72656c656173652f6469676974616c6465762d6c782f6c6f672d686f6c653f7374796c653d666c61742d737175617265)](https://github.com/digitaldev-lx/log-hole/releases)[![GitHub license](https://camo.githubusercontent.com/4c4f65b83428caf2605955e61f4972e2d8b4280db71d876672b23e89b1edf485/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6469676974616c6465762d6c782f6c6f672d686f6c653f7374796c653d666c61742d737175617265)](https://github.com/digitaldev-lx/log-hole/blob/master/LICENSE)

---

Features
--------

[](#features)

- Custom Monolog database channel that stores logs in a configurable DB table
- Multi-driver support: MySQL, MariaDB (auto-detected), PostgreSQL, SQLite, SQL Server
- Web dashboard at `/log-hole` with Tailwind CSS v3 and Alpine.js (dark/light mode, filters, stats, auto-refresh)
- Artisan command `log-hole:tail` to query and purge logs from the CLI
- PHP 8.4+ attribute `#[Loggable]` for declarative method and class-level logging via middleware
- LogLevel enum with color-coded badges and Monolog conversion
- Strategy Pattern architecture with `LogDriverInterface` for extensibility
- Cross-database LIKE escaping with explicit `ESCAPE` clause — wildcards in search terms are always treated literally
- Optional stats query cache for dashboards on large logs tables
- Chunked purge for multi-million-row tables

---

Requirements
------------

[](#requirements)

ReleasePHPLaravel4.x&gt;= 8.413.x3.x&gt;= 8.411.x, 12.x2.x&gt;= 8.210.x, 11.x1.x&gt;= 8.210.x**Supported databases:** MySQL, MariaDB, PostgreSQL, SQLite, SQL Server

---

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require digitaldev-lx/log-hole
```

Publish the configuration file:

```
php artisan vendor:publish --tag="log-hole-config"
```

Run the migration to create the `logs_hole` table:

```
php artisan migrate
```

---

Configuration
-------------

[](#configuration)

After publishing, the configuration file is located at `config/log-hole.php`:

```
return [
    'database' => [
        'driver' => 'custom',
        'via' => DigitalDevLx\LogHole\Channels\DatabaseChannel::class,
        'level' => env('LOG_LEVEL', 'debug'),
        'table' => 'logs_hole',
    ],

    // Database connection to use for logs (null = default connection)
    'connection' => env('LOG_HOLE_DB_CONNECTION', null),

    // Emails of users authorized to access the dashboard (empty = open access)
    'authorized_users' => [],

    // Route prefix for the dashboard
    'dashboard_route' => 'log-hole',

    // Number of logs per page in the dashboard
    'per_page' => 25,

    // Auto-refresh the dashboard every 5 seconds
    'auto_refresh' => false,

    // Cache TTL (in seconds) for the stats() query. 0 disables caching.
    // Useful when the dashboard auto-refreshes against a large logs table.
    'stats_cache_ttl' => env('LOG_HOLE_STATS_CACHE_TTL', 0),
];
```

### Setting up the log channel

[](#setting-up-the-log-channel)

Add the LogHole database channel to your `config/logging.php`:

```
'channels' => [
    // ... other channels

    'database' => config('log-hole.database'),
],
```

Then set the channel as your default (or use it alongside other channels):

**Option A - Set as default channel in `.env`:**

```
LOG_CHANNEL=database
```

**Option B - Use as part of a stack:**

```
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'database'],
    ],

    'database' => config('log-hole.database'),
],
```

**Option C - Log to the database channel on demand:**

```
use Illuminate\Support\Facades\Log;

Log::channel('database')->info('This goes to the database');
```

### Using a different database connection

[](#using-a-different-database-connection)

If you want logs stored in a different database, set the environment variable:

```
LOG_HOLE_DB_CONNECTION=mysql_logs
```

Make sure the connection is defined in `config/database.php` and the migration has run on that connection.

### Caching dashboard stats

[](#caching-dashboard-stats)

On a logs table with millions of rows, the per-level `COUNT(*)` query that powers the dashboard's stats bar is expensive. With dashboard auto-refresh on, it runs every 5 seconds per visitor.

To cache the stats query, set a positive TTL:

```
LOG_HOLE_STATS_CACHE_TTL=5
```

The driver uses Laravel's default cache store under the key `log-hole:stats:{connection}:{table}`.

---

Usage
-----

[](#usage)

### Logging via the Log facade

[](#logging-via-the-log-facade)

```
use Illuminate\Support\Facades\Log;

// If 'database' is your default channel
Log::info('User logged in', ['user_id' => 1]);
Log::error('Payment failed', ['order_id' => 42, 'reason' => 'timeout']);
Log::warning('Disk space running low');

// Or target the database channel explicitly
Log::channel('database')->debug('Debug info', ['context' => 'value']);
```

All standard log levels are supported: `emergency`, `alert`, `critical`, `error`, `warning`, `notice`, `info`, `debug`.

---

PHP Attributes
--------------

[](#php-attributes)

LogHole provides the `#[Loggable]` PHP attribute for declarative logging on controller methods and classes. When the LogHole middleware is active, annotated actions are automatically logged after the request is handled.

### Setup the middleware

[](#setup-the-middleware)

In `bootstrap/app.php`:

```
use DigitalDevLx\LogHole\Middlewares\LogHoleMiddleware;

return Application::configure(basePath: dirname(__DIR__))
    // ...
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(LogHoleMiddleware::class);
    })
    ->create();
```

> Older Laravel setups (10.x with `app/Http/Kernel.php`) are supported by LogHole `^2.0`. v3 and v4 expect the Laravel 11+ middleware bootstrap layout.

### Method-level attribute

[](#method-level-attribute)

```
use DigitalDevLx\LogHole\Attributes\Loggable;

class OrderController extends Controller
{
    #[Loggable(message: 'Order was created', level: 'info')]
    public function store(Request $request)
    {
        // ... your logic
    }

    #[Loggable(message: 'Order was deleted', level: 'warning')]
    public function destroy(Order $order)
    {
        // ... your logic
    }
}
```

### Class-level attribute

[](#class-level-attribute)

Apply the attribute to the class to log all controller actions:

```
use DigitalDevLx\LogHole\Attributes\Loggable;

#[Loggable(level: 'info')]
class UserController extends Controller
{
    public function index() { /* logged automatically */ }
    public function show(User $user) { /* logged automatically */ }
}
```

Method-level attributes take precedence over class-level attributes.

### Attribute options

[](#attribute-options)

ParameterTypeDefaultDescription`message``string``''`Custom log message. If empty, uses `"{method} was called"``level``LogLevel|string``LogLevel::Info`Log level (enum or string like `'error'`)`includeRequest``bool``false`Include request method, URL, and IP in context`channel``?string``null`Target a specific log channel (null = default)### Using the LogLevel enum

[](#using-the-loglevel-enum)

```
use DigitalDevLx\LogHole\Attributes\Loggable;
use DigitalDevLx\LogHole\Enums\LogLevel;

#[Loggable(message: 'Critical action', level: LogLevel::Critical, includeRequest: true)]
public function dangerousAction()
{
    // ...
}
```

---

Dashboard
---------

[](#dashboard)

The LogHole dashboard provides a modern web interface for browsing and filtering logs.

**URL:** `/log-hole` (configurable via `dashboard_route` in config)

### Features

[](#features-1)

- **Stats bar** with total count and per-level counters with color-coded badges
- **Server-side filters:** level, search term, date range (from/to)
- **Log table** with level badges, truncated messages with tooltip, expandable JSON context, and relative timestamps
- **Dark/light mode** toggle with localStorage persistence
- **Auto-refresh** toggle (reloads every 5 seconds)
- **Pagination** with Tailwind styling and stable ordering across page boundaries

### Restricting dashboard access

[](#restricting-dashboard-access)

By default, the dashboard is open to everyone. To restrict access, add authorized emails to the config:

```
'authorized_users' => [
    'admin@example.com',
    'developer@example.com',
],
```

When the list is not empty, only authenticated users with matching emails can access the dashboard. Unauthenticated users or users not in the list will receive a 403 error.

### Gate authorization

[](#gate-authorization)

LogHole also registers a `viewLogHole` Gate that you can use in your own authorization logic:

```
if (Gate::allows('viewLogHole')) {
    // user can view logs
}
```

---

Artisan Command
---------------

[](#artisan-command)

The `log-hole:tail` command allows you to query and purge logs directly from the CLI.

```
php artisan log-hole:tail {options}
```

### Options

[](#options)

OptionDescription`--emergency`Filter by EMERGENCY level`--alert`Filter by ALERT level`--critical`Filter by CRITICAL level`--error`Filter by ERROR level`--warning`Filter by WARNING level`--notice`Filter by NOTICE level`--info`Filter by INFO level`--debug`Filter by DEBUG level`--from=`Start date filter (e.g., `2024-10-01`)`--to=`End date filter (e.g., `2024-10-31`)`--take=`Limit the number of entries (default: 10, clamped 1-1000)`--purge`Purge all logs (asks for confirmation)If no level option is specified, all levels are returned.

### Examples

[](#examples)

Fetch the last 10 logs (default):

```
php artisan log-hole:tail
```

Fetch error-level logs from a date range:

```
php artisan log-hole:tail --error --from=2024-10-01 --to=2024-10-31
```

Fetch the last 5 critical logs:

```
php artisan log-hole:tail --critical --take=5
```

Purge all logs (with confirmation prompt):

```
php artisan log-hole:tail --purge
```

### Output

[](#output)

The command displays logs in a table with the following columns:

```
+-----------+----------------------------------+----------------------------+---------------------+
| Level     | Message                          | Context                    | Logged At           |
+-----------+----------------------------------+----------------------------+---------------------+
| ERROR     | Payment failed                   | {                          | 2024-10-15 14:30:00 |
|           |                                  |     "order_id": 42,        |                     |
|           |                                  |     "reason": "timeout"    |                     |
|           |                                  | }                          |                     |
| WARNING   | Disk space running low           |                            | 2024-10-15 14:25:00 |
| INFO      | User logged in                   | {                          | 2024-10-15 14:20:00 |
|           |                                  |     "user_id": 1           |                     |
|           |                                  | }                          |                     |
+-----------+----------------------------------+----------------------------+---------------------+

```

Context is displayed as pretty-printed JSON for readability.

---

Driver Architecture
-------------------

[](#driver-architecture)

LogHole uses the Strategy Pattern for database access. All drivers implement `LogDriverInterface`:

```
use DigitalDevLx\LogHole\Drivers\Contracts\LogDriverInterface;
```

The `DriverFactory` auto-detects your database driver and returns the appropriate implementation:

DatabaseDriver ClassContext search strategyMySQL`MySqlDriver``CAST(context AS CHAR) LIKE ? ESCAPE ?`MariaDB`MariaDbDriver`Inherits MySQL strategy. Auto-detected via PDO version string, or matched by the explicit `mariadb` driver namePostgreSQL`PostgreSqlDriver``context::text ILIKE ? ESCAPE ?` (case-insensitive Unicode)SQLite`SqliteDriver``IFNULL(context, '') LIKE ? ESCAPE ?`SQL Server`SqlServerDriver``CAST(context AS NVARCHAR(MAX)) LIKE ? ESCAPE ?`All drivers use `~` as the LIKE escape character — chosen for portability across MySQL/Postgres/SQLite/SQL Server string-literal handling. User-facing search semantics are unchanged: `%`, `_`, and `~` typed into the search box are matched literally.

The driver is registered as a singleton in the service container. You can resolve it directly:

```
use DigitalDevLx\LogHole\Drivers\Contracts\LogDriverInterface;
use DigitalDevLx\LogHole\Enums\LogLevel;

$driver = app(LogDriverInterface::class);

// Insert a log
$driver->insert(LogLevel::Info, 'Hello', ['key' => 'value'], now());

// Query logs with filters
$logs = $driver->query(level: LogLevel::Error, search: 'payment', limit: 20);

// Get paginated results
$paginated = $driver->paginate(level: LogLevel::Warning, perPage: 25);

// Get stats (cached when log-hole.stats_cache_ttl > 0)
$stats = $driver->stats();
echo $stats->total;
echo $stats->countForLevel(LogLevel::Error);

// Purge logs
$driver->purge();                                      // purge all
$driver->purge(level: LogLevel::Debug);                // purge only debug logs
$driver->purge(before: now()->subMonth());             // purge logs older than 1 month
$driver->purge(before: now()->subYear(), chunkSize: 5000); // batched delete
```

The `chunkSize` argument on `purge()` lets you delete in batches instead of one statement — recommended on multi-million-row tables to reduce lock contention and binlog volume. Pass `0` (default) for a single `DELETE` statement.

---

Database Table
--------------

[](#database-table)

The migration creates a `logs_hole` table (configurable via `config('log-hole.database.table')`) with the following structure:

ColumnTypeNotes`id`bigintPrimary key, auto-increment`message`textLog message`level`stringLog level (e.g., `ERROR`)`context`jsonNullable, additional context`logged_at`datetimeNullable, when the log was createdIndexes: `logged_at`, and a composite `(level, logged_at)` that also covers level-only filters via leftmost-prefix.

---

Upgrading
---------

[](#upgrading)

### v3.x → v4.0

[](#v3x--v40)

`v4` is a Laravel 13 release with no public API breaks. The driver layer was overhauled internally to fix LIKE wildcard escaping bugs in PostgreSQL and SQL Server, and to unify search semantics across MySQL/MariaDB. If you only used the Log facade, the dashboard, the `log-hole:tail` command or `#[Loggable]`, the upgrade is `composer require digitaldev-lx/log-hole:^4.0`.

If you implemented a custom driver against `LogDriverInterface`, note that `purge()` now accepts a third optional argument `int $chunkSize = 0`. Existing callers continue to work; the new argument has a backward-compatible default.

### v1.x → v2.0

[](#v1x--v20)

1. **Config file structure changed** — re-publish the config:

    ```
    php artisan vendor:publish --tag="log-hole-config" --force
    ```
2. **Publish tag renamed** — changed from `--tag=logs-config` to `--tag=log-hole-config` (Spatie convention)
3. **`#[Loggable]` attribute** — the `level` property is now a `LogLevel` enum (string values still work for backward compatibility). The `$level` public property was removed in favor of `$logLevel` (readonly `LogLevel` enum).
4. **Views and routes moved** — views moved from `src/resources/views/` to `resources/views/`. Routes moved from `src/routes/` to `routes/`. If you published views, re-publish them.
5. **Migration indexes** — re-run migrations to add the new performance indexes:

    ```
    php artisan migrate
    ```

---

What's new in v4.0
------------------

[](#whats-new-in-v40)

- **Laravel 13 support** (Pest 4, Orchestra Testbench 11.1, Larastan 3.9)
- **Bug fix:** PostgreSQL and SQL Server now emit an `ESCAPE` clause — wildcards in the search term are no longer ignored
- **Bug fix:** MySQL/MariaDB use `CAST(context AS CHAR) LIKE ? ESCAPE ?` instead of `JSON_SEARCH`, giving identical substring semantics across `message` and `context`
- **Bug fix:** Pagination is now stable when many rows share the same `logged_at` (added `id` tiebreaker)
- **Performance:** `DatabaseChannel` resolves the driver from the container singleton on each write; `DriverFactory::isMariaDb()` is cached per-connection
- **Performance:** `stats_cache_ttl` config option for caching the dashboard stats query
- **Robustness:** chunked `purge()`; `insert()` falls back to `now()` when `loggedAt` is null; `error_log` fallback in the channel is rate-limited
- **Tests:** 175 tests on PHPStan level 6, plus a separate integration suite that runs against real Postgres 16 and MySQL 8.4 services in CI

See [CHANGELOG.md](CHANGELOG.md) for the full list.

---

Testing
-------

[](#testing)

```
composer run test           # Run default test suite (Pest, SQLite in-memory)
composer run test-coverage  # Tests with coverage report
composer run analyse        # PHPStan level 6
composer run format         # Laravel Pint (PSR-12)
composer run check          # analyse + format together
```

To run the integration suite against a real Postgres or MySQL database (skipped by default), set `LOG_HOLE_INTEGRATION_DB`:

```
LOG_HOLE_INTEGRATION_DB=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 \
DB_DATABASE=log_hole_test DB_USERNAME=postgres DB_PASSWORD=postgres \
vendor/bin/pest --testsuite=integration
```

CI runs this suite automatically against Postgres 16 and MySQL 8.4 services.

---

License
-------

[](#license)

digitaldev-lx/log-hole is open-sourced software licensed under the [MIT license](LICENSE.md).

About DigitalDev
----------------

[](#about-digitaldev)

[DigitalDev](https://www.digitaldev.pt) is a web development agency based in Lisbon, Portugal. We specialize in Laravel, Livewire, and Tailwind CSS.

[Codeboys](https://www.codeboys.pt) is our special partner and we work together to deliver the best solutions for our clients.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance88

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity63

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~79 days

Recently: every ~138 days

Total

8

Last Release

60d ago

Major Versions

v1.3.0 → v2.0.02026-02-27

v2.0.0 → v3.0.02026-04-01

v3.0.0 → v4.0.02026-05-04

PHP version history (2 changes)v1.0.0PHP ^8.2

v3.0.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/c6498ae164eb066d13ae98668c2c9df065410dcb1da196561ef8f76c93c1a827?d=identicon)[digitaldevlx](/maintainers/digitaldevlx)

---

Top Contributors

[![digitaldev-lx](https://avatars.githubusercontent.com/u/81043832?v=4)](https://github.com/digitaldev-lx "digitaldev-lx (25 commits)")

---

Tags

laravellogsdashboardmulti-driverdatabase logslog-hole

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/digitaldev-lx-log-hole/health.svg)

```
[![Health](https://phpackages.com/badges/digitaldev-lx-log-hole/health.svg)](https://phpackages.com/packages/digitaldev-lx-log-hole)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M47](/packages/spatie-laravel-pdf)[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.1k11.2M100](/packages/dedoc-scramble)[spatie/laravel-passkeys

Use passkeys in your Laravel app

471890.7k39](/packages/spatie-laravel-passkeys)[filament/support

Core helper methods and foundation code for all Filament packages.

2331.0M245](/packages/filament-support)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.6k](/packages/rawilk-profile-filament-plugin)[wnx/laravel-backup-restore

A package to restore database backups made with spatie/laravel-backup.

213420.1k2](/packages/wnx-laravel-backup-restore)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
