38

Can we create a custom log file for different purposes in laravel 5.2 like for order related log entries that should be in order.log and for payment related stuff the entry should get logged in payments.log

I want to find the best possible Laravel way.

Currently we can only change the log file frequency (like daily, single) or we can change the name of the log file other than default i.e laravel.log

kjones
  • 1,339
  • 1
  • 13
  • 28
Gaurav Bakshi
  • 430
  • 1
  • 6
  • 13
  • This is a comment posted on Laracast for the same question: https://laracasts.com/discuss/channels/general-discussion/advance-logging-with-laravel-and-monolog – AfikDeri Jun 14 '16 at 15:00
  • Is it possible to use `Log::useFiles()` and specifying the file name before logging the message? If so, Laravel's [5.2 API docs](https://laravel.com/api/5.2/Illuminate/Log/Writer.html#method_useFiles) talk about it. – Schoffelman Jul 16 '17 at 07:23

10 Answers10

55

There is a simple way:

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = ['orderId' => 10,
        'description' => 'Some description'];

//first parameter passed to Monolog\Logger sets the logging channel name
$orderLog = new Logger('order');
$orderLog->pushHandler(new StreamHandler(storage_path('logs/order.log')), Logger::INFO);
$orderLog->info('OrderLog', $log);

Output in logs/order.log:

[2017-04-30 00:00:00] order.INFO: OrderLog {"orderId":10, "description":"Some description"} []
kjones
  • 1,339
  • 1
  • 13
  • 28
Marcelo
  • 703
  • 7
  • 11
  • 3
    This is a very neat and easy way of adding extra logging, just a question will this keep adding entries to the single log file or will it also work on the daily pattern like laravel's logging, where if you set it to daily it will log each day in it's own file? – Louwki Jun 28 '17 at 13:09
  • 1
    @massreuy `Logger()` has string as class constructor parameter (at least in laravel 5.5.28). So for 5.5.28 the right way is `Logger('order')` (I've just tried on clean laravel 5.5.28 project). I don't know exactly if you amde a mistake, so I don't edit your answer but write a comment. I can't even edit because of edit might have at least 6 chars modified) – Tarasovych Mar 28 '18 at 14:10
37

Here you go... I've spent so much time to add custom functionality to Monolog which able to do THAT in a proper way. I tried sooooo many different ways, but all was a bit hacky. Finally I found a good way to get this functionality working....

As the application is big, I needed separate log files, and maintain the existing Laravel's Log interface as much as possible. I needed something like:

Log::write('audit', 'User logged in to the app.');
Log::info('event', 'User sent out 2 emails.');

The Solution:

App\Providers\AppServiceProvider.php (add to register function)

//Facade to Object binding
$this->app->bind('chanellog', 'App\Helpers\ChannelWriter');

config\app.php (add to aliases)

//Custom Alias Class
'ChannelLog' => App\Contracts\Facades\ChannelLog::class,

App\Contracts\Facades\ChannelLog.php

<?php

namespace App\Contracts\Facades;

use Illuminate\Support\Facades\Facade;

/**
 * @see \Illuminate\Log\Writer
 */
class ChannelLog extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'chanellog';
    }
}

App\Helpers\ChannelWriter.php

<?php

namespace App\Helpers;

use Monolog\Logger;

use App\Helpers\ChannelStreamHandler;

class ChannelWriter
{
    /**
     * The Log channels.
     *
     * @var array
     */
    protected $channels = [
        'event' => [ 
            'path' => 'logs/audit.log', 
            'level' => Logger::INFO 
        ],
        'audit' => [ 
            'path' => 'logs/audit.log', 
            'level' => Logger::INFO 
        ]
    ];

    /**
     * The Log levels.
     *
     * @var array
     */
    protected $levels = [
        'debug'     => Logger::DEBUG,
        'info'      => Logger::INFO,
        'notice'    => Logger::NOTICE,
        'warning'   => Logger::WARNING,
        'error'     => Logger::ERROR,
        'critical'  => Logger::CRITICAL,
        'alert'     => Logger::ALERT,
        'emergency' => Logger::EMERGENCY,
    ];

