Log viewer
daravel_log_viewer serves a page, from your application, that reads its
log files: the newest entries first, filtered by level or text, a stack
trace a click away, and new entries as they are written. It reads a file
from its end, so the last entries of a 500 MB file come back in under a
millisecond, in the same memory as those of a small one.
dartisan pub:add log-viewer
pub:add adds the dependency, the .withLogViewer() call below (without
middleware: add it before the page leaves local) and the LOG_VIEWER_*
settings to .env.example. With dart pub add daravel_log_viewer you
register it yourself:
// bootstrap/app.dart
import 'package:daravel_log_viewer/daravel_log_viewer.dart';
Application createApp() => Application.configure(basePath: basePath)
.withLogViewer(middleware: ['auth', 'can:view-logs'])
// ...
.create();
The page is at /log-viewer (LOG_VIEWER_PATH, or withLogViewer(path: '/admin/logs')). It reads the folder the daily log channel writes to
(LOG_PATH, storage/logs), so log to a file:
LOG_STACK=console,daily
Who can see it#
Logs say a lot about an application. Outside APP_ENV=local and testing
the viewer answers 403 until withLogViewer is given middleware, such as
auth and a gate:
// bootstrap/policies.dart
gate.define('view-logs', (user, args) => user is User && user.isAdmin);
LOG_VIEWER_ENABLED=false turns it off (404) everywhere.
What it shows#
- Files. Every
.logfile of the folder, newest first, with its size;daravel-YYYY-MM-DD.logfiles from Daravel andlaravel.logfrom a Laravel application sharing the server alike. - Entries. Time, level, message; a click shows the context (as JSON), the error and the stack trace, where the application's frames stand out from the framework's and the SDK's.
- Levels and search. Level chips (
emergencytodebug) and a search over the whole entry: a request id, a user id, an exception's class. - Live. New entries appear as they are written, over server-sent events, filtered the same way.
- Older entries. Pages go back through the file without reading it from the start.
Daravel writes one JSON object per line (time, level, channel,
message, context, error, trace); Laravel writes Monolog lines
([2026-10-28 08:00:00] production.ERROR: ...) followed by the stack
trace. Both are read. A line in neither format is shown as it is.
Secrets#
The logger already replaces the values of LOG_REDACT keys when it writes
an entry. The viewer also hides, before an entry leaves the server, the
values of LOG_VIEWER_MASK_KEYS keys (password, secret, token,
authorization, cookie, api_key, app_key, credit_card by default,
matched as parts of a key, ignoring case): in the context at any depth, and
in text such as password=..., "token": "..." or Authorization: ....
The page loads nothing from other hosts: its script and styles are inline,
allowed by a fresh nonce in a Content-Security-Policy that permits
nothing else, and log text is only ever inserted as text, never as HTML.
A file name must be a .log file directly in the folder: paths, ..,
hidden files and links out of the folder answer 404.
Settings#
| Variable | Default | |
|---|---|---|
LOG_VIEWER_ENABLED |
true |
false answers 404 |
LOG_VIEWER_PATH |
/log-viewer |
the page's URL |
LOG_VIEWER_DIRECTORY |
LOG_PATH |
the folder of the log files |
LOG_VIEWER_PER_PAGE |
50 |
entries per page, at most 500 |
LOG_VIEWER_ALLOW_DOWNLOAD |
true |
the Download button |
LOG_VIEWER_ALLOW_DELETE |
false |
the Delete button (DELETE request) |
LOG_VIEWER_MASK_KEYS |
see above | keys whose values are hidden |
LOG_VIEWER_MAX_SCAN_MB |
64 |
how much of a file one filtered page reads before it offers to read on |
The API#
The page uses JSON endpoints under its path, which tools can use too (behind the same middleware):
GET /log-viewer/api/files |
the files, newest first |
GET /log-viewer/api/entries?file=&levels=error,warning&search=&before= |
a page of entries, newest first, and next, the before of the older page |
GET /log-viewer/api/tail?file=&levels=&search= |
server-sent entry events |
GET /log-viewer/api/download?file= |
the file |
DELETE /log-viewer/api/files?file= |
deletes the file (when allowed) |
Large files#
bench/bin/log_viewer_bench.dart (an AOT build, Windows 11, Dart 3.13.4):
| File | Newest 50 entries | A search that matches nothing (64 MB read) | Peak memory |
|---|---|---|---|
| 100 MB | 0.8 ms | 1.2 s | 17 MB |
| 500 MB | 0.6 ms | 0.9 s | 17 MB |
The memory is the process's whole peak, the runtime included. Reading a page or following a file holds a 64 KB block and one entry at a time.