Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Monolog Browser Bundle Laravel Package

lexik/monolog-browser-bundle

Symfony bundle that provides a web interface for browsing, filtering, and searching Monolog logs. Inspect entries by channel, level, and date, view stack traces, and page through results without digging through raw log files.

View on GitHub
Deep Wiki
Context7

Getting Started

lexik/monolog-browser-bundle gives you two things: a Monolog handler that writes log records to a Doctrine DBAL table, and a small Symfony web UI to filter and read them. It is most useful when you need cross-request, cross-user logs on a shared dev or staging box — the cases where tail -f var/log/dev.log stops being practical. It is not a production observability tool, and with a last release in 2016, 59 stars, and essentially no dependents, treat it as legacy software. Spike it against your Symfony/PHP version before committing.

Minimal setup for a Symfony 2/3-era app:

  1. Install as a dev dependency. composer require --dev lexik/monolog-browser-bundle. On Symfony 4+ you will also need to register the bundle in config/bundles.php; on Symfony 2/3 add it to AppKernel.
  2. Import the routes under a prefix, e.g. /monolog, from the bundle's routing file. The UI lives at that prefix.
  3. Create the log table. The bundle writes with raw DBAL, not an entity, so it does not manage schema migrations. Look in the bundle's Resources/ for the shipped SQL/schema, or run php bin/console list lexik to discover any table-create/purge commands. Add indexes on created_at, level, and channel immediately — the default schema is minimal and the UI filters on those columns.
  4. Configure the bundle. The important options are in DependencyInjection/Configuration.php: table name, allowed environments, base layout/template overrides, and route/UI settings. Restrict allowed_environments to dev/staging (this is a visibility guard, not authentication).
  5. Wire the handler into Monolog. Find the exact handler service ID in the bundle's Resources/config/services.xml (commonly documented as lexik_monolog_browser.monolog.handler). Then use Monolog's type: service:
monolog:
    handlers:
        monolog_browser:
            type: service
            id: lexik_monolog_browser.monolog.handler   # verify in services.xml
            level: debug
            channels: ["!doctrine", "!event"]
  1. Open the UI at /monolog, reproduce a problem, and filter by channel, level, message, and date range. That is the first real use case: instead of grepping one log file, you see what the application logged across requests, with context and stack traces attached.

Where to look first when something does not behave: Handler/ for how records are inserted and serialized, Resources/config/services.xml for service IDs and constructor arguments, DependencyInjection/Configuration.php for every option, Resources/views/ for the UI and template override points, and Command/ for purge/schema commands.

Implementation Patterns

Run it alongside file logs, not instead of them. The DBAL handler is synchronous and one INSERT per record. Keep your normal fingers_crossed/stream handler for the full log and add the browser handler as a secondary debug/staging sink:

monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: file
        file:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
        db:
            type: whatfailuregroup
            members:
                monolog_browser:
                    type: service
                    id: lexik_monolog_browser.monolog.handler
                    level: warning
                    channels: ["!doctrine", "!event"]

Wrapping the DB handler in whatfailuregroup is worth doing: if the log database is down or the insert fails, you do not want a logging failure to become the exception that breaks the request or masks the original one.

Use channels deliberately. The UI is only as useful as your channel names. Define app-level channels (app, billing, import, api) and filter by them. Exclude doctrine, event, and often security from the DB handler; otherwise ORM SQL noise and security chatter drown out the signal and can trigger the recursion problem described below.

Add request correlation via a processor. The UI may not expose arbitrary extra filters, but the data is still in the table. A small Monolog processor that adds a request ID, route, and user identifier turns direct SQL into a per-request trace viewer:

