3

Im building project on Laravel 7.3 with multiple Jobs that run at the same time. I need to make each Job write logs to different daily rotated file. The name of the log file should be based on model, that Job is processing.

The issue is I cant find smart solution.

What I have tried:

1) creating multiple channels in config/logging.php.

That works as expected but at the moment there are about 50 different Jobs and amount keeps growing. Method is ugly and hardly maintained.

2) setting up Config(['logging.channels.CUSTOMCHANNEL.path' => storage_path('logs/platform/'.$this->platform->name.'.log')]);.

Messing with Config variable was bad idea because of many Jobs running one time. As a result messages from one job often were written in another Job log.

3) using Log::useDailyFiles()

Seems like this stops working since laravel 5.5 or 5.6. Just getting error Call to undefined method Monolog\Logger::useDailyFiles(). Any thoughts how to make with work in laravel 7?

4) using tap parameter for channel in config/logging.php.

Example in laravel docs No ideas how to pass model name into CustomizeFormatter to setup file name.

Im almost sure there is smart solution and Im just missing something. Any suggests? Thanks!

966p
  • 535
  • 2
  • 7
  • 17

2 Answers2

1

You could inherit the log manager to allow a dynamic configuration

<?php

namespace App\Log;

use Illuminate\Support\Str;
use Illuminate\Log\LogManager as BaseLogManager;

class LogManager extends BaseLogManager
{
    /**
     * Get the log connection configuration.
     *
     * @param  string  $name
     * @return array
     */
    protected function configurationFor($name)
    {
        if (!Str::contains($name, ':')) {
            return parent::configurationFor($name);
        }
        [$baseName, $model] = explode(':', $name, 2);
        $baseConfig = parent::configurationFor($baseName);
        $baseConfig['path'] = ...; //your logic
        return $baseConfig;
    }
}

Likewise about Laravel's log service provider except this one can be totally replaced

<?php

namespace App\Log;

use Illuminate\Support\ServiceProvider;

class LogServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function ($app) {
            return new LogManager($app);
        });
    }
}

EDIT: I've just seen that Laravel's log service provider is missing from config/app.php, this is because it's "hard-loaded" by the application. You still can replace it by inheriting the application itself

<?php

namespace App\Foundation;

use App\Log\LogServiceProvider;
use Illuminate\Events\EventServiceProvider;
use Illuminate\Routing\RoutingServiceProvider;
use Illuminate\Foundation\Application as BaseApplication;

class Application extends BaseApplication
{
    /**
     * Register all of the base service providers.
     *
     * @return void
     */
    protected function registerBaseServiceProviders()
    {
        $this->register(new EventServiceProvider($this));
        $this->register(new LogServiceProvider($this));
        $this->register(new RoutingServiceProvider($this));
    }
}

And finally in bootstrap/app.php, replace Illuminate\Foundation\Application with App\Foundation\Application

For example, if you try this

app('log')->channel('single:users')->debug('test');

Laravel will use the single channel's config and write to users.log if your resolution logic is

$baseConfig['path'] = $model + '.log';
Shizzen83
  • 3,325
  • 3
  • 12
  • 32
  • Thanks for your solution. It works, but while testing faced strange problem. Created new empty Controller and tested with 1) Log::info('test'); 2) app('log')->channel('myconfigname:testmodel')->info('test'); Both tests resulted in error Undefined offset: 1 in LogManager (explode string). Any thoughts? Isnt that strange that static Log::info() uses same LogManager? – 966p Apr 04 '20 at 16:04
  • 1
    Yes, my code sample works only if there is a `:` in channel's name, I edited it, it should work now :) – Shizzen83 Apr 04 '20 at 16:09
  • My pleasure, I encountered the same issue about databases and filesystems ;) – Shizzen83 Apr 04 '20 at 16:35
0

I got a solution that I've been using since Laravel 4 that works, although it doesn't follow 'Laravel' way of doing things.

class UserTrackLogger
{
    /**
     * @var $full_path string
     */
    protected $full_path;
    /**
     * @var $tenant string
     */
    protected $tenant;
    /**
     * @var $user User
     */
    protected $user;
    /**
     * @var $request Request
     */
    protected $request;

    public static function log(string $message, Request $request, User $user, array $data = []): void
    {
        /** @noinspection PhpVariableNamingConventionInspection */
        $userTrack = new static($request, $user);
        $userTrack->write($message, $data);
    }

    protected function __construct(Request $request, User $user)
    {
        $this->request = $request;
        $this->user = $user;
        $this->tenant = app()->make('tenant')->tenant__name;
        $path = storage_path() . "/logs/{$this->tenant}/users";

        $filename = $this->user->username_with_name;
        $this->full_path = Formatter::formatPath("{$path}/{$filename}.log");
        self::makeFolder($this->full_path);
    }

    protected function write(string $message, array $data = []): void
    {
        $formatter = $this->getFormat();
        $record = [
            'message'    => $message,
            'context'    => $data,
            'extra'      => [],
            'datetime'   => date(Utility::DATETIME_FORMAT_DEFAULT),
            'level_name' => 'TRACK',
            'channel'    => '',
        ];
        file_put_contents($this->full_path, $formatter->format($record), FILE_APPEND);
    }

    protected function getFormat(): FormatterInterface
    {
        $ip = $this->request->getClientIp();
        $method = strtoupper($this->request->method());
        $format = "[%datetime%][{$this->tenant}][{$this->user->username}][{$this->user->name}]: $ip $method %message% %context%\n";
        return new LineFormatter($format, null, true);
    }

    protected static function makeFolder(string $full_path): bool
    {
        $path = dirname($full_path);
        if ( !is_dir($path) ) {
            return mkdir($path, 0755, true);
        }

        return false;
    }
}

And when I want to log something, I do UserTrackLogger::log($request->fullUrl(), $request, $user, $data);

What I would suggest is creating a logger similar to this but extends RotatingFileHandler.

Raza
  • 3,147
  • 2
  • 31
  • 35