    public function __construct() {}

    /**
     * Write to log based on the given channel and log level set
     * 
     * @param type $channel
     * @param type $message
     * @param array $context
     * @throws InvalidArgumentException
     */
    public function writeLog($channel, $level, $message, array $context = [])
    {
        //check channel exist
        if( !in_array($channel, array_keys($this->channels)) ){
            throw new InvalidArgumentException('Invalid channel used.');
        }

        //lazy load logger
        if( !isset($this->channels[$channel]['_instance']) ){
            //create instance
            $this->channels[$channel]['_instance'] = new Logger($channel);
            //add custom handler
            $this->channels[$channel]['_instance']->pushHandler( 
                new ChannelStreamHandler( 
                    $channel, 
                    storage_path() .'/'. $this->channels[$channel]['path'], 
                    $this->channels[$channel]['level']
                )
            );
        }

        //write out record
        $this->channels[$channel]['_instance']->{$level}($message, $context);
    }

    public function write($channel, $message, array $context = []){
        //get method name for the associated level
        $level = array_flip( $this->levels )[$this->channels[$channel]['level']];
        //write to log
        $this->writeLog($channel, $level, $message, $context);
    }

    //alert('event','Message');
    function __call($func, $params){
        if(in_array($func, array_keys($this->levels))){
            return $this->writeLog($params[0], $func, $params[1]);
        }
    }

}

App\Helpers\ChannelStreamHandler.php

<?php

namespace App\Helpers;

use Monolog\Handler\StreamHandler;

/**
 * Use channels to log into separate files
 *
 * @author Peter Feher
 */
class ChannelStreamHandler extends StreamHandler
{
    /**
     * Channel name
     * 
     * @var String 
     */
    protected $channel;

    /**
     * @param String $channel Channel name to write
     * @see parent __construct for params
     */
    public function __construct($channel, $stream, $level = Logger::DEBUG, $bubble = true, $filePermission = null, $useLocking = false)
    {
        $this->channel = $channel;

        parent::__construct($stream, $level, $bubble);
    }

    /**
     * When to handle the log record. 
     * 
     * @param array $record
     * @return type
     */
    public function isHandling(array $record)
    {
        //Handle if Level high enough to be handled (default mechanism) 
        //AND CHANNELS MATCHING!
        if( isset($record['channel']) ){
            return ( 
                $record['level'] >= $this->level && 
                $record['channel'] == $this->channel 
            );
        } else {
            return ( 
                $record['level'] >= $this->level
            );
        }
    }

}

After this, you can do in any file:

use ChannelLog as Log;
...
function myFunction(){
    //Recommended (writes INFO to logs/event.log)
    Log::write('event', 'User sent out 3 voucher.')
    //Possible to use (writes ALERT to logs/audit.log)
    Log::alert('audit', 'User modified xyz entry.')
    //Or even: 
    Log::write('audit', 'User modified xyz entry.', ['user'=>1])
}
ShQ
  • 756
  • 7
  • 10
  • 2
    Additionally one can have daily log files by adding these lines to __construct method: $this->channels['event']['path'] = 'logs/audit-' . date('Y-m-d') . '.log'; $this->channels['audit']['path'] = 'logs/audit-' . date('Y-m-d') . '.log'; – ux.engineer Nov 23 '16 at 19:59
  • this code creates additional [] [] in log message, any idea? – codebob Feb 09 '17 at 07:51
  • @codebob see my answer below regarding the `[] []` issue. Its because its using the default `LineFormatter.php` instance with default formatter – wired00 Feb 28 '17 at 07:09
  • I have the same [] [] problem, how can I remove them? – Alan May 12 '17 at 20:49
  • Just a note. It does not remove older files specified in log_max_files. – Jeffz Aug 21 '17 at 21:45
  • Creating a channel runtime. if (!isset($this->channels[$channel])) { $this->channels[$channel] = [ 'path' => "logs/$channel.log", 'level' => Logger::INFO ]; } – f_i Jul 28 '18 at 07:51
33

