Back to Logging guides

How to Get Started with Logging in Laravel

Eric Hu
Updated on November 23, 2023

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
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:

.env
. . .
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:

config/logging.php
. . .
"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.

config/logging.php
. . .
"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:

config/logging.php
. . .
    "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:

config/logging.php
<?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.

config/logging.php
. . .
"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
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 to EMERGENCY, 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 to INFO 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.");
Output
[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:

config/logging.php
. . .

"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.

Create a new Telegram 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.

Telegram bot API key

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.

Telegram channel name

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:

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
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
Output
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
Output
[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:

routes/web.php
. . .
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:

routes/web.php
. . .
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:

routes/web.php
. . .
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:

Output
[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:

config/logging.php
. . .
"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
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.

Logtail Sources Page

Choose PHP as your platform and click the Create Source button.

Connect Source

Once your Logtail source is created, copy the Source token to your clipboard.

Copy Source token

Afterward, run the command below in your project to install the Logtail library for Monolog:

 
composer require logtail/monolog-logtail
Output
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:

routes/web.php
<?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:

Live Tail

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!

Author's avatar
Article by
Eric Hu
Eric is a technical writer with a passion for writing and coding, mainly in Python and PHP. He loves transforming complex technical concepts into accessible content, solidifying understanding while sharing his own perspective. He wishes his content can be of assistance to as many people as possible.
Got an article suggestion? Let us know
Next article
How To Start Logging With Log4php
Learn how to start logging with Log4php and go from basics to best practices in no time.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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 us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build 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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github