final class RequestContextProcessor
{
    public function __invoke(array $record): array
    {
        $record['extra']['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8));
        $record['extra']['route'] = $_SERVER['REQUEST_URI'] ?? 'cli';
        // add user id from your security context if available
        return $record;
    }
}

Then, when the UI shows one interesting error, query all records for that request ID directly:

SELECT created_at, channel, level, message, context, extra
FROM monolog_entries
WHERE JSON_EXTRACT(extra, '$.request_id') = ?
ORDER BY created_at;

Even if extra is a serialized blob rather than native JSON, the pattern holds: the database is the real interface, the UI is a convenience.

Treat the table as queryable data. Beyond the built-in browser, use it for daily error-rate checks, rough alerting, or a team dashboard:

SELECT channel, level, COUNT(*) AS hits
FROM monolog_entries
WHERE created_at > NOW() - INTERVAL 1 DAY
GROUP BY channel, level
ORDER BY hits DESC;

You can fingerprint messages (strip IDs, paths, numbers) in SQL or a small script to find regressions after deploys.

Schedule retention. The bundle ships or documents a purge/clear command; discover the exact name with bin/console list lexik rather than copying old blog posts. Run it from cron, or do a direct DELETE FROM ... WHERE created_at < .... Without retention, a busy staging environment fills the table and the UI becomes slow. Keep days, not weeks, in dev; shorter in staging; and vacuum/optimize periodically.

Put the UI behind real access control. allowed_environments merely disables routes outside listed environments. Add a security firewall rule, an IP allowlist, basic auth at the reverse proxy, or a VPN requirement. Logs frequently contain SQL, stack traces with absolute paths, user data, and sometimes credentials in context.

Embed the UI in your admin theme. If the default page does not match your admin layout, use the bundle's base_layout option or override the Twig templates. On Symfony 3.4+/4+ override under templates/bundles/LexikMonologBrowserBundle/; on older apps use app/Resources/LexikMonologBrowserBundle/views/. This is usually a copy-and-adjust of the layout, not a rewrite.

Disable it in tests and CI. Add a test environment Monolog config that omits the DB handler, or point it to a dedicated test database. Otherwise every test run writes noise into the shared table and can slow the suite. If you do test against it, truncate the table in setUp and use a separate connection so transaction rollbacks do not hide log rows.

Gotchas and Tips

  • It is abandoned. Last release 2016-09-27. Expect incompatibilities with PHP 8 (DBALException, old Statement::execute signatures, dynamic properties), Doctrine DBAL 3 (fetchAll → fetchAllAssociative, execute changes), Monolog 2/3 handler interfaces, and Symfony 5/6 (removed base controller, annotation routing, kernel changes). Pin old versions, fork it, or budget time to copy the handler into your own code — it is small.
  • Doctrine logging feedback loop. If Doctrine SQL logging goes through Monolog and your DB handler accepts the doctrine channel, every log INSERT is itself logged, producing another INSERT. The table grows absurdly fast. Always exclude doctrine (channels: ["!doctrine"]) and verify by watching row counts after a single request.
  • A broken DB can break the request. Logging failures inside error handling can throw a second exception. Wrap the handler in whatfailuregroup or make the handler swallow exceptions and continue. This matters most exactly when you need logs: during outages.
  • One synchronous INSERT per record is expensive. At debug in a busy environment, the handler becomes a performance problem and a write hotspot. Log warning or higher in shared environments, or accept the cost only for low-traffic dev.
  • The default schema likely lacks useful indexes. Add indexes on created_at, level, and channel; ideally a composite index for date + level queries. The UI's message search is usually a LIKE '%...%' scan, which does not use a normal index and gets slow quickly.
  • Large context payloads blow up rows. Logging an entire entity, request body, or HTTP client response can exceed TEXT limits or max_allowed_packet, and can fail serialization. Add normalizers/processors that truncate strings and convert objects to scalars. Never log secrets or full PII; both DB access and the UI expose it.
  • Serialization failures are silent until they are not. json_encode fails on invalid UTF-8, resources, recursion, and closures. If entries disappear, check whether the handler is serializing context/extra and add a normalizer.
  • type: service wiring can ignore Monolog options. Depending on the MonologBundle version, level, channels, and formatter may not be applied the way they are for built-in handler types, or may be applied by wrapping the service. Inspect the compiled container with debug:container and set critical options inside the service definition if needed.
  • Environment mismatch yields a 404, not a helpful error. If the UI returns not found, confirm allowed_environments includes the current kernel.environment, the routes are imported with the expected prefix, and the cache is cleared.
  • Clear the cache after config or routing changes. In prod-like environments, stale container/route caches mask configuration edits and lead to confusing "my change did nothing" sessions.
  • Schema management can fight you. Because there is no entity, doctrine:schema:update --force may try to drop or ignore the log table. Keep the table outside your normal entity schema workflow, or explicitly exclude it.
  • Timezone drift. created_at may be written in PHP's timezone while the DB stores something else; date filters in the UI can be off by hours. Standardize on UTC at write time and convert only for display.
  • Console and worker logs use the same table. Command output and long-running worker logs often have no request context and can dominate the UI. Give them their own channel and filter or exclude them, and watch for stale DBAL connections in workers.
  • Missing entries usually mean handler configuration, not data loss. Check level, channels, bubble, and whether an outer fingers_crossed or buffer handler never flushed. Confirm the record reaches Monolog at all before debugging the DB insert, then check the DB general log for the INSERT.
  • No authentication, no CSRF, no access model. The bundle is a developer tool, not an admin product. Front it with your existing security stack and avoid exposing it on public interfaces.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect