.../articles/
Customizing Log Output in Laravel

Customizing Log Output in Laravel

2021.09.02

Environment

  • Laravel 6

Creating the Logging Classes

First, let's create Formatter classes in app/Logging/ that define the content of the action logs. The Action and Query logs differ in their output format. Also, depending on the environment, calling Auth::id() to get the user ID can trigger a DB access, so in QueryLogFormatter we control this using the static variable $disableUserId. If anyone knows a better way to handle this, I'd love to hear about it.


// app/Logging/ActionLogFormatter.php
<?php 
namespace App\Logging;

use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\UidProcessor;

class ActionLogFormatter
{
    private $dateFormat = 'Y-m-d H:i:s.v';
    public function __invoke($logger)
    {
        // Specify the log format and date format
        $format = '[%datetime%] action %channel%.%level_name% %extra.uid% %extra.userid%" %message% %context% ' . PHP_EOL;
        $lineFormatter = new LineFormatter($format, $this->dateFormat, true, true);

        // Processing to attach an ID for tying the output log to the request
        $uidProcessor = app()->make(UidProcessor::class);

        // Specify namespaces to exclude from the log output
        $introProcessor = new IntrospectionProcessor(Logger::DEBUG, [
            'Monolog\\',
            'Illuminate\\',
            'App\\Providers\\',
            'App\\Logging\\',
        ]);

        // Set up the formatter and content
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter($lineFormatter);
            $handler->pushProcessor($introProcessor);
            $handler->pushProcessor($uidProcessor);
            $handler->pushProcessor(function (array $record) {
                $record['extra']['userid'] = Auth::id() ?? '';
                return $record;
            });
        }
    }
}
// app/Logging/QueryLogFormatter.php
<?php
namespace App\Logging;
use Illuminate\Support\Facades\Auth;
use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Monolog\Processor\IntrospectionProcessor;
use Monolog\Processor\UidProcessor;

class QueryLogFormatter
{
    /**
     * NOTE
     *  Flag used to avoid an infinite loop in query log output, since calling
     *  Auth::id() to get the user ID triggers a DB access
     * @var bool
     */
    private static bool $disableUserId = false;

    private $dateFormat = 'Y-m-d H:i:s.v';
    public function __invoke($logger)
    {
        $format = '[%datetime%] query %channel%.%level_name% %extra.uid% %extra.userid% query %extra.file%(%extra.line%) %message% %context% ' . PHP_EOL;
        if (self::$disableUserId) {
            $format = '[%datetime%] query %channel%.%level_name% %extra.uid% %extra.file%(%extra.line%) %message% %context% ' . PHP_EOL;
        }

        // Specify the log format and date format
        $lineFormatter = new LineFormatter($format, $this->dateFormat, true, true);

        $uidProcessor = app()->make(UidProcessor::class);
        $introProcessor = new IntrospectionProcessor(Logger::DEBUG, [
            'Monolog\\',
            'Illuminate\\',
            'App\\Providers\\',
            'App\\Logging\\',
        ]);

        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter($lineFormatter);
            $handler->pushProcessor($introProcessor);
            $handler->pushProcessor($uidProcessor);

            $handler->pushProcessor(function (array $record) {
                if (self::$disableUserId) {
                    return $record;
                }
                try {
                    self::$disableUserId = true;
                    $record['extra']['userid'] = Auth::id() ?? '';
                } finally {
                    self::$disableUserId = false;
                }
                return $record;
            });
        }
    }
}

Configuring logging.php

Next, in Laravel, logging settings are described in config/logging.php. This file is created at install time, so we'll edit it. We add channels for outputting the action and query logs we created. By specifying the Formatter class we just created in tap, we can switch the Formatter used for the specified channel when logging.

// config/logging.php
<?php
return [
    'channels' => [

        ...

        'action' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'info',
            'days' => 7,
            'permission' => 0777,
            'tap' => [App\Logging\ActionLogFormatter::class],
        ],

        'query' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'info',
            'days' => 7,
            'permission' => 0777,
            'tap' => [App\Logging\QueryLogFormatter::class],
        ],
    ],
];

Outputting Logs

Now let's implement the code that actually outputs the logs. Naturally, the action log and the query log fire at different times, so the code goes in different places.

Action Log

Since we want the action log to be output when Laravel receives a request, we create a Middleware to output the log there. This class is also where we mask any content — such as passwords or personal information — that shouldn't be left in the log as plain text.