This is supported in a much easier way now

  1. Create a channel

    goto: root/config/logging.php, under channels array add your custom channel i.e

      'payments' => [
          'driver' => 'single',
          'path' => storage_path('logs/payments.log'),
          'level' => 'info',
    ],
  1. In your route or controller write to this log
    Log::channel('payments')->info('A transaction has been made!');
  1. The payment logs can be found at /storage/logs/payments.log

NOTE: extendible to enhance furthur your requirements

Laravel version 5.6 Docs

Community
  • 1
  • 1
f_i
  • 3,084
  • 28
  • 31
11

You can try repurposing the log functions to write different types of logs to different files. This can be done by editing the bootstrap/app.php file:

$app->configureMonologUsing(function($monolog) {
    $bubble = false;
    $infoStreamHandler = new Monolog\Handler\StreamHandler( storage_path("/logs/orders.log"), Monolog\Logger::INFO, $bubble);
    $monolog->pushHandler($infoStreamHandler);

    $warningStreamHandler = new Monolog\Handler\StreamHandler( storage_path("/logs/logins.log"), Monolog\Logger::WARNING, $bubble);
    $monolog->pushHandler($warningStreamHandler);
});

Then in your code, you can do:

Log::info('Order was created', ['ORDER-123']);

Log::warning('User login', ['USER-1']);

You can use this method to edit all the available log functions:

  • DEBUG
  • INFO
  • NOTICE
  • WARNING
  • ERROR
  • CRITICAL
  • ALERT
  • EMERGENCY
Niraj Shah
  • 15,087
  • 3
  • 41
  • 60
  • Working great for separate logs by errorlevel, thank you. – Miloslav Milo Janoušek Oct 10 '16 at 13:06
  • instead of using this storage_path("/logs/orders.log"), can we use env() to set log path outside – Nathanphan Nov 27 '17 at 06:50
  • Yes, you can change the `storage_path("/logs/orders.log")` line to `env('LOG_PATH', storage_path("/logs/orders.log"))`, so the logs can fall back to storage_path if no env is defined. – Niraj Shah Nov 28 '17 at 09:12
  • I already upvoted this answer, but it would have been more helpful if it linked to docs: https://laravel.com/docs/5.2/errors#configuration – Ryan Aug 18 '19 at 12:37
7

To expand on ShQ's answer:

One issue I noticed is that the log will be appended with [] [], which are the empty array values for $context and $extra within LineFormatter.format();

ie, vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php

There are two ways around this, either provide a format which does not include extra or context to the constructor of LineFormatter, or provide the 4th argument $ignoreEmptyContextAndExtra = true.

All files within ShQ's answer remain the same but ChannelStreamHandler must change.

ChannelStreamHandler:

<?php

namespace App\Helpers;

use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

/**
 * Use channels to log into separate files
 *
 */
class ChannelStreamHandler extends StreamHandler
{
    /**
     * Channel name
     *
     * @var String
     */
    protected $channel;

    /**
     * @param String $channel Channel name to write
     * @param bool|int $stream
     * @param bool|int $level
     * @param bool $bubble
     * @param null $filePermission
     * @param bool $useLocking
     * @see parent __construct for params
     */
    public function __construct(
        $channel,
        $stream,
        $level = Logger::DEBUG,
        $bubble = true,
        $filePermission = null,
        $useLocking = false
    ) {
        $this->channel = $channel;

        $formatter = new LineFormatter(null, null, false, true);
        $this->setFormatter($formatter);

        parent::__construct($stream, $level, $bubble);
    }

    /**
     * When to handle the log record.
     *
     * @param array $record
     * @return bool
     */
    public function isHandling(array $record)
    {
        //Handle if Level high enough to be handled (default mechanism)
        //AND CHANNELS MATCHING!
        if (isset($record['channel'])) {
            return ($record['level'] >= $this->level && $record['channel'] == $this->channel);
        } else {
            return ($record['level'] >= $this->level);
        }
    }

}

The important change is to provide 4th param of true, which is $ignoreEmptyContextAndExtra. This param, tells LineFormatter to ignore either context of extra arrays if empty:

$formatter = new LineFormatter(null, null, false, true);
$this->setFormatter($formatter);

