PHPackages                             scafera/log - 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. scafera/log

ActiveSymfony-bundle[Logging &amp; Monitoring](/categories/logging)

scafera/log
===========

Structured logging for the Scafera framework

v1.0.2(1mo ago)0171MITPHPPHP &gt;=8.4

Since Apr 13Pushed 1mo agoCompare

[ Source](https://github.com/scafera/log)[ Packagist](https://packagist.org/packages/scafera/log)[ Docs](https://github.com/scafera/log)[ RSS](/packages/scafera-log/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (3)Dependencies (3)Versions (4)Used By (1)

scafera/log
===========

[](#scaferalog)

Structured logging for the Scafera framework. Implements PSR-3 with a zero-dependency `StreamLogger` that writes JSON Lines to `var/log/{environment}.log`.

> **Provides:** Structured logging (PSR-3) for Scafera — a zero-dependency `StreamLogger` that writes JSON Lines to `var/log/{env}.log`. Uncaught framework exceptions are auto-logged; application code writes its own entries.
>
> **Depends on:** A Scafera host project (kernel + architecture package) with a writable `var/log/` directory on reliable local storage. Application code injects `Psr\Log\LoggerInterface` — not `StreamLogger` directly.
>
> **Extension points:** None by design — `StreamLogger` is not extensible. You can alias `Psr\Log\LoggerInterface` to a custom implementation, but doing so breaks the `logs:*` CLI commands, which depend on the JSON Lines format.
>
> **Not responsible for:** Log rotation (delegated to `logrotate`/OS) · best-effort logging (`RuntimeException` on write failure, per ADR-049) · application log routing or multi-destination (use Monolog manually) · runtime validation of the `event` key (build-time only via `EventContextValidator`) · Symfony's default error logging (disabled via compiler pass to avoid duplicate entries).

This is a **capability package**. It adds optional structured logging to a Scafera project. It does not define folder structure or architectural rules — those belong to architecture packages.

Core Idea
---------

[](#core-idea)

Scafera treats logging the same way it treats every other capability — explicit, minimal, and boundary-safe. Application log calls are written by the developer at the call site — no userland listeners, middleware, or automatic logging. Framework-level errors (uncaught exceptions, console failures) are captured automatically by the package as infrastructure. The logger writes structured JSON Lines with a required `event` field for categorization — required by build-time validation (`scafera validate`), not enforced at runtime.

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

[](#installation)

```
composer require scafera/log
```

The bundle is auto-discovered via Scafera's `symfony-bundle` type detection. No manual registration needed.

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

[](#requirements)

- PHP 8.4+
- `scafera/kernel` ^1.0
- `psr/log` ^3.0

Usage
-----

[](#usage)

Inject `Psr\Log\LoggerInterface` via constructor — it resolves to `StreamLogger` automatically:

```
use Psr\Log\LoggerInterface;

final class OrderService
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function place(Order $order): void
    {
        // ... business logic ...

        $this->logger->info('Order placed', [
            'event' => 'order.created',
            'orderId' => $order->getId(),
        ]);
    }
}
```

### Event Field Convention

[](#event-field-convention)

Every log call should include an `event` key in the context array. Events use lowercase dot notation (`domain.action`):

```
$this->logger->info('User signed in', ['event' => 'auth.login', 'userId' => 42]);
$this->logger->error('Payment failed', ['event' => 'payment.failed', 'orderId' => 1]);
$this->logger->warning('Slow query', ['event' => 'db.slow_query', 'duration_ms' => 1250]);
```

The `event` key is extracted from the context array and promoted to a top-level JSON field — it does not appear inside `context`. The remaining context is written under `context`, which is omitted entirely when empty after extraction. This makes log entries greppable and filterable by the CLI commands.

### Log Format

[](#log-format)

Each line is a self-contained JSON object:

```
{"timestamp":"2026-04-10T16:22:40.501+00:00","level":"info","message":"Order placed","event":"order.created","ip":"192.168.1.42","context":{"orderId":1}}
```

Fields: `timestamp` (RFC3339 with milliseconds), `level` (PSR-3), `message`, `event` (present only when the context includes an `event` key), `ip` (client IP address, present only during HTTP requests), `context` (remaining context after `event` extraction, omitted if empty). Client IP is logged automatically during HTTP requests. To disable, override the `StreamLogger` service definition and omit the `RequestStack` argument.

### Context Serialization

[](#context-serialization)

The context array is sanitized to produce valid JSON. Scalars and arrays pass through, `\Throwable` instances (under the `exception` key per PSR-3 convention) are expanded to structured data, and non-serializable values are reduced to a safe representation. The exact serialization rules are an implementation detail and may evolve, but the output is always valid JSON.

Example of `\Throwable` serialization:

```
$this->logger->error('Payment failed', [
    'event' => 'payment.failed',
    'exception' => $e,
]);
```

```
{"timestamp":"...","level":"error","message":"Payment failed","event":"payment.failed","context":{"exception":{"class":"App\\Exception\\PaymentFailedException","message":"Card declined","code":0,"file":"/app/src/Service/PaymentService.php","line":42}}}
```

### Failure Behavior

[](#failure-behavior)

Logging failures throw `\RuntimeException`. If the log directory doesn't exist, permissions are wrong, or the underlying disk cannot accept the write, the error is visible immediately — no silent failures, no fallback channel. The underlying OS error message is captured via a temporary error handler and folded into the exception message, so the exception text includes the real reason (e.g. `No space left on device`, `Permission denied`).

This is a conscious trade-off, documented in [ADR-049](../../docs/ADR/decisions.md#adr-049-logging-is-a-failure-surface-fail-loud-io): Scafera treats logging as part of the system's feedback loop, not a best-effort side channel. A successful request that was not logged is *unobservable*, and in Scafera's model an unobservable success is worse than a visible failure. Projects that need best-effort logging (log loss acceptable to preserve availability) should use a different PSR-3 implementation.

The logger does **not** validate the `event` key at runtime. It writes whatever context it receives. Structured logging (consistent `event` key, lowercase dot notation) is guaranteed only when `EventContextValidator` is run via `scafera validate`.

### Storage and Rotation

[](#storage-and-rotation)

Because write failures propagate as exceptions, storage choice matters:

- **Logs must be written to reliable local storage.** Local disk, local tmpfs, or a mounted volume backed by local block storage.
- **NFS and other network filesystems are not recommended as a log sink.** Network storage introduces failure modes (remote unavailability, soft-mount timeouts, partial writes) that will surface as `RuntimeException` and affect request handling. If you mount `var/log/` over NFS, you accept that transient network issues will cause request failures.
- **Rotation is delegated to the operating system.** `scafera/log` does not implement in-process rotation — the hot write path is intentionally simple. Use `logrotate` (or an equivalent) configured to rotate on size or date. `StreamLogger` opens the log file fresh on every write via `file_put_contents`, so post-rotation writes go to the new file without needing a reload signal.

Framework Error Logging
-----------------------

[](#framework-error-logging)

When `scafera/log` is installed, uncaught exceptions are automatically logged to the same log file alongside application entries. This is framework infrastructure — the log package takes ownership of error visibility.

**HTTP exceptions** are logged with event `framework.http.error`:

- 4xx client errors (404, 403, etc.) are logged at `warning` level — these are client mistakes, not system failures
- 5xx server errors and unhandled exceptions are logged at `error` level

```
{"timestamp":"...","level":"warning","message":"No route found for \"GET /nonexistent\"","event":"framework.http.error","context":{"exception":{...},"method":"GET","path":"/nonexistent","status":404}}
```

**Console exceptions** are logged with event `framework.console.error` at `error` level, including the command name and exit code.

The CLI commands work naturally with framework entries — `logs:errors` shows framework 500s, `logs:filter framework.http.error` isolates framework entries, and `logs:stats` counts them as a distinct event.

Symfony's built-in error logging is disabled via a compiler pass to prevent duplicate entries. Symfony's error response handling (the error page in dev, the error controller in prod) continues to work normally.

Build-Time Validation
---------------------

[](#build-time-validation)

The `EventContextValidator` runs during `scafera validate` and checks:

- Every logger call in `src/` includes an `'event' =>` key in the context
- Event values match the format `domain.action` (lowercase dot notation: `/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/`)

The validator uses PHP's tokenizer (`\PhpToken::tokenize`, not an AST parser). It handles single-line and multi-line logger calls, inspects only the top level of the context array (nested `'event'` keys are ignored), and format-checks event values only when they are string literals — dynamic values like `Event::CREATED` are accepted as present. Context built via a variable (`$logger->info($msg, $context)`), method return, or array spread cannot be inspected and is silently skipped.

CLI Commands
------------

[](#cli-commands)

All commands are available via `vendor/bin/scafera`:

```
# Show latest log entries (default 50)
vendor/bin/scafera logs:recent

# Show latest 10 entries
vendor/bin/scafera logs:recent --limit=10

# Show latest entries filtered by level
vendor/bin/scafera logs:recent --level=error

# Show only application logs (excludes framework.* events)
vendor/bin/scafera logs:recent --scope=app

# Show only framework logs
vendor/bin/scafera logs:recent --scope=framework

# Operational summary — errors/warnings from the last 24 hours (by timestamp), top events, recent failures
vendor/bin/scafera logs:status

# Show entries with severity >= error (error, critical, alert, emergency)
vendor/bin/scafera logs:errors

# Aggregate counts grouped by event, with level column
vendor/bin/scafera logs:stats

# Group by event and level separately
vendor/bin/scafera logs:stats --by-level

# Filter by event name
vendor/bin/scafera logs:filter order.created

# Filter by level
vendor/bin/scafera logs:filter --level=warning

# Search text in messages or context
vendor/bin/scafera logs:filter --search="timeout"

# Filter by scope
vendor/bin/scafera logs:filter --scope=app
vendor/bin/scafera logs:filter --scope=framework

# Combine filters (AND logic — all conditions must match)
vendor/bin/scafera logs:filter --level=error --scope=app

# Limit results (default 50 for both logs:filter and logs:errors)
vendor/bin/scafera logs:errors --limit=10

# Clear the log file
vendor/bin/scafera logs:clear

# JSON output (all commands)
vendor/bin/scafera logs:status --json
vendor/bin/scafera logs:errors --json
vendor/bin/scafera logs:stats --json
vendor/bin/scafera logs:recent --json
vendor/bin/scafera logs:filter order.created --json
```

### JSON Output

[](#json-output)

All commands support `--json` for machine-readable output. The format uses a `meta` + `data` envelope:

```
{
    "meta": {"command": "logs:stats", "env": "dev", "file": "/app/var/log/dev.log"},
    "data": [...]
}
```

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

[](#configuration)

None. The bundle registers `StreamLogger` with `%kernel.logs_dir%` and `%kernel.environment%` — one logger, one file per environment. No config keys, no extension points.

While it is possible to override the logger by aliasing `Psr\Log\LoggerInterface` to your own class, this is **not recommended**. Scafera packages are designed to work together — `StreamLogger` produces the JSON Lines format that the CLI commands (`logs:stats`, `logs:filter`, `logs:errors`, `logs:status`) depend on. A custom implementation that changes the output format will break CLI compatibility. Prefer staying with `StreamLogger` unless you have a specific need that it cannot satisfy.

```
# Not recommended — only if StreamLogger truly cannot meet your needs
services:
    Psr\Log\LoggerInterface:
        alias: App\CustomLogger
```

If you must override, your implementation should write JSON Lines to `var/log/{environment}.log` with the same field structure (`timestamp`, `level`, `message`, `event`, `context`) to maintain CLI compatibility.

Roadmap
-------

[](#roadmap)

### Monolog Adapter

[](#monolog-adapter)

A built-in adapter for `monolog/monolog` for applications needing log routing, multiple outputs, or advanced formatting. Deferred — `StreamLogger` covers the common case, and applications can wire Monolog manually via the `LoggerInterface` alias override.

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance89

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

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

Total

3

Last Release

54d ago

### Community

Maintainers

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

---

Top Contributors

[![samaphp](https://avatars.githubusercontent.com/u/531627?v=4)](https://github.com/samaphp "samaphp (23 commits)")

---

Tags

logpsr-3phploggingscafera

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/scafera-log/health.svg)

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

###  Alternatives

[sentry/sentry

PHP SDK for Sentry (http://sentry.io)

1.9k240.0M312](/packages/sentry-sentry)[analog/analog

Fast, flexible, easy PSR-3-compatible PHP logging package with dozens of handlers.

3511.6M24](/packages/analog-analog)[markrogoyski/simplelog-php

Powerful PSR-3 logging. So easy, it's simple.

2819.1k4](/packages/markrogoyski-simplelog-php)

PHPackages © 2026

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