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 toINFObut 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!