PHPackages                             brunoggdev/ci4-logging-extended - 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. [Logging &amp; Monitoring](/categories/logging)
4. /
5. brunoggdev/ci4-logging-extended

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

brunoggdev/ci4-logging-extended
===============================

Extended logging for CodeIgniter 4: improved context serialization, exception() method with rich context, web-based Log Viewer with IDE deep links, and exception alerting.

v1.2.2(3w ago)0117↓65%MITPHP

Since Mar 23Pushed 2w agoCompare

[ Source](https://github.com/brunoggdev/ci4-logging-extended)[ Packagist](https://packagist.org/packages/brunoggdev/ci4-logging-extended)[ RSS](/packages/brunoggdev-ci4-logging-extended/feed)WikiDiscussions master Synced 3w ago

READMEChangelog (6)Dependencies (7)Versions (8)Used By (0)

ci4-logging-extended
====================

[](#ci4-logging-extended)

Extended logging for CodeIgniter 4 — part of the [`ci4-*-extended`](https://github.com/brunoggdev) series.

**No database. No migrations. No new infrastructure. No new conventions to learn.**

This package enhances CI4's native logging capabilities **without getting in your way** — every existing `log_message()` and `logger()->*()` call in your app works automatically from the moment you install it. No finicky or complex configurations, **it just works**.

Three things in one package:

1. **Context serialization** — CI4's native logger silently discards context keys that have no matching `{placeholder}`. The extended logger appends them automatically as structured `key=value` pairs.
2. **`exception()` method** — Log a `Throwable` with rich, structured context: location, request, user identity, session, stack trace — all configurable and extensible.
3. **Log Viewer** — A clean web UI for browsing, filtering, and searching your daily log files, with deep links straight to your IDE.

---

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

[](#installation)

```
composer require brunoggdev/ci4-logging-extended
```

That's it. The package auto-registers via its `Services` config — no setup required. To customize behavior (Log Viewer gate, exception context, deep links), publish the config:

```
php spark logging-extended:publish
```

> 💡**Tip:** since config values are returned from methods, `env()` works anywhere in your config. There's no forced convention, but `LE_` is a natural prefix if you want one:
>
> ```
> 'enabled' => (bool) env('LE_VIEWER_ENABLED', true),
> 'perPage' => (int) env('LE_PER_PAGE', 50),
> ```

---

Context serialization
---------------------

[](#context-serialization)

PSR-3 only interpolates context keys that have a matching `{placeholder}` in the message — everything else is discarded by design. With the extended logger, leftover keys are appended automatically:

```
// Native CI4 — context is silently discarded, log only shows "copyProducts failed"
log_message('error', 'copyProducts failed', [
    'source' => $sourceId,
    'target' => $targetId,
    'error'  => $e->getMessage(),
]);
```

With the extended logger:

```
ERROR - 2026-03-20 14:32:01 --> copyProducts failed | source=42 target=99 error="Division by zero"

```

PSR-3 placeholder interpolation still works — keys consumed by `{placeholders}` are not duplicated in the appended context:

```
logger()->warning('Retry {job} on attempt {attempt}', [
    'job'     => 'SendEmail',   // consumed by {job}
    'attempt' => 3,             // consumed by {attempt}
    'reason'  => 'timeout',    // not a placeholder — appended
]);
// WARNING - ... --> Retry SendEmail on attempt 3 | reason=timeout
```

### Value formatting

[](#value-formatting)

PHP typeLog format`null``key=null``true` / `false``key=true` / `key=false`String without spaces`key=value`String with spaces`key="value with spaces"`Array / object`key={"json":"encoded"}`---

`exception()` method
--------------------

[](#exception-method)

Log a `Throwable` with a single call:

```
try {
    // ...
} catch (Throwable $e) {
    logger()->exception($e);              // defaults to 'error' level
    logger()->exception($e, 'warning');   // any PSR-3 level
    logger()->exception($e, 'error', 'Failed to process order #' . $orderId); // custom message
}
```

Default output (with `trace: false` for brevity):

```
ERROR - 2026-03-20 14:32:01 --> [RuntimeException] Something went wrong | message="Something went wrong" location=/var/www/app/Services/OrderService.php:84

```

`location` points to the call site — if the exception was created via a static constructor (e.g. `AppException::for(...)`), the package walks the stack trace to skip the factory frame and report where it was actually called from.

With a custom message, the exception's own message is omitted from context — the call-site message is the log entry:

```
ERROR - 2026-03-20 14:32:01 --> [RuntimeException] Failed to process order #99 | location=/var/www/app/Services/OrderService.php:84

```

### Configuration

[](#configuration)

Publish the config (`php spark logging-extended:publish`) and edit `app/Config/LoggingExtended.php`. Everything lives in the `exception()` method — modify what you need:

```
protected function exception(): array
{
    return [
        'trace'   => true,
        'request' => [
            'enabled' => true,
            'params'  => false,     // GET/POST/JSON body — off by default (privacy)
            'headers' => false,     // true = all headers; array of names = allow-list; false = off
            'redact'  => ['password', 'token', 'api_key', 'authorization', 'cookie', ...],
        ],
        'captures' => [
            'user'    => fn () => ['id' => auth()->id(), 'email' => auth()->user()?->email],
            'session' => false,     // true = all session data; callable for specific keys
            'extra'   => ['tenant' => fn () => session('tenant_id')],
        ],
        'alerts'  => [
            'handlers' => [SlackAlertHandler::class],
            'levels'   => ['critical', 'error'],
            'throttle' => 15 * MINUTE,
        ],
    ];
}
```

#### Rich exception context example

[](#rich-exception-context-example)

With `user`, `request`, `captures`, and `trace` enabled, a single `exception()` call produces:

```
ERROR - 2026-03-20 14:32:01 --> [RuntimeException] Something went wrong | message="Something went wrong" location=/var/www/app/Services/OrderService.php:84 request={"method":"POST","url":"https://app.test/checkout"} user={"id":7,"email":"alice@example.com"} tenant=acme-corp
#0 /var/www/app/Controllers/CheckoutController.php(42): OrderService->process()
#1 /var/www/vendor/codeigniter4/framework/system/Router/Router.php(563): ...
...

```

Every structured key is searchable in the Log Viewer using dot-notation: `user.email=alice@example.com`, `request.method=POST`, `tenant=acme-corp`.

### Extending with external trackers

[](#extending-with-external-trackers)

Override `exception()` in a subclass and call `parent::` to keep the file log entry:

```
use Brunoggdev\LoggingExtended\Logger;

class SentryLogger extends Logger
{
    public function exception(Throwable $e, string $level = 'error', ?string $message = null): void
    {
        \Sentry\captureException($e);
        parent::exception($e, $level, $message);
    }
}
```

Wire your subclass in `app/Config/Services.php` instead of the base logger. The `buildExceptionContext()` method is also `protected` — override it to add fields without replacing the whole method.

### Alert handlers

[](#alert-handlers)

Handlers are notified after each matching log entry is written to file — useful for Slack notifications, PagerDuty, custom webhooks, etc. Configure them in the `alerts` key of your `exception()` method.

A handler can be a closure, an invokable class, or any class with a `handle(LogAlert $alert): void` method:

```
class SlackAlertHandler
{
    public function handle(LogAlert $alert): void
    {
        Http::post(env('SLACK_WEBHOOK'), [
            'text' => "[{$alert->level}] {$alert->message}",
            'user' => $alert->captures['user'] ?? null,
        ]);
    }
}
```

`LogAlert` exposes:

PropertyTypeDescription`$level``string`PSR-3 level in lowercase (`'error'`, `'critical'`, …)`$message``string`Exception message or interpolated log message`$captures``array`Your configured captures: `request`, `user`, `session`, and any `extra` keys`$context``array`Everything else: CI4 native keys (`routeInfo`, `exFile`, …) and any key/value pairs passed to plain `logger()->*()` calls`$timestamp``Time`Alert time in the app timezone`$exception``?Throwable`The raw exception, when triggered via `exception()` or CI4's native handler`$location``?string``file.php:line` of the throw site, smart-resolved for named constructorsA broken handler never takes down the logger — all calls are wrapped in try/catch and the file log is always written first.

`throttle` accepts any duration using CI4's time constants (`15 * MINUTE`, `2 * HOUR`, etc.) and uses an isolated file cache — never touches your app's cache store — to suppress repeat alerts for the same level + message within that window. If a handler returns `false`, the throttle lock is not saved — use this to signal that the alert was not delivered so the next occurrence gets another chance.

---

Log Viewer
----------

[](#log-viewer)

A web UI for browsing your daily log files, with multiple themes to choose from and light/dark mode:

[![Log Viewer — brite theme](docs/brite.png)](docs/brite.png)[![Log Viewer — lumen theme](docs/lumen.png)](docs/lumen.png)[![Log Viewer — litera theme](docs/litera.png)](docs/litera.png)[![Log Viewer — pulse theme](docs/pulse.png)](docs/pulse.png)

### Features

[](#features)

- **Browse** daily log files with pagination and level breakdowns
- **Filter by level** — click any level badge to narrow entries
- **Search** — full-text, dot-notation context lookup, and regex (see [Filtering and search](#filtering-and-search))
- **Shareable links** — search query, level filter, file, and page are all reflected in the URL
- **Live tail** — new entries stream in automatically while viewing today's file (via SSE)
- **File management** — delete individual files or bulk-select and delete multiple at once
- **IDE deep links** — click any stack frame to jump straight to that file and line in your editor
- **Occurrence tracking** — repeated messages show `2/5` to surface noise without losing context

### Setup

[](#setup)

The viewer is enabled by default in your published config. Set the `gate` in your `viewer()` method to control who can access it:

```
protected function viewer(): array
{
    return [
        // ...
        'gate' => fn () => auth()->loggedIn() && auth()->user()->isAdmin(),
        // or: 'gate' => self::GATE_LOGIN,
    ];
}
```

Using `self::GATE_LOGIN` activates the built-in login page — run `php spark log-viewer:set-password` to set the password (stored as a bcrypt hash in `.env`).

By default (before publishing) the gate allows access only in the `development` environment and denies with 404 everywhere else.

You can also require existing CI4 filters before the gate fires — useful if you have an existing auth filter:

```
'routes' => [
    'path'    => 'logs',
    'filters' => ['auth'],  // CI4 filter aliases applied before the gate
],
```

### Accessing the viewer

[](#accessing-the-viewer)

The viewer mounts at `/logs` by default. Change `routes.path` in your `viewer()` method to move it:

```
'routes' => [
    'path'    => 'devtools/logs',
    'filters' => [],
],
```

### Filtering and search

[](#filtering-and-search)

The search box supports:

QueryMatches`payment failed`entries containing both words (AND logic)`user.email=alice@example.com`dot-notation equality`request.method=POST`dot-notation equality`user.email=.*@example\.com`dot-notation with regex`user.id`dot-notation presence check (key exists)Multiple space-separated terms all must match (AND, not OR).

### IDE deep links

[](#ide-deep-links)

Stack frame and `location` file paths in the viewer link directly to your editor (`vscode://` / `phpstorm://`).

These settings are **per-developer and stored in the browser** — open the **⚙ Settings** panel (top of the sidebar) and set your editor, your local project path, and an optional WSL distro. Each developer configures their own once, which is what makes deep links work on a **shared staging/production** viewer: there's no single server-wide local path to fight over.

The server path is **auto-detected** (`ROOTPATH`) and rewritten to your local path, so the same logs deep-link correctly on every machine. The first time you click a deep link without a local path set, the Settings panel opens automatically.

The `deeplink` config block only seeds first-run defaults and handles one server-side edge case:

```
'deeplink' => [
    // First-run defaults seeded into each viewer's browser. Developers override
    // these in the Settings panel — no need to set them per environment.
    'ide'        => 'vscode',   // 'vscode', 'phpstorm', or null
    'wslDistro'  => null,       // WSL distro name (VS Code only)
    'localPath'  => null,       // a developer's local project path

    // Server path prefix rewritten to each developer's localPath.
    // null = auto-detect from ROOTPATH (recommended). Set it only for symlinked
    // releases or containers where the paths recorded in the logs differ.
    'serverPath' => null,
],
```

> **Upgrading from a single-`localPath` setup?** Your existing `ide` / `wslDistro` / `localPath` values still apply as each browser's first-run defaults, so nothing breaks. Going forward, configure per-developer in the Settings panel; these seed keys are deprecated and will be removed in v2.

### Viewer configuration reference

[](#viewer-configuration-reference)

```
protected function viewer(): array
{
    return [
        'enabled'  => true,         // false = completely hide the viewer and its routes
        'gate'     => null,         // null = deny (404); GATE_LOGIN = built-in login; callable = custom
        'routes'   => [
            'path'    => 'logs',    // URL path where the viewer is accessible
            'filters' => [],        // CI4 filter aliases applied before the gate
        ],
        'deeplink' => [                 // first-run seeds; configured per-developer in Settings
            'ide'        => 'vscode',   // 'vscode', 'phpstorm', or null
            'wslDistro'  => null,       // WSL distro name (VSCode only)
            'localPath'  => null,       // a developer's local path
            'serverPath' => null,       // null = auto-detect (ROOTPATH); set for symlink/container deploys
        ],
        'perPage'  => 50,           // entries per page
    ];
}
```

---

`log:tail`
----------

[](#logtail)

Watch your log file live in the terminal:

```
php spark log:tail
```

Shows the last 20 lines on startup, then streams new entries as they arrive. Rolls over automatically at midnight.

### Options

[](#options)

OptionDescriptionDefault`-level`Filter by log level—`-filter`Filter lines containing this text (case-insensitive)—`-lines`Lines to show on startup (`0` to skip history)`20`### Examples

[](#examples)

```
php spark log:tail                                        # watch everything
php spark log:tail -level error -lines 0                 # errors only, no history
php spark log:tail -filter payment -lines 100            # keyword filter, last 100 lines
php spark log:tail -level warning -filter checkout       # combine both filters
```

### Output

[](#output)

```
  ERROR      2026-03-20 14:32:01  copyProducts failed | source=42 error="Timeout"
  WARNING    2026-03-20 14:32:05  Retry scheduled | job=CopyProducts attempt=2
  INFO       2026-03-20 14:32:10  Job completed successfully

```

Level labels are colorized: red for `error`/`critical`/`alert`/`emergency`, yellow for `warning`, cyan for `notice`, green for `info`, gray for `debug`. The context block (after `|`) is highlighted in cyan.

---

Related packages
----------------

[](#related-packages)

- [brunoggdev/ci4-events-extended](https://github.com/brunoggdev/ci4-events-extended)

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance96

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 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 ~11 days

Recently: every ~2 days

Total

7

Last Release

24d ago

Major Versions

v0.1.1 → v1.0.02026-05-20

### Community

Maintainers

![](https://www.gravatar.com/avatar/6e9c442eeefed0fff4829fc7eb398a15f1b4b84ccf2dec8dcf879a8f473ea6c6?d=identicon)[brunoggdev](/maintainers/brunoggdev)

---

Top Contributors

[![brunoggdev](https://avatars.githubusercontent.com/u/114432497?v=4)](https://github.com/brunoggdev "brunoggdev (11 commits)")

### Embed Badge

![Health badge](/badges/brunoggdev-ci4-logging-extended/health.svg)

```
[![Health](https://phpackages.com/badges/brunoggdev-ci4-logging-extended/health.svg)](https://phpackages.com/packages/brunoggdev-ci4-logging-extended)
```

###  Alternatives

[psr/log

Common interface for logging libraries

10.4k1.2B10.8k](/packages/psr-log)[open-telemetry/api

API for OpenTelemetry PHP.

1938.5M261](/packages/open-telemetry-api)[open-telemetry/sdk

SDK for OpenTelemetry PHP.

2326.5M315](/packages/open-telemetry-sdk)[illuminated/console-logger

Logging and Notifications for Laravel Console Commands.

8676.7k](/packages/illuminated-console-logger)

PHPackages © 2026

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