emielburgman/symfony-log-viewer

Config-driven admin log viewer for Symfony applications: parses Monolog line and JSON formats, handles rotating files, deploy-session logs and log truncation.

Maintainers

Package info

bitbucket.org/emielburgman/symfony-log-viewer

Type:symfony-bundle

pkg:composer/emielburgman/symfony-log-viewer

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

v1.1.1 2026-07-30 11:48 UTC

This package is auto-updated.

Last update: 2026-07-30 12:18:22 UTC


README

A config-driven log reader for a Symfony admin screen. Shared by portfolio and wealth-monitor so a fix lands once and both apps pick it up.

The bundle gives you the data: parsed log entries, deploy sessions, and log file metadata. Each app renders its own Twig, because the two do not share a design system.

What it handles

  • Both Monolog formats. LineFormatter ([2026-07-27T…] app.ERROR: msg) and JsonFormatter ({"message":…,"datetime":…}) are sniffed per line, not per file — our apps use JSON in prod and the line format in dev, and a single file can contain both after a config change.
  • Rotating files. A rotating source reads prod.log and its dated siblings prod-2026-07-27.log. Siblings whose mtime falls outside the window are never opened, so max_files: 30 does not mean thirty file reads.
  • Deploy sessions. deploy.log is split on the === Deploy started/finished at … === markers that deploy.sh writes. A started-but-never-finished block is reported as in_progress, which is how a deploy that aborted under set -euo pipefail shows up.
  • Truncation. Log files matching a configured pattern can be emptied without being deleted, so the logger keeps writing to the same inode.

Install

composer require emielburgman/symfony-log-viewer

Register the bundle in config/bundles.php:

Emielburgman\LogViewerBundle\LogViewerBundle::class => ['all' => true],

Configure

config/packages/log_viewer.yaml:

log_viewer:
  logs_dir: '%kernel.logs_dir%'
  deploy_log: 'deploy.log'

  # Filenames or glob patterns that may be listed and truncated.
  managed_files:
    - 'dev.log'
    - 'prod*.log'
    - 'security.log'
    - 'deploy.log'

  # Each source becomes a tab on the admin screen.
  sources:
    errors:
      label: 'Errors'
      file: '%kernel.environment%.log'
      rotating: true
      days: 7
      limit: 500
      levels: [ERROR, CRITICAL, ALERT, EMERGENCY]

    auth:
      label: 'Auth failures'
      file: '%kernel.environment%.log'
      rotating: true
      channels: [security]
      message_pattern: '/failure|failed|invalid|denied|blocked/i'

Per-source keys: label, file (required), rotating, days, limit, levels, channels, message_pattern. Empty levels/channels mean "all".

% in a message_pattern must be escaped as %%. The value goes through the container's parameter resolution, which reads a lone % as the start of a parameter name.

Use

public function logs(LogViewerService $logViewer): Response
{
    return $this->render('admin/logs.html.twig', [
        'sources'  => $logViewer->getSources(),   // config, for tab labels
        'entries'  => $logViewer->getAllEntries(), // keyed by source name
        'deploys'  => $logViewer->getDeploySessions(50),
        'logFiles' => $logViewer->getLogFiles(),
    ]);
}

LogEntry exposes ts, level, channel, message; DeploySession exposes ts, status, duration, output plus isSuccess(); LogFile exposes filename, path, size, exists plus getHumanSize() and isTruncatable(). All are public readonly properties, so Twig reads them as entry.level directly.

Truncation is guarded: always route user input through truncate(), which rejects anything that is not a bare filename matching a configured pattern, and pair it with a CSRF token in the controller.

Maintaining this across both apps

The package lives in its own git repo. Both apps consume it through composer, so the update path is: change here → tag → composer update in each app.

Day-to-day: change the package from inside an app

Add a path repository above the VCS one in the app's composer.json while you work. Composer prefers the first repository that can satisfy the constraint, and path repositories symlink by default, so edits in either checkout are the same files:

"repositories": [
  { "type": "path", "url": "../symfony-log-viewer", "options": { "symlink": true } },
  { "type": "vcs", "url": "https://bitbucket.org/emielburgman/symfony-log-viewer.git" }
]
composer update emielburgman/symfony-log-viewer

Remove the path entry before committing. It resolves relative to the project root, which does not exist on the production host, so composer install during deploy would fail — and deploy.sh runs under set -euo pipefail, so that failure aborts the migrations and cache warmup behind it.

Shipping a change

cd ../symfony-log-viewer
git commit -am "log-viewer: <what changed>"
git tag v1.1.0 && git push --follow-tags

Then in each app:

composer update emielburgman/symfony-log-viewer

Both apps track ^1.0, so a minor tag reaches them on the next update, and a breaking change needs a 2.0.0 tag plus an explicit constraint bump — which is the point: neither app can be broken by a push to this repo alone.

Rules that keep it shareable

  • No app entities, repositories or routes in here. wealth-monitor's AuditLog-backed tabs stay in wealth-monitor; only file-based log reading is shared.
  • No Twig, no CSS. portfolio is Tailwind 3 + Stimulus, wealth-monitor is Tailwind 4 + Alpine. Templates are per-app on purpose.
  • New behaviour arrives as configuration, not as an if ($app === …).

Tests

composer install && vendor/bin/phpunit

License

MIT — see LICENSE.