Logging
Log.info('post.created id=${post.id}');Log.debug('Cache miss for key $key');Log.warning('Payment retried');Log.error('Checkout failed', error, stackTrace);Every line is written to standard output, and to a file when one is configured:
[2026-09-02 09:48:22] production.ERROR: Checkout failed | Bad state: no gatewayThe format is timestamp, environment, level, message — so local.INFO and production.ERROR are distinguishable when logs from several environments end up in the same place.
Levels
| Method | Use for |
|---|---|
Log.debug |
Detail useful only while diagnosing something. |
Log.info |
Ordinary events worth a record. |
Log.warning |
Something recoverable that should not be normal. |
Log.error |
A failure. |
Each accepts an optional error and stack trace:
Log.error(Object message, [Object? error, StackTrace? stackTrace]);The error is appended after a |, and the stack trace on the following lines.
Configuration
The log path comes from your application config:
Map<String, dynamic> get app => { 'log_path': 'storage/logs/app.log',};The path is resolved against the application’s base path, and directories are created as needed. Remove the key and logs go to standard output only — which is what you usually want in a container, where the runtime collects stdout.
Log.environment is set from APP_ENV when the application is created.
Writing Somewhere Else
Log.sink is a plain StringSink, which makes logging testable and redirectable:
final buffer = StringBuffer();Log.sink = buffer;
Log.info('hello');
expect(buffer.toString(), contains('INFO: hello'));Observing Every Log Line
Log.listener is a single observer slot — not a list — notified with the
level, message, error, and stack trace after every line is written:
Log.listener = (level, message, error, stackTrace) { metrics.increment('log.$level');};Setting it replaces whatever was there before. A package that wants to add its own observer without silently unhooking another one already installed must chain onto the existing listener rather than overwrite it:
final previous = Log.listener;Log.listener = (level, message, error, stackTrace) { previous?.call(level, message, error, stackTrace); metrics.increment('log.$level');};A listener that throws does not stop the line from being written to the sink or file.
Logging and Error Handling
Unhandled exceptions are reported through Log.error automatically, with their stack trace. You do not need to log an exception you are about to throw — the handler does it.
HttpException and ValidationException are deliberately not logged. A 404 or a failed validation is an ordinary outcome, and recording them buries the failures that matter.
To send errors somewhere else as well, add a reporter rather than replacing the log:
.withExceptions((exceptions) { exceptions.report((error, stackTrace) => errorTracker.send(error, stackTrace));})See Error Handling.
Production Guidance
Write logs to stdout in containers and let the runtime collect them. Avoid passwords, bearer tokens, full request bodies, and other secrets; logs usually outlive the request and are visible to more systems and people than application data.
Use stable event-like messages such as post.created plus identifiers. They are
easier to search than prose that changes from one call site to another.