Laravel, a popular framework for building PHP projects, has a robust logging
system that allows you to send log messages to files, system logs, and various
other destinations. Logging in Laravel is channel-based, and each channel
defines a specific way of writing log messages. For example, the single
channel writes log files to a single log file, while the slack
channel sends
log messages to Slack.
Laravel logging is created on top of Monolog, which is a powerful logging library for PHP projects. We've already covered Monolog in-depth in this article, so we'll mostly focus on the Laravel-specific bits here.
Prerequisites
You need to have the latest version of PHP and Composer installed on your computer. You should also create a new Laravel project so that you can test the code snippets in this article. You can consult Laravel's official documentation for details on creating a new project on your machine.
Exploring the logging config file
Laravel projects include a config
directory containing several configuration
files used to customize the project's different aspects, such as the database
setup, caching, session management, and more. In addition, the configuration
options related to logging are also present in this directory in the
logging.php
file. Go ahead and open this file in your editor to examine its
contents:
code config/logging.php
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
"default" => env("LOG_CHANNEL", "stack"),
"deprecations" => [
"channel" => env("LOG_DEPRECATIONS_CHANNEL", "null"),
"trace" => false,
],
"channels" => [
"stack" => [
"driver" => "stack",
"channels" => ["single", "daily"],
"ignore_exceptions" => false,
],
"single" => [
"driver" => "single",
"path" => storage_path("logs/laravel.log"),
"level" => env("LOG_LEVEL", "debug"),
],
"daily" => [
"driver" => "daily",
"path" => storage_path("logs/laravel.log"),
"level" => env("LOG_LEVEL", "debug"),
"days" => 14,
],
. . .
],
];
Notice how three Monolog handlers are imported at the top of the file. This confirms what we explained earlier about Laravel logging being based on Monolog. Below the imports, you can see three distinct options in the array returned by the config file.
The default
option specifies the default channel for writing all log messages
produced by Laravel. The value provided to this option should match a channel
defined by the channels
array. In the above snippet, the value of the
LOG_CHANNEL
environmental variable (defined in your .env
file) will be used
by default if it exists. Otherwise, it will fall back to the stack
channel. If
you check the .env
file at the project root, you'll notice that the
LOG_CHANNEL
variable is also set to stack
by default:
. . .
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
. . .
The deprecations
option allows you to control where deprecation warnings from
PHP, Laravel, and other third-party libraries are placed. This uses the
LOG_DEPRECATIONS_CHANNEL
environmental variable (set to null
by default)
which means that deprecation warnings will be ignored. Recording such notices is
useful when preparing for major version upgrades so you should set this variable
to a valid channel. You may also define a special deprecations
channel for
this purpose and it will always be used to log deprecations regardless of the
value of the top-level deprecations
key.
"channels" => [
"deprecations" => [
"driver" => "single",
"path" => storage_path("logs/deprecation-warnings.log"),
],
]
Lastly, the channels
option is the most important part of the file as it is
where all the logging channels are defined and configured. An example is shown
in the previous snippet above. We defined a channel called deprecations
which
uses the single
channel driver.
This driver determines how log messages sent to the corresponding channel are
recorded. For instance, the single
driver writes all log messages to a local
file as specified by the path
option, which is storage/logs/laravel.log
in
this case. If the specified file does not exist, Laravel will automatically
create it.
"channels" => [
"single" => [
"driver" => "single",
"path" => storage_path("logs/laravel.log"),
"level" => env("LOG_LEVEL", "debug"),
],
]
Another option that is configurable on channels is level
(highlighted above),
and it specifies the channel's minimum log level. The
log level is a way for us to classify different messages based on their urgency
and the level
option determines the minimum log level the message must
have to be logged by the channel. For the single
channel, this option is
determined by the value of the LOG_LEVEL
environmental variable (set to
debug
by default) and falling back to debug
if the LOG_LEVEL
variable is
not present in the environment.
Understanding channel drivers
In this section, we will take a closer look at channel drivers in Laravel and
how to utilize them. We've already seen the single
driver, which logs messages
to a single file on the local disk. The daily
driver is similar to single
as
it also writes to a file, but it rotates daily and automatically deletes old
logs. It uses Monolog's RotatingFileHandler
under the hood. Here's how it is
typically configured:
. . .
"daily" => [
"driver" => "daily",
"path" => storage_path("logs/laravel.log"),
"level" => env("LOG_LEVEL", "debug"),
"days" => 14,
],
. . .
This daily
driver will create one log file per day in the format of
laravel-YYYY-MM-DD.log
. The days
option specifies how long to retain each
log file, meaning that files older than 14 days will be deleted. It's generally
better to prefer the daily
driver instead of single
so that individual log
files do not grow too large and become unwieldy. Another solution is to continue
using the single
driver and implement log file rotation using a standard
utility such as
logrotate.
. . .
"syslog" => [
"driver" => "syslog",
"level" => env("LOG_LEVEL", "debug"),
],
"errorlog" => [
"driver" => "errorlog",
"level" => env("LOG_LEVEL", "debug"),
],
. . .
The syslog
and errorlog
drivers work very similarly. They both write to the
system log, except that the syslog
driver invokes PHP's
syslog() function, and the
errorlog
driver invokes the
error_log() function.
You can learn more about how these functions work in our article on logging in
PHP.
The stack
driver allows you to log a message into multiple channels at once by
using its channels
option. For example:
. . .
"channels" => [
"stack" => [
"driver" => "stack",
"channels" => ["one", "two", "three"],
"ignore_exceptions" => false,
],
"one" => [
"driver" => "single",
"path" => storage_path("logs/laravel_1.log"),
"level" => "debug",
],
"two" => [
"driver" => "single",
"path" => storage_path("logs/laravel_2.log"),
"level" => "warning",
],
"three" => [
"driver" => "single",
"path" => storage_path("logs/laravel_3.log"),
"level" => "error",
],
],
. . .
The highlighted channels
option above binds channels one
, two
, and three
together so you can log to all three channels simultaneously by invoking the
stack
channel.
You to take further control of how logging should work in your application by
using the following one these two channel drivers: the monolog
driver for
invoking any Monolog handlers directly, and the custom
driver for creating
custom loggers using factories.
Since Laravel logging is based on the Monolog library, we can access any of its
handlers
using the monolog
driver as shown below:
<?php
use Monolog\Handler\StreamHandler
. . .
"monolog_handler" => [
"driver" => "monolog",
"handler" => Monolog\Handler\FilterHandler::class,
"with" => [
"handler" => new StreamHandler(storage_path("logs/info.log")),
"minLevelOrList" => [Monolog\Logger::INFO],
],
],
In the highlighted section, we have the handler
option which specifies the
Monolog handler we are going to use which is FilterHandler
in this example.
This handler allows us to assign specific log levels to the channel, instead of
a minimum level such that only entries that match the specified levels gets
logged to the channel.
Next, the with
option is where we pass some information that are required by
the chosen Monolog handler. In this example, a second handler
option is used
to specify that StreamHandler
should be used log the messages sent to the
monolog_handler
channel to a local file.
The minLevelOrList
option defines one or more log levels that should be
recorded to the destination defined within the channel. In this case, only
INFO
level entries will be logged to the logs/info.log
file. You can read
more about Monolog handlers in this
article.
When you need an even higher level of customization for your project, you may
create a completely customized channel using the custom
driver.
. . .
"example-custom-channel" => [
"driver" => "custom",
"via" => App\Logging\CreateCustomLogger::class,
],
Then we can create the CreateCustomLogger.php
file where we can create a fully
customized logging channel using Monolog:
code app/Logging/CreateCustomLogger.php
<?php
namespace App\Logging
use Monolog\Logger;
class CreateCustomLogger
{
/**
* Create a custom Monolog instance.
*
* @param array $config
* @return \Monolog\Logger
*/
public function __invoke(array $config)
{
return new Logger(
. . .
);
}
}
You do need more advanced knowledge related to Monolog to achieve this, so if you are interested, read our tutorial on Monolog to find out more.
Understanding log levels in Laravel
Laravel offers the eight severity levels defined in the
RFC 5424 specification. The list below is
ordered according to their severity levels, with emergency
being the most
severe and debug
being the least severe:
EMERGENCY
: means the application is unusable, and the issue needs to be addressed immediately.ALERT
: similar toEMERGENCY
, but less severe.CRITICAL
: critical errors in a core aspect of your application.ERROR
: error conditions in your application.WARNING
: something unusual happened that may need to be addressed later.NOTICE
: similar toINFO
but more significant.INFO
: informational messages that describe the normal operation of the program.DEBUG
: used to record some debugging messages.
There is no standard on what conditions should be considered as ALERT
,
CRITICAL
, WARNING
, etc. It all depends on the purpose of the program you are
writing. For example, imagine an e-commerce application where users can purchase
items. You can log successful orders using the INFO
level, and failed orders
due to some external API being faulty using the ERROR
level.
Here's how to use the above log levels in a Laravel application:
use Illuminate\Support\Facades\Log;
Log::debug("This is a debug message.");
Log::info("This is an info level message.");
Log::notice("This is a notice level message.");
Log::warning("This is a warning level message.");
Log::error("This is an error level message.");
Log::critical("This is a critical level message.");
Log::alert("This is an alert level message.");
Log::emergency("This is an emergency level message.");
[2022-07-20 16:11:34] local.INFO: This is an info level message.
[2022-07-20 16:11:34] local.NOTICE: This is a notice level message.
[2022-07-20 16:11:34] local.WARNING: This is a warning level message.
[2022-07-20 16:11:34] local.ERROR: This is an error level message.
[2022-07-20 16:11:34] local.CRITICAL: This is a critical level message.
[2022-07-20 16:11:34] local.ALERT: This is an alert level message.
[2022-07-20 16:11:34] local.EMERGENCY: This is an emergency level message.
Notice how the log level is included just before the log message above. This helps you make sense of each entry at a glance by annotating how severe the event is so you know where to concentrate your efforts. If you send your logs to a log management service, you can set up log monitoring based on these levels, such that you are promptly alerted of notable events in your application.
Creating a log stack
In this section, we'll demonstrate how to use the stack
channel driver to
create a basic logging system for a Laravel application. Head back to the
logging.php
file and replace the default channel configurations with the
following lines:
. . .
"channels" => [
"stack" => [
"driver" => "stack",
"channels" => [
"daily",
"important",
"urgent",
],
"ignore_exceptions" => false,
],
"daily" => [
"driver" => "daily",
"path" => storage_path("logs/daily.log"),
"level" => "info",
],
"important" => [
"driver" => "daily",
"level" => "warning",
"path" => storage_path("logs/important.log"),
],
"urgent" => [
"driver" => "daily",
"path" => storage_path("logs/urgent.log"),
"level" => "critical",
],
],
We created three unique channels, each with different minimum log levels, and we
combined them in the stack
channel using the stack
driver. Recall that the
level
option defines the minimum level the message must have to be logged
by the channel. So in this example, if we have a DEBUG
level message, it won't
be logged by any channel as DEBUG
is less severe than INFO
, WARNING
, and
CRITICAL
. However, if we have an EMERGENCY
level message, it would be logged
by all three channels.
In this logging system, the daily.log
file will contain all log records except
debugging messages. The important.log
file will include all records you should
pay attention to (WARNINGS
or more severe), and the urgent.log
file will
contain all potentially showstopping issues that should be resolved immediately.
The problem is with this system is that it could lead to a waste of storage
resources due to the repetition of logs in each file. It will be better if we
can define a set of acceptable log levels for each handler instead of just the
minimum level. Laravel doesn't offer a native way to setup such a system, but it
can be achieved by using the FilterHandler
discussed earlier:
. . .
"channels" => [
"stack" => [
"driver" => "stack",
"channels" => ["debug", "info", "warning", "critical", "emergency"],
"ignore_exceptions" => false,
],
"debug" => [
'driver' => 'monolog',
'handler' => Monolog\Handler\FilterHandler::class,
'with' => [
'handler' => new Monolog\Handler\RotatingFileHandler(storage_path('logs/debug.log'), 15),
'minLevelOrList' => [Monolog\Logger::DEBUG],
],
],
"info" => [
'driver' => 'monolog',
'handler' => Monolog\Handler\FilterHandler::class,
'with' => [
'handler' => new Monolog\Handler\RotatingFileHandler(storage_path('logs/info.log'), 15),
'minLevelOrList' => [Monolog\Logger::INFO],
],
],
"warning" => [
'driver' => 'monolog',
'handler' => Monolog\Handler\FilterHandler::class,
'with' => [
'handler' => new Monolog\Handler\RotatingFileHandler(storage_path('logs/warning.log'), 15),
'minLevelOrList' => [Monolog\Logger::NOTICE, Monolog\Logger::WARNING],
],
],
"critical" => [
'driver' => 'monolog',
'handler' => Monolog\Handler\FilterHandler::class,
'with' => [
'handler' => new Monolog\Handler\RotatingFileHandler(storage_path('logs/critical.log'), 15),
'minLevelOrList' => [Monolog\Logger::ERROR, Monolog\Logger::CRITICAL],
],
],
"emergency" => [
'driver' => 'monolog',
'handler' => Monolog\Handler\FilterHandler::class,
'with' => [
'handler' => new Monolog\Handler\TelegramBotHandler($apiKey = "<telegram_bot_api>", $channel = "@<channel_name>"),
'minLevelOrList' => [Monolog\Logger::ALERT, Monolog\Logger::EMERGENCY],
],
],
],
In this example, the debug
channel will only log the DEBUG
messages, the
warning
channel will log the NOTICE
and WARNING
messages, and the
emergency
will push the ALERT
and EMERGENCY
messages a Telegram channel
through the TelegramBotHandler
.
To set up the TelegramBotHandler
, you need to create a Telegram bot through
BotFather first. Open the link in the Telegram
client and start the conversation by sending the message /start
. You should
receive a message with the next steps to creating your bot.
Follow the instructions to create a new username and bot name, and you should receive an API key after you are done. You can use this API key to access your bot.
Next, create a public Telegram channel, and add the bot to the channel. Finally,
use the API key and the channel name (with prefix @
) to construct a new
instance of TelegramBotHandler
.
This time, if you push an EMERGENCY
level message, it should only go through
the emergency
channel.
use Illuminate\Support\Facades\Log;
. . .
Log::alert("This is an alert level message.");
Log::emergency("This is an emergency level message.");
You should receive messages like this on Telegram:
Sending a log message
We've mainly discussed Laravel's logging configuration so far, so it's time for
us to put what we've learned to practice in the application code. Head to the
routes
directory and open the web.php
file. We will create a new route and
log a message whenever a user access it. Note that we are using the stack
log
channel setup from the previous section.
code routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Log;
Route::get("/", function () {
return view("welcome");
});
Route::get("/user", function () {
Log::info("The route /user is being accessed.");
return "This is the /user page.";
});
Laravel's Log
facade is being used to
write log messages, and the Log::info()
method is used for logging INFO
level messages. To see this in action, launch the development server by running
the following command in the terminal:
php artisan serve
Starting Laravel development server: http://127.0.0.1:8000
Open your browser and go to http://127.0.0.1:8000/user
; you should see the
string returned by the router. Head back to the terminal and inspect the
contents of the storage/logs
directory. You should observe that an
info-<date>.log
file is present. Examine the contents of this file as follows:
cat storage/logs/daily-<date>.log
[2022-07-01 15:46:12] local.INFO: The route /user is being accessed.
Since this log message has the INFO
log level, it will only be logged by the
info
channel, so no other files will contain this entry due to our use of the
FilterHandler
in the configuration file.
You can add another route below to simulate an error condition in the application:
. . .
Route::get("/user/{username}", function ($username) {
$users = ["user1", "user2", "user3"];
if (in_array($username, $users)) {
return "User found!";
} else {
Log::error("User does not exist.");
return "User does not exist.";
}
});
Open your browser and go to http://127.0.0.1:8000/user/test
. Since the test
user is not found in the $users
array, an error log will be recorded to the
critical
channel.
Logging to a specific channel
It's also possible to log to a specific channel other than the default by using
the channel()
method shown below:
. . .
Route::get("/user", function () {
Log::channel("info")->info("The route /user is being accessed.");
return "This is the /user page.";
});
This highlighted message will be logged directly to the info
channel without
going through the stack
channel. One thing to note when using a specific
channel like this is that the message must correspond to the configured levels
on the channel. If you try to log an INFO
message to the warning channel
, it
will be ignored since only NOTICE
and WARNING
logs are accepted on the
channel.
Adding contextual data to log messages
In our examples so far, we are only logging simple messages, but it's often necessary to include more information in the log entry to provide more context on the event that caused it to be logged. This can be accomplished by adding a second parameter to the log level method like this:
. . .
Route::get("/user/{username}", function ($username) {
Log::info("The route /user is being accessed.", ["username" => $username]);
return "The route /user is being accessed.";
});
Open your browser and go to http://127.0.0.1:8000/user/test
. Return to the
terminal and check the latest record in the info-<date>.log
file using the
command below:
tail -n 1 storage/logs/info-<date>.log
You should observe a JSON formatted object at the end of the log record like this:
[2022-07-01 19:46:10] local.INFO: The route /user is being accessed. {"username":"test"}
If you need more control over how logs are formatted in Laravel, you can
customize it through Monolog's formatters. Head back to the config/logging.php
and add a tap
option for the stack
channel:
. . .
"info" => [
"driver" => "single",
"tap" => [App\Logging\CustomizeFormatter::class],
"path" => storage_path("logs/daily.log"),
"level" => "info",
],
. . .
Next, we need to create the CustomizeFormatter.php
file:
code app/Logging/CustomizeFormatter.php
<?php
namespace App\Logging;
use Monolog\Formatter\LineFormatter;
class CustomizeFormatter
{
/**
* Customize the given logger instance.
*
* @param \Illuminate\Log\Logger $logger
* @return void
*/
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter(new LineFormatter(
'%level_name% | [%datetime%] | %message% | %context%'
));
}
}
}
In the above snippet, Monolog's LineFormatter
is used to change the format of
the log message such that the log level comes first, and each portion of the log
entry is separated by a pipe (|
) character. When you visit the /user
route
and inspect the info-<date>.log
file once again, you will observe that the
format for each entry is now as follows:
INFO | [2022-08-15T10:49:41.056687+00:00] | The route /user is being accessed. | {"username":"test"}
If you'd like to learn more about formatting your log entries in Laravel, check out our getting started tutorial on Monolog.
Centralizing and storing your logs
So far, we've mostly considered logging into files, and we also demonstrated a basic way to draw attention to critical events by sending them to a Telegram channel. You can also aggregate all your logs in one place for processing, monitoring, and alerting such that you can view and correlate your logs easily without having to log into each server individually.
Logtail is a log management service that allows you to centralize all your logs in a matter of minutes, and you only need a free account to get set up. We will quickly demonstrate how to send your Laravel logs to Logtail in this section.
Once you are signed into Logtail, head to the Sources page and click the Connect sources button.
Choose PHP as your platform and click the Create Source button.
Once your Logtail source is created, copy the Source token to your clipboard.
Afterward, run the command below in your project to install the Logtail library for Monolog:
composer require logtail/monolog-logtail
Using version ^0.1.3 for logtail/monolog-logtail
./composer.json has been updated
Running composer update logtail/monolog-logtail
Loading composer repositories with package information
. . .
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
No publishable resources for tag [laravel-assets].
Publishing complete.
Head back to the web.php
file, and configure the Logtail package as shown
below (ensure to replace the <your_source_token>
) placeholder:
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Log;
use Monolog\Logger;
use Logtail\Monolog\LogtailHandler;
. . .
$logtail_logger = new Logger("logtail-source");
$logtail_logger->pushHandler(
new LogtailHandler("<your_source_key>")
);
Route::get("/user/{username}", function ($username) {
$logtail_logger->info("The route /user is being accessed.", ["username" => $username]);
return "The route /user is being accessed.";
});
From this point onwards, you can use the $logtail_logger
to send all your logs
to the service. Each logged entry will appear instantly on the live tail page as
shown below:
Logtail also enables you to send specific messages to email, Slack, and other platforms. You can find all of them in the Integrations tab.
Conclusion and next steps
This article discussed several concepts related to logging in Laravel applications, such as log channels, log levels, contextual logging, and formatting log entries. As a result, you should now be well equipped to track down errors and improve your development experience as a whole especially if you're monitoring your logs through Logtail.
Note that we've only scratched the surface of logging in Laravel in this article. To unlock its full potential, you must dig deeper into the underlying Monolog library, which powers its entire logging infrastructure. For further resources on PHP logging libraries, read our new article.
Thanks for reading, and happy logging!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for usBuild on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github