Log files are just good practice; not only can they help in development and debugging, but they can act as silent sentinels, watching over your code and helping you find a quick solutions when things go wrong.

How to use Monolog with a custom WordPress plugin

Monolog is a PHP library that’s widely used for logging. We develop a lot of custom plugins at Webinology, and a while back we decided that we wanted to use Monolog in our WordPress Plugin Boilerplate-based plugins.

WordPress Plugin Boilerplate is class-centric and we wanted to devise a way to add Monolog that fit with the structure. This is how we did it.

Step One: Get Boilerplate

https://wppb.me

The easiest way to start a new boilerplate plugin is with the WordPress Plugin Boilerplate Generator. Just enter the basic info and you’ll get a zip file that’s ready to load in your development site. Install it in WordPress like you would any other plugin.

For the sake of this post, we’ll use the incredibly imaginative name “My Plugin” for the examples.

Step Two: Add Composer

In the root directory of your plugin, add a composer.json file with your basic requirements:

{
    "require": {
        "psr/log": "^1.1",
        "monolog/monolog": "^2.3"
    }
}

Next, run “composer install” from the command line in your plugin’s root directory. This will create a “vendor” folder containing the Monolog files.

Your plugin will need to load the files you’ve added via Composer, so in the main plugin file (i.e. /my-plugin.php), add the following lines:

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}
// Autoload Composer-included libraries
require plugin_dir_path( __FILE__ ) . '/vendor/autoload.php';
define( 'PLUGIN_ROOT_PATH', plugin_dir_path( __FILE__ ));

Note that I also define “PLUGIN_ROOT_PATH” for later use; that’s where we’ll store our log file.

Step Three: Load Monolog

In order to load Monolog and make it available throughout your plugin, edit the main file in the includes folder (i.e. /includes/class-my-plugin.php) and add the required use statements at the top:

<?php
/**
 * The file that defines the core plugin class
 *
 * A class definition that includes attributes and functions used across both the
 * public-facing side of the site and the admin area.
 *
 */
use MonologLogger;
use MonologHandlerStreamHandler;

We’ll need a place from which to invoke Monolog, so we’ll add a new protected property:

/**
 * The Monolog Logger
 * @var Logger $logger
 */
protected $logger;

Then in the constructor function of this file (/includes/class-my-plugin.php), initialize your Monolog instance:

/**
 * Define the core functionality of the plugin.
 *
 * Set the plugin name and the plugin version that can be used throughout the plugin.
 * Load the dependencies, define the locale, and set the hooks for the admin area and
 * the public-facing side of the site.
 *
 * @since    1.0.0
 */
public function __construct() {
    ...
    $this->logger = new Logger('MY-LOGGER');
    $this->logger->pushHandler(new StreamHandler(PLUGIN_ROOT_PATH . 'plugin.log', Logger::INFO));
    ...
}

Notice that we named our logger “MY-LOGGER” when we instantiated it (line 13, above). This is the channel name, which can be useful if you have a reason to want some segregation of log messages in your log file.

Next, we define a StreamHandler with the name of our log file (‘plugin.log’), the location (PLUGIN_ROOT_PATH), and the log level (in this case we used INFO but there are several options available; I’ll explain this in a little more detail at the end).

Further down in this same file (/includes/class-my-plugin.php) you’ll see two methods: define_admin_hooks and define_public_hooks. We’re going to add our logger to the objects that they instantiate.

In the admin method (define_admin_hooks), you’ll see that Boilerplate instantiates My_Plugin_Admin with two “getter” methods (for plugin name and version). We’re adding a third, the as-of-yet undefined get_logger method:

/**
 * Register all of the hooks related to the admin area functionality
 * of the plugin.
 *
 * @since    1.0.0
 * @access   private
 */
private function define_admin_hooks() {
    $plugin_admin = new My_Plugin_Admin( $this->get_plugin_name(), $this->get_version(), $this->get_logger() );
    $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' );
    $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' );
    ...
}

Next, do the same thing in the public section:

/**
 * Register all of the hooks related to the public-facing functionality
 * of the plugin.
 *
 * @since    1.0.0
 * @access   private
 */
private function define_public_hooks() {
    $plugin_public = new My_Plugin_Public( $this->get_plugin_name(), $this->get_version(), $this->get_logger() );
    $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' );
    $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' );
    ...
}

Finally, add the getter method at the bottom of this file:

/**
 * Retrieve the Monolog logger
 * @return Logger
 */
public function get_logger() {
    return $this->logger;
}

Step Four: Using the Logger

In a Boilerplate setup, most of your code is going to happen in the main admin and public class files. These are:

  • /admin/class-my-plugin-admin.php
  • /public/class-my-plugin-public.php

What you’ll do in both of these files is essentially the same.

First, add a new private $logger property:

/**
 * @var MonologLogger $logger
 */
private $logger;

Next, add $logger to the constructor (is this starting to look familiar?!):

/**
 * Initialize the class and set its properties.
 *
 * @since    1.0.0
 * @param      string    $plugin_name       The name of this plugin.
 * @param      string    $version    The version of this plugin.
 */
public function __construct( $plugin_name, $version, $logger ) {
    $this->plugin_name = $plugin_name;
    $this->version = $version;
    $this->logger = $logger;
}

Consistent with other things that Boilerplate can’t live without (its name and its version), we think you can’t live without logging (well, you can, but you’ll be sorry later!) So we’ve made it an integral part of the process. On line 8, above, we’ve added $logger to the list of fields the method receives and then we plug it in as a property for subsequent use (line 12).

Step Four: Log stuff!

You’re now ready to use Monolog in your plugin. Here are a couple of examples:

$this->logger->warning('Something happened!');
$this->logger->notice('This is a test.', ['username' => 'LAMPStack_Ninja']);

In the first example, we just logged a single message. In the second, we passed in some context information. There are other options available so I encourage you to check out the official usage instructions.

Extra Info

Remember how we defined our log-level as “INFO” earlier on? That means that anything valued at the level of INFO (100) or higher (for example, WARNING (300) or ERROR (400)) will be written to the log. On the other hand, DEBUG (100) would not get written to the file in our example.

Why is this important?

We’ve all used “var_dump($something); die;” when debugging. With Monolog, you can include statements like this in your code:

$field1 = "Some value.";
$this->logger->debug('Before doing something:', ['$field1' => $field1]);
$field1 = strrev($field1);
$this->logger->debug('After doing something:', ['$field1' => $field1]);

With the log-level set to INFO, no messages will be recorded in the log file. However, if you’re trying to debug, you can change INFO to DEBUG in one place:

$this->logger->pushHandler(new StreamHandler(PLUGIN_ROOT_PATH . 'plugin.log', Logger::DEBUG));

And then you’ll see your messages in the log file:

[2021-12-02T18:55:13.606535+00:00] MY-LOGGER.DEBUG: Before doing something: {"$field1":"Some value."} []
[2021-12-02T18:55:13.607268+00:00] MY-LOGGER.DEBUG: After doing something: {"$field1":".eulav emoS"} []

Before you deploy your code, you’ll want to set the log level higher so that only important things (like unexpected errors) get logged.

Also, you’ll probably want to include your log file in your .gitignore file if you’re using Git (GitHub, Bitbucket, GitLab, etc.) Finally, it would be a good idea to add some code to clean up your log file to keep it from becoming overly large.

Happy logging!

One Response

Leave a Reply

Your email address will not be published. Required fields are marked *