// Http/Middleware/ActionLogMiddleware.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class ActionLogMiddleware
{
    public function handle($request, Closure $next)
    {
        $logContext = [
            'url' => $request->path(),
            'request' => $this->maskRequest($request->all()),
        ];
        Log::channel('action')->info($request->method(), $logContext);
        return $next($request);
    }

    public function maskRequest($params)
    {
            # Mask any values sent under the keys "password" or "password_confirmation".
        # Add more entries here if you need to mask other fields as well
        array_walk_recursive($params, function (&$val, $key) {
            if (($key === 'password')||($key === 'password_confirmation')) {
                $val = '********';
            }
        });
        return $params;
    }
}

Query Log

The query log outputs a log entry whenever SQL is executed. To achieve this in Laravel, we create a ServiceProvider dedicated to query log output and register it with the application.

// Providers/QueryLogServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Database\Events\TransactionBeginning;
use Illuminate\Database\Events\TransactionCommitted;
use Illuminate\Database\Events\TransactionRolledBack;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

class QueryLogServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        DB::listen(function ($query) {
            $sql = $query->sql;
            for ($i = 0; $i < count($query->bindings); $i++) {
                if (! is_object($query->bindings[$i])) {
                    $sql = preg_replace("/\?/", $query->bindings[$i], $sql, 1);
                } else {
                    $sql = preg_replace("/\?/", date_format($query->bindings[$i], 'Y-m-d'), $sql, 1);
                }
            }
            $this->writeLog($sql);
        });

        Event::listen(TransactionBeginning::class, function (TransactionBeginning $event): void {
            $this->writeLog('begin transaction');
        });

        Event::listen(TransactionCommitted::class, function (TransactionCommitted $event): void {
            $this->writeLog('commit transaction');
        });

        Event::listen(TransactionRolledBack::class, function (TransactionRolledBack $event): void {
            $this->writeLog('rollback transaction');
        });
    }

    private function writeLog($msg)
    {
        Log::channel('query')->info($msg);
    }
}
// config/app.php
<?php
return [

    ...

    'providers' => [

        ...

        \App\Providers\QueryLogServiceProvider::class

        ...
    ]
    ...

]

Articles I referenced

Outputting the executed SQL query log in Laravel Attaching a per-request ID to Laravel logs

written by

.../article/

Articles

All articles

From Firebase to Vercel, Contentful to microCMS — a migration log written with Claude Code

From Firebase to Vercel, Contentful to microCMS — a migration log written with Claude Code

We moved our corporate site's hosting and CMS, and made it bilingual along the way. The constraints we only found by running against real data were more useful than the migration itself, so this post focuses on where we got stuck.

Can't Read POST Data with Firebase Functions × Remix?

Can't Read POST Data with Firebase Functions × Remix?

How to read POST data from a Remix action when running on Firebase Functions.

Generative AI for Executives and Leaders: An Approach to Self-Driven DX

Generative AI for Executives and Leaders: An Approach to Self-Driven DX

Building a structure where executives and leaders themselves can identify issues and evaluate solutions using generative AI. We introduce how combining this with our hands-on support dramatically improves both the quality and speed of digital transformation.

Deploying a Monorepo Next.js App (App Router) to AWS Amplify

Deploying a Monorepo Next.js App (App Router) to AWS Amplify

Notes on the obstacles we hit while deploying a Next.js app managed in a monorepo to AWS Amplify.

Keeping Production Running Smoothly with Remote Work and Online Meetings [Documentation]

Keeping Production Running Smoothly with Remote Work and Online Meetings [Documentation]

Many production companies have adopted remote work as a result of the pandemic, and we are one of them.

Designing an E-Commerce Site That Sells: How to Find Great Reference Examples

Designing an E-Commerce Site That Sells: How to Find Great Reference Examples

There is no single formula for e-commerce design that sells. Driving revenue requires a solid concept, and getting to that concept requires thorough research.

Productivity Tools We Recommend as a Production Company, Including Services That Work Well Solo

Productivity Tools We Recommend as a Production Company, Including Services That Work Well Solo

With remote work becoming the norm during the COVID-19 pandemic, our team now works from home most days of the week.

We Released Thought Recorder, a Figma Plugin for Keeping a Commit History of Your Designs

We Released Thought Recorder, a Figma Plugin for Keeping a Commit History of Your Designs

We hope this helps web designers who work in Figma. Read on for how to use it.

How to Build an E-Commerce Site, and Which Platforms We Recommend

How to Build an E-Commerce Site, and Which Platforms We Recommend

Shopping online for fashion, appliances, and even groceries is now routine. With the pandemic accelerating the shift, we receive a steady stream of questions about which platform to use and how much it costs.

Generating FastAPI Schema Classes from OpenAPI

Generating FastAPI Schema Classes from OpenAPI

We chose FastAPI, a relatively modern framework, for a Python API project. FastAPI can generate an OpenAPI definition from your backend code, but here we do the opposite: generating FastAPI schema classes from an OpenAPI definition prepared in advance.

View all articles

Contact us