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.
Package info
bitbucket.org/emielburgman/symfony-log-viewer
Type:symfony-bundle
pkg:composer/emielburgman/symfony-log-viewer
Requires
- php: >=8.2
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
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) andJsonFormatter({"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
rotatingsource readsprod.logand its dated siblingsprod-2026-07-27.log. Siblings whose mtime falls outside the window are never opened, somax_files: 30does not mean thirty file reads. - Deploy sessions.
deploy.logis split on the=== Deploy started/finished at … ===markers thatdeploy.shwrites. A started-but-never-finished block is reported asin_progress, which is how a deploy that aborted underset -euo pipefailshows 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 amessage_patternmust 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.