You must be sure to also ensure your running monolog 1.22 because it includes a bug fix regarding ignoreEmptyContextAndExtra.

I also added an override for info() to the ChannelWritter class:

public function info($channel, $message, array $context = [])
{
    $level = array_flip($this->levels)[$this->channels[$channel]['level']];
    $this->writeLog($channel, $level, $message, $context);
}

Additionally, I wasn't happy with the "lazy load logger" in ShQ's solution so modified to use the service provider/IoC

Replace ChannelWriter.writeLog():

public function writeLog(string $channel, string $level, string $message, array $context = [])
{
    if (!in_array($channel, array_keys($this->channels))) {
        throw new InvalidArgumentException('Invalid channel used.');
    }

    $logger = \App::make("{$channel}log");
    $channelHandler = new ChannelStreamHandler(
        $channel,
        storage_path() . '/' . $this->channels[$channel]['path'],
        $this->channels[$channel]['level']
    );
    $logger->pushHandler($channelHandler);
    $logger->{$level}($message);
}

and in your AppServiceProvider:

    $this->app->bind('eventlog', function () {
        return new Logger('event');
    });

    $this->app->bind('auditlog', function () {
        return new Logger('audit');
    });

I'll try bundle this together into a package.

wired00
  • 13,930
  • 7
  • 70
  • 73
4

Quickest Way to output log to different files

Log::useFiles('path/to/file.log');
Log::info('Info');
Deepak Pandey
  • 1,322
  • 12
  • 21
0

For me in Laravel 5.3, I am not sure if it was my install previously but I found the bootstrap/app.php did not work for me.

I needed to put this in app/Providers/AppServiceProvider.php.

n.b. This is where I had the setting of the log level from config before, so I end up with 3 log handlers.

public function register()
{
   $monolog = Log::getMonolog();
   foreach ($monolog->getHandlers() as $handler) {
      $handler->setLevel(Config::get('app.log_level'));
   }

   $bubble = false;
   $infoStreamHandler = new \Monolog\Handler\StreamHandler( storage_path("logs/info.log"), \Monolog\Logger::INFO, $bubble);
   $monolog->pushHandler($infoStreamHandler);

   $warningStreamHandler = new \Monolog\Handler\StreamHandler( storage_path("logs/warning.log"), \Monolog\Logger::WARNING, $bubble);
   $monolog->pushHandler($warningStreamHandler);

}
tristanbailey
  • 4,427
  • 1
  • 26
  • 30
  • Here is the answer that uses the approach described in the docs: https://stackoverflow.com/a/57544771/470749 – Ryan Aug 18 '19 at 12:58
0

Based on the ShQ answer, a shorter and simpler logger helper that allows you to log to a custom file on the fly. You can also add your custom handler and set the file path.

App\Helper

<?php
/**
 * Logger helper to log into different files
 *
 * @package    App\Helpers
 * @author     Romain Laneuville <romain.laneuville@hotmail.fr>
 */

namespace App\Helpers;

use Monolog\Logger;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\StreamHandler;

/**
 * Class LogToChannels
 *
 * @package App\Helpers
 */
class LogToChannels
{
    /**
     * The LogToChannels channels.
     *
     * @var Logger[]
     */
    protected $channels = [];

    /**
     * LogToChannels constructor.
     */
    public function __construct()
    {
    }

    /**
     * @param string $channel The channel to log the record in
     * @param int    $level   The error level
     * @param string $message The error message
     * @param array  $context Optional context arguments
     *
     * @return bool Whether the record has been processed
     */
    public function log(string $channel, int $level, string $message, array $context = []): bool
    {
        // Add the logger if it doesn't exist
        if (!isset($this->channels[$channel])) {
            $handler = new StreamHandler(
                storage_path() . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . $channel . '.log'
            );

            $this->addChannel($channel, $handler);
        }

        // LogToChannels the record
        return $this->channels[$channel]->{Logger::getLevelName($level)}($message, $context);
    }

