Creating a Custom Logger for Craft CMS
Tags: craftcms, devops, logs
Published: 2020-03-04 05:43:23

---
title: Creating a Custom Logger for Craft CMS
published: true
date: 2019-07-21 17:52:00 UTC
tags: craftcms,devops,logs
canonical_url: https://nystudio107.com/blog/creating-a-custom-logger-for-craft-cms
---

# Creating a Custom Logger for Craft CMS

### The built-in Craft CMS log­ging sys­tem is suf­fi­cient for most needs, but what if you need cus­tom log­ging, or want to send your logs to a third-par­ty service?

Andrew Welch / [nystudio107](https://nystudio107.com)

![Writing a custom logger craft cms](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/writing-a-custom-logger-craft-cms.jpg)

The Craft CMS log­ging sys­tem (based on the [Yii Log­ging](https://www.yiiframework.com/doc/guide/2.0/en/runtime-logging) sys­tem) is usu­al­ly suf­fi­cient for most needs. Indeed, many peo­ple don’t even think about it until some­thing goes wrong.

But what if you need to change the built-in log­ging behav­ior? Or you need to imple­ment some kind of cus­tom log­ging behav­ior? Or you want to send the Craft CMS logs to a SaaS that han­dles log aggre­ga­tion such as [Paper­Trail](https://papertrailapp.com/) or [Sen­try](https://sentry.io)?

We’ll learn how to han­dle all three of the above sce­nar­ios in this arti­cle. We won’t cov­er how to read stan­dard Craft CMS log files; for that check out the [Zen and the Art of Craft CMS Log File Read­ing](https://nystudio107.com/blog/zen-and-the-art-of-craft-cms-log-file-reading) article.

<aside>
                                Cus­tom log­ging in Craft <span>CMS</span> is pret­ty easy to do, thanks to Yii
                            </aside>

To cre­ate a cus­tom log­ger we’ll need to use a bit of con­fig or code, depend­ing on what we’re try­ing to do. Thank­ful­ly Yii makes this pret­ty pain­less to do, so let’s get going!

The web app that is Craft CMS relies pret­ty heav­i­ly on [Yii Con­fig­u­ra­tions](https://www.yiiframework.com/doc/guide/2.0/en/concept-configurations) as a way to dynam­i­cal­ly con­fig­ure itself. It’s using [Depen­den­cy Injec­tion Con­tain­ers](https://www.yiiframework.com/doc/guide/2.0/en/concept-di-container) under the hood to accom­plish it.

While you don’t have to ful­ly under­stand either to fol­low along in this arti­cle, it will help to get a com­plete sense of what’s going on.

## Cus­tomiz­ing the exist­ing Logger

The first thing you might con­sid­er doing is cus­tomiz­ing the exist­ing file-based log­ger that Craft CMS uses by default.

![Logging lumberjack 01](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/logging-lumberjack-01.jpg)

Here’s an exam­ple of what prop­er­ties you can change with just a cus­tom configuration:

- <kbd>includeUserIp</kbd> — _default:_ <kbd>false</kbd> — Whether the user IP should be includ­ed in the default log prefix
- <kbd>logFile</kbd> — _default:_ <kbd>@storage/logs/web.log</kbd> — The log file path or path alias
- <kbd>enableRotation</kbd> — _default:_ <kbd>true</kbd> — Whether log files should be rotat­ed when they reach a cer­tain <kbd>maxFileSize</kbd>
- <kbd>maxFileSize</kbd> — _default:_ <kbd>10240</kbd> — Max­i­mum log file size, in kilo-bytes
- <kbd>maxLogFiles</kbd> — _default:_ <kbd>5</kbd> — Num­ber of log files used for rotation
- <kbd>levels</kbd> — _default_: <kbd>0</kbd> (all) with <kbd>devMode</kbd> on, <kbd>3</kbd> (<kbd>Logger::LEVEL_ERROR | Logger::LEVEL_WARNING</kbd>) with <kbd>devMode</kbd> off — A [bit­mask of log lev­els](https://github.com/yiisoft/yii2/blob/master/framework/log/Logger.php#L46) that should be record­ed in the log

There are a cou­ple of oth­er set­tings you can cus­tomize too, but they typ­i­cal­ly aren’t very inter­est­ing. See [FileTarget.php](https://github.com/yiisoft/yii2/blob/master/framework/log/FileTarget.php#L26) for details.

So how can we do this? We can con­fig­ure the web app that is Craft CMS via the <kbd>config/app.php</kbd> file by adding a <kbd>log</kbd> component:

```

<?php

return [
    'components' => [
        'log' => function() {
            $config = craft\helpers\App::logConfig();
            if ($config) {
                $config['targets'][0]['includeUserIp'] = false;
                $config['targets'][0]['logFile'] = '@storage/logs/web.log';
                $config['targets'][0]['enableRotation'] = true;
                $config['targets'][0]['maxFileSize'] = 10240;
                $config['targets'][0]['maxLogFiles'] = 5;
                return Craft::createObject($config);
            }
            return null;
        },
    ],
];

```

There’s a bit to unpack here, but noth­ing we can’t han­dle! We’re set­ting the <kbd>log</kbd> com­po­nent to an [anony­mous func­tion](https://www.php.net/manual/en/functions.anonymous.php) that gets a base <kbd>$config</kbd> from the built-in Craft helper method <kbd>craft\helpers\App::logConfig()</kbd>.

I imag­ine that Pix­el & Ton­ic wrote a helper method because the default log­ger con­fig that Craft CMS uses has some meat on its bones. Let’s have a look at it:

```

    /**
     * Returns the `log` component config.
     *
     * @return array|null
     */
    public static function logConfig()
    {
        // Only log console requests and web requests that aren't getAuthTimeout requests
        $isConsoleRequest = Craft::$app->getRequest()->getIsConsoleRequest();
        if (!$isConsoleRequest && !Craft::$app->getUser()->enableSession) {
            return null;
        }

        $generalConfig = Craft::$app->getConfig()->getGeneral();

        $target = [
            'class' => FileTarget::class,
            'fileMode' => $generalConfig->defaultFileMode,
            'dirMode' => $generalConfig->defaultDirMode,
            'includeUserIp' => $generalConfig->storeUserIps,
            'except' => [
                PhpMessageSource::class . ':*',
            ],
        ];

        if ($isConsoleRequest) {
            $target['logFile'] = '@storage/logs/console.log';
        } else {
            $target['logFile'] = '@storage/logs/web.log';

            // Only log errors and warnings, unless Craft is running in Dev Mode or it's being installed/updated
            if (!YII_DEBUG && Craft::$app->getIsInstalled() && !Craft::$app->getUpdates()->getIsCraftDbMigrationNeeded()) {
                $target['levels'] = Logger::LEVEL_ERROR | Logger::LEVEL_WARNING;
            }
        }

        return [
            'class' => Dispatcher::class,
            'targets' => [
                $target,
            ]
        ];
    }

```

We don’t need to grok every­thing it’s doing here, but let’s at least have a look at what this <kbd>$config</kbd> typ­i­cal­ly looks like when returned if <kbd>devMode</kbd> is on(thus <kbd>levels</kbd> is not limited):

```

[
    'class' => 'yii\\log\\Dispatcher'
    'targets' => [
        0 => [
            'class' => 'craft\\log\\FileTarget'
            'fileMode' => null
            'dirMode' => 509
            'includeUserIp' => false
            'except' => [
                0 => 'yii\\i18n\\PhpMessageSource:*'
            ]
            'logFile' => '@storage/logs/web.log'
        ]
    ]
]

```

Look­ing at it this way, it makes a whole lot more sense why we’re doing:

```

                $config['targets'][0]['enableRotation'] = true;

```

The log con­fig has a <kbd>class</kbd> of <kbd>yii\log\Dispatcher</kbd> which almost all log­gers will have, because this core Yii com­po­nent han­dles dis­patch­ing mes­sages from a log­ger to an arbi­trary num­ber of reg­is­tered targets.

And that’s the oth­er prop­er­ty we’re set­ting in the con­fig, an array of <kbd>targets</kbd>. The tar­get arrays have a con­fig­u­ra­tion for an arbi­trary num­ber of class­es that extend the abstract class <kbd>yii\log\Target</kbd>.

The first (<kbd>0</kbd>th) ele­ment in the <kbd>targets</kbd> array is Craft CMS’s default file tar­get, so we can set con­fig set­tings for that log tar­get here, <kbd>enableRotation</kbd> in this case.

So let’s look at a prac­ti­cal exam­ple. For [Craft con­sole requests](https://dev.to/gaijinity/exploring-the-craft-cms-3-console-command-line-interface-cli-452-temp-slug-1468335) run via the com­mand line, [dev­Mode is enabled by default](https://github.com/craftcms/cms/blob/develop/bootstrap/bootstrap.php#L170-L184), but we can fix this easily:

```

<?php

use craft\helpers\ArrayHelper;
use yii\log\Logger;

return [
    'components' => [
        'log' => function() {
            $config = craft\helpers\App::logConfig();
            if ($config) {
                $generalConfig = Craft::$app->getConfig()->getGeneral();
                $devMode = ArrayHelper::getValue($generalConfig, 'devMode', false);
                // Only log errors and warnings, unless Craft is running in Dev Mode or it's being installed/updated
                if (!$devMode && Craft::$app->getIsInstalled() && !Craft::$app->getUpdates()->getIsCraftDbMigrationNeeded()) {
                    $config['targets'][0]['levels'] = Logger::LEVEL_ERROR | Logger::LEVEL_WARNING;
                }

                return Craft::createObject($config);
            }
            return null;
        },
    ],
];

```

This will effec­tive­ly make con­sole requests work just like web requests, in that it’ll only log <kbd>Logger::LEVEL_ERROR | Logger::LEVEL_WARNING</kbd> log lev­els if <kbd>devMode</kbd> is off.

## Cus­tom Plugin/​Module Log­ging Behavior

Anoth­er use case for cus­tom log­ging behav­ior is if you have a plu­g­in or mod­ule that you want to send all of its log mes­sages to a sep­a­rate file.

![Logging lumberhack 03](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_992x558_crop_center-center_82_line/logging-lumberhack-03.jpg)

Maybe for debug­ging pur­pos­es, we want to be able to look at just the mes­sages that our plu­g­in or mod­ule logged with­out any oth­er log mes­sages mud­dy­ing the waters.

We can actu­al­ly do this very eas­i­ly, just by adding a bit of code to our plu­g­in or mod­ule’s <kbd>init()</kbd> method:

```

public function init() {
    // Create a new file target
    $fileTarget = new \craft\log\FileTarget([
        'logFile' => '@storage/logs/retour.log',
        'categories' => ['nystudio107\retour\*']
    ]);
    // Add the new target file target to the dispatcher
    Craft::getLogger()->dispatcher->targets[] = $fileTarget;
}

```

This cre­ates a new <kbd>craft\log\FileTarget</kbd> ini­tial­ized with a cus­tom <kbd>logFile</kbd> set to <kbd>@storage/logs/retour.log</kbd>. It when gets the cur­rent log­ger via <kbd>Craft::getLogger()</kbd> and adds the new­ly cre­at­ed <kbd>$fileTarget</kbd> to the array of exist­ing <kbd>targets</kbd> in the dispatcher.

We set the <kbd>categories</kbd> array to just one item: <kbd>nystudio107\retour\*</kbd>. This means that this log tar­get will only be sent log mes­sages when the cat­e­go­ry is set to any­thing that begins with <kbd>nystudio107\retour\</kbd> (the <kbd>*</kbd> is a wildcard).

<aside>
                                This allows us to lever­age the exist­ing log­ging sys­tem very effectively.
                            </aside>

This will log any mes­sages com­ing from the Retour plug­in’s name­space <kbd>nystudio107\retour\*</kbd> because when we log, we do <kbd>Craft::error('message', __METHOD__ );</kbd>

The <kbd> __METHOD__ </kbd> [PHP mag­ic con­stant](https://www.php.net/manual/en/language.constants.predefined.php) out­puts the cur­rent <kbd>fully\qualified\class::method</kbd>.

The nice thing about doing it this way is that in addi­tion to get­ting just our plug­in’s mes­sages logged to a sep­a­rate log file, they also still go to to pri­ma­ry log <kbd>@storage/logs/web.log</kbd> file. We just added an addi­tion­al log target.

This mat­ters because then we can still use the Yii Debug Tool­bar to sift through all of the logs, as described in the [Pro­fil­ing your Web­site with Craft CMS 3’s Debug Tool­bar](https://dev.to/gaijinity/profiling-your-website-with-craft-cms-3-8217-s-debug-toolbar-1jig-temp-slug-5108979) article.

**N.B.:** This method is a refine­ment of the method used by [Robin here](https://craftcms.stackexchange.com/questions/25427/craft-3-plugins-logging-in-a-separate-log-file); there’s also a vari­ant by [Abra­ham here](https://stackoverflow.com/questions/24507489/how-to-maintain-different-log-files-for-different-purposes-in-yii2).

## A Com­plete­ly Cus­tom Logger

Let’s say you want a com­plete­ly cus­tom log­ger, per­haps to send the Craft CMS logs to a SaaS that han­dles log aggre­ga­tion such as [Paper­Trail](https://papertrailapp.com/) or [Sen­try](https://sentry.io/) or the like.

![Logging lumberjack 03](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/logging-lumberjack-03.jpg)

In this case, we’ll need both some cus­tom con­fig as well as cus­tom code. Let’s start with the con­fig; we’ll once again need to put a cus­tom <kbd>log</kbd> com­po­nent in our <kbd>config/app.php</kbd>:

```

<?php

return [
    'components' => [
        'log' => [
            'class' => 'yii\\log\\Dispatcher'
            'targets' => [
                [
                    'class' => 'modules\sitemodule\log\CustomTarget',
                    'logFile' => '@storage/logs/custom.log',
                    'levels' => ['error', 'warning'],
                    'exportInterval' => 100,
                ],
            ],
        ],
    ],
];

```

In this case, we com­plete­ly replace the <kbd>log</kbd> com­po­nent with our own via a con­fig­u­ra­tion array. The tar­get’s <kbd>class</kbd> points to a cus­tom log tar­get in our site mod­ule’s name­space at <kbd>modules\sitemodule\log\CustomTarget</kbd>.

If you’re not famil­iar with cus­tom mod­ules, check out the [Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule](https://dev.to/gaijinity/enhancing-a-craft-cms-3-website-with-a-custom-module-33g1-temp-slug-8101978) article.

We have a few oth­er prop­er­ties that we’re con­fig­ur­ing here:

- <kbd>logFile</kbd> — the file we’re log­ging to (see below)
- <kbd>levels</kbd> — the log lev­els we’re inter­est­ed in, in this case just <kbd>error</kbd> & <kbd>warning</kbd>
- <kbd>exportInterval</kbd> — how many mes­sages should be accu­mu­lat­ed before they are exported

There are oth­er prop­er­ties we might con­fig­ure here, too; see [Target.php](https://github.com/yiisoft/yii2/blob/master/framework/log/Target.php) for more.

You might won­der why we spec­i­fy a <kbd>logFile</kbd> here, if we’re going to be send­ing our log mes­sages to a SaaS some­where out in the cloud anyway.

The rea­son is that we want a local log as well, in case some­thing goes awry with our log­ging aggre­ga­tion ser­vice, or we sim­ply want to be able to debug & work com­plete­ly offline.

For this rea­son, our <kbd>CustomTarget</kbd> class extends the built-in <kbd>FileTarget</kbd> class, so we can inher­it all of its file log­ging good­ness for free, while adding our own cus­tom bit to trans­mit data to our API:

```

<?php
/**
 * Site module for Craft CMS 3.x
 *
 * An example module for Craft CMS 3 that lets you enhance your websites with a custom site module
 *
 * @link https://nystudio107.com/
 * @copyright Copyright (c) 2019 nystudio107
 */

namespace modules\sitemodule\log;

use Craft;
use craft\helpers\Json;
use craft\log\FileTarget;

/**
 * @author nystudio107
 * @package SiteModule
 * @since 1.0.0
 */
class CustomTarget extends FileTarget
{
    // Constants
    // =========================================================================

    const API_URL = 'https://api.example.com';
    const API_KEY = 'password';

    // Public Methods
    // =========================================================================

    /**
     * @inheritDoc
     */
    public function export()
    {
        // Let the parent FileTarget export the messages to a file
        parent::export();
        // Convert the messages into a JSON-encoded array of formatted log messages
        $messages = Json::encode(array_map([$this, 'formatMessage'], $this->messages));
        // Create a guzzle client, and send our payload to the API
        $client = Craft::createGuzzleClient();
        try {
            $client->post(self::API_URL, [
                'auth' => [
                    self::API_KEY,
                    '',
                ],
                'form_params' => [
                    'messages' => $messages,
                ],
            ]);
        } catch (\Exception $e) {
        }
    }
}

```

Let’s break down what we’re doing here. We over­ride only a sin­gle method <kbd>export()</kbd> which is called by the log­ger at <kbd>exportInterval</kbd> when it is time to export the mes­sages to an exter­nal destination.

First we call <kbd>parent::export()</kbd> to let our <kbd>FileTarget</kbd> par­ent class do its thing export­ing the log mes­sages to a file.

Then we call the par­ent <kbd>Target</kbd> class’s <kbd>formatMessage</kbd> method as a [PHP Callable](https://www.php.net/manual/en/language.types.callable.php) that we pass into the [array\_​map](https://www.php.net/manual/en/function.array-map.php) func­tion, which returns an array of for­mat­ted log lines that we then JSON encode.

Final­ly, we cre­ate a [Guz­zle client](http://docs.guzzlephp.org/en/stable/) and send off our JSON-encod­ed mes­sages along with our <kbd>auth</kbd> to an exam­ple API.

While the code you’d use to com­mu­ni­cate with an actu­al SaaS would be slight­ly dif­fer­ent depend­ing on their API, the approach is the same.

And as you can see, we did­n’t write all that much code to accom­plish it.

## Monolog as a Logger

If we’re cus­tomiz­ing the Craft CMS log­ger, a very typ­i­cal use-case is that we’re just send­ing the log data to some third par­ty log­ging ser­vice such as [Paper­Trail](https://papertrailapp.com/) or [Sen­try](https://sentry.io/). We saw in the pre­vi­ous sec­tion that we could do that by writ­ing our own cus­tom logger.

But we can also do it by lever­ag­ing a PHP library called [Monolog](https://github.com/Seldaek/monolog). Monolog is sort of a meta log­ger, in that it han­dles send­ing your logs to files, sock­ets, inbox­es, data­bas­es and var­i­ous web services.

Thank­ful­ly, this is very easy to set up as a log tar­get in Craft CMS, with just config!

Install Monolog in our project:

```

composer require "monolog/monolog"

```

Then we’ll also need to install [Yii 2 PSR Log Tar­get](https://github.com/samdark/yii2-psr-log-target) in our project:

```

composer require "samdark/yii2-psr-log-target"

```

Then we just need to add some con­fig to our <kbd>config/app.php</kbd> file to tell it to use Monolog, and con­fig­ure Monolog to send the log­ging where we want it to:

```

<?php

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

return [
    'components' => [
        'log' => [
            'targets' => [
                [
                    'class' => \samdark\log\PsrTarget::class,
                    'except' => ['yii\web\HttpException:40*'],
                    'logVars' => [],
                    'logger' => (new Logger('app'))
                        ->pushHandler(new StreamHandler('php://stderr', \Monolog\Logger::WARNING))
                        ->pushHandler(new StreamHandler('php://stdout', \Monolog\Logger::DEBUG)),
                    'addTimestampToContext' => true,
                ]
            ],
        ]
    ],
];

```

This just sets up a han­dler that logs to the stan­dard [php IO streams](https://www.php.net/manual/en/wrappers.php.php) (which then might be going else­where), but Monolog sup­ports many [third par­ty pack­ages](https://github.com/Seldaek/monolog/wiki/Third-Party-Packages).

In addi­tion, it’s so pop­u­lar that there are con­figs explic­it­ly list­ed for ser­vices like [Paper­trail](https://help.papertrailapp.com/kb/configuration/configuring-centralized-logging-from-php-apps/#monolog) and [Sen­try](https://github.com/getsentry/sentry-docs/issues/1104).

## Log­ging Out

We’ve just scratched the sur­face of what you could poten­tial­ly do with cus­tom log­ging. For exam­ple, you could have it nor­mal­ly log to a file, but if there’s an error log lev­el, you could have it email some­one important.

![Logging lumberjack 06](https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/logging-lumberjack-06.jpg)

Hope­ful­ly this arti­cle has giv­en you have some idea of the method­ol­o­gy to use when approach­ing cus­tom logging.

<aside>
                                Embrace <span>&amp;</span> extend, don’t rein­vent the wheel
                            </aside>

Yii has some fan­tas­tic log­ging facil­i­ties built into it. Lever­age the work that they have done, and hoist your cus­tom code on top of it.

Hap­py logging!

## Further Reading

_If you want to be notified about new articles, follow [nystudio107](https://twitter.com/nystudio107) on Twitter._

<small>Copyright ©2020 nystudio107. Designed by nystudio107</small>