    /**
     * Add a channel to log in
     *
     * @param string           $channelName The channel name
     * @param HandlerInterface $handler     The channel handler
     * @param string|null      $path        The path of the channel file, DEFAULT storage_path()/logs
     *
     * @throws \Exception When the channel already exists
     */
    public function addChannel(string $channelName, HandlerInterface $handler, string $path = null)
    {
        if (isset($this->channels[$channelName])) {
            throw new \Exception('This channel already exists');
        }

        $this->channels[$channelName] = new Logger($channelName);
        $this->channels[$channelName]->pushHandler(
            new $handler(
                $path === null ?
                    storage_path() . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . $channelName . '.log' :
                    $path . DIRECTORY_SEPARATOR . $channelName . '.log'
            )
        );
    }

    /**
     * Adds a log record at the DEBUG level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function debug(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::DEBUG, $message, $context);
    }

    /**
     * Adds a log record at the INFO level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function info(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::INFO, $message, $context);
    }

    /**
     * Adds a log record at the NOTICE level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function notice(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::NOTICE, $message, $context);
    }

    /**
     * Adds a log record at the WARNING level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function warn(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::WARNING, $message, $context);
    }

    /**
     * Adds a log record at the WARNING level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function warning(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::WARNING, $message, $context);
    }

    /**
     * Adds a log record at the ERROR level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function err(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::ERROR, $message, $context);
    }

    /**
     * Adds a log record at the ERROR level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function error(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::ERROR, $message, $context);
    }

    /**
     * Adds a log record at the CRITICAL level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function crit(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::CRITICAL, $message, $context);
    }

    /**
     * Adds a log record at the CRITICAL level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return Boolean Whether the record has been processed
     */
    public function critical(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::CRITICAL, $message, $context);
    }

    /**
     * Adds a log record at the ALERT level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function alert(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::ALERT, $message, $context);
    }

    /**
     * Adds a log record at the EMERGENCY level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function emerg(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::EMERGENCY, $message, $context);
    }

    /**
     * Adds a log record at the EMERGENCY level.
     *
     * @param  string $channel The channel name
     * @param  string $message The log message
     * @param  array  $context The log context
     *
     * @return bool Whether the record has been processed
     */
    public function emergency(string $channel, string $message, array $context = []): bool
    {
        return $this->log($channel, Logger::EMERGENCY, $message, $context);
    }
}

App\Providers\AppServiceProvider.php (add to register function)

//Facade to Object binding
$this->app->bind('LogToChannels', 'App\Helpers\LogToChannels');

config\app.php (add to aliases)

// Custom Alias Class
'Log' => App\Contracts\Facades\LogToChannels::class

Then anywhere in your app you can call

Log::info('logger_name', 'Log message');
Log::error('other_logger_name', 'Log message', $someContext);

You can even customize your logger output by calling

Log::addChannel('channel_name', $customHandler);

And it will be accessible when you will call its name anywhere in your app.

0
Solution:

step1: create a channel inside config/logging.php file

example :

'channels' => [
    'single' => [
    'driver' => 'single', 
    'path' => storage_path('logs/laravel.log'),
    'level' => 'debug',
],

'web' => [
      'driver' => 'single',
      'path' => storage_path('logs/web/web.log'),
   ],

]

Step2: Now set dynamic path from the controller  like this

config(['logging.channels.web.path' => storage_path('logs/web/'.time().'.log')]);

Step3 : now generate your log

  Log::channel('web')->info("your message goes here");

Enjoy :)
Kundan roy
  • 3,082
  • 3
  • 18
  • 22
0

I managed my own log function which can be put in helper.php file in app dir.

if ( ! function_exists( 'write_log' ) ) {
    /**
     * Write log to log file
     *
     * @param string|array|object $log
     */
    function write_log( $log ) {
        if ( env('APP_LOG_LEVEL', 'debug') == 'debug' ) {
            if ( is_array( $log ) || is_object( $log ) ) {
                file_put_contents(laravelInstallDir().'../debug.log', print_r( $log, true ), FILE_APPEND);
            } else {
                file_put_contents(laravelInstallDir().'../debug.log', $log, FILE_APPEND);
            }
        }
    }
}

Please adjust the path laravelInstallDir().'../debug.log' as need

Manchumahara
  • 139
  • 1
  • 9