Getting Started Quickly With PHP Logging

The previous articles in this series covered the basics of logging in C#, Java, Python, Ruby, Node.js, and JavaScript. In this post, I’ll show you how to use logging techniques in yet another very popular language: PHP.

I’ll open with a quick example of manual PHP logging. Then we’ll revisit the details of why logging matters and what your logs should show. And lastly, I’ll show you how to set up and use the most popular PHP logging framework.

Let’s get started, then!

PHP with Scalyr colors 2

A (Super Simple) Example of PHP Logging

For the purpose of this tutorial, I’ll be using Linux with a clean installation of LAMP stack. You might need to adjust the tools and the commands if you’re using Mac or Windows instead of Linux.

Navigate to the /var/www/html folder and delete a file named index.html from there:

$ cd /var/www/html
$ rm index.html

Then create a new file index.php and open it in your favorite text editor (I’m using Emacs):

$ emacs index.php

Add the following HTML code into index.php:

<form method="post" style="margin-top: 100px; margin-left: auto; margin-right: auto; text-align: center;">
    <div>
	<label for="name">Name:</label>
	<input type="text" id="name" name="name">
    </div>
    <div>
	<label for="email">E-mail:</label>
	<input type="email" id="email" name="email">
    </div>
    <div>
	<button type="submit" name="submit">Subscribe</button>
    </div>
</form>

This HTML code will create a simple subscription form. To see the resulting page, open your browser and go to http://localhost:

Even though you can see the form in the browser, it doesn’t do anything useful right now. To make it functional, add the following PHP script at the top of the index.php file, before the HTML code:

<?php
if(isset($_POST['submit']))
{
    $name = $_POST['name'];
    $email = $_POST['email'];

    subscribe($name, $email);
}

function subscribe($name, $email) {
    // subscription logic here
}
?>

The above code will extract name and email parameters from the submitted request and pass them into the subscribe function, which will have the code that takes care of subscribing new users.

But since this tutorial is about logging, I’ll leave the subscribe function’s body empty for now.

Now, let’s say you want to keep a log file that keeps track of all subscription requests alongside their respective parameters. You can do that by adding just two lines right before a call to subscribe function:

<?php
if(isset($_POST['submit']))
{
    $name = $_POST['name'];
    $email = $_POST['email'];

    $logMsg = "Subscription; user: " . $name . " email: " . $email . PHP_EOL;
    error_log($logMsg, 3, 'servorer.log');

    subscribe($name, $email);
}

function subscribe($name, $email) {
    // subscription logic here
}
?>

The additional two lines of code will call to PHP’s error_log function that will append a new log message to the contents of server.log file.

Now if you open http://localhost in your browser, populate the input fields, and click on subscribe button, this script will create a new file named server.log in the /var/www/html directory and write the log entry into it. I can read the contents of this file using cat command:

$ cat server.log
Subscription; user: testName email: [email protected]

Congratulations: you’ve logged your first entry written in PHP!

Now, let’s back up a bit.

What Is Application Logging?

You probably already have an intuitive understanding of what logging is, but it’s still worth taking the time to go over a proper definition we can build upon.

In the first article in this series, we defined application logging like this:

Application logging involves recording information about your application’s runtime behavior to a more persistent medium.

There are two important points to note here.

  • First, logging is all about the application’s runtime behavior. In other words, it involves recording the application’s events as a function of time.
  • Second, you always want to store logs in a more persistent medium. The exact meaning of “more persistent” will vary depending on your requirements and environment, but at the very least, you’ll want to be able to read the logs even if the application—or the entire server—crashes.

While this definition explains what logging is, it doesn’t explain why you’d want to use logging in your application. That’s an important question, and I’d like to address it in detail.

Why Log?

If we lived in an ideal world, our applications would just work as we planned. However, the reality is that even well-constructed applications have bugs and will be at risk of crashing at one point or another.

php logger

When you develop on your personal computer and encounter errors, they are usually relatively simple to resolve.

After all, you know what you’ve been working on and what sequence of events led to the error. In addition, you can develop in debug mode, which gives you lots of additional information about the system.

But at some point, you need to release the application to your users.

They will use it in original ways and in environments you couldn’t have imagined, and there will be many of them, potentially using the application concurrently.

Plus your application will need to function for much longer periods of time. Debugging errors in this situation is impossible without additional information.

Enter logging.

Having a record of your application’s past runtime behavior allows you to investigate all kinds of undesired behavior post-factum.

In other words, even if your application crashes and loses all its runtime state, you’ll still be able to understand what led to that crash using application’s logs.

But the usefulness of logs doesn’t stop there.

Logs become priceless when users report bugs: they can audit your application, including regulatory audits required in specific business domains; they can be processed and analyzed in real time to warn you about potential performance issues; and more.

In short: application logging is a must for production applications because it gives you visibility into your application’s runtime history.

What Should You Log?

The simple logging approach described above fulfills the persistence requirement because it stores the logs in a file. However, it’s not great for recording runtime behavior because it doesn’t tell you when exactly each specific event happened.

This leads to a more general question: what information should be logged?

php logger

It’s a widespread practice to include at least the following information in each log entry:

  • Timestamp that states when exactly the event described by the log entry happened. To make your life easier, use properly formatted ISO 8601 date/time in UTC.
  • Event context. For example, it would be a bad idea to log “name: testName; email: [email protected]” because a month from now, you’ll probably forget what this means. Here’s some context to make the log entry more descriptive: “Subscription; user: testName email: [email protected].”
  • Log severity level, such as “error,” “warning,” “info,” etc. This information provides additional context and allows for easy filtering of logs by severity.

I suggest that you treat the above list as the absolute baseline for your logging strategy and don’t log less information than that. Otherwise, you’ll have a really hard time reading your logs and making sense of them.

Keep in mind that these are very basic guidelines. They will get you started, but there is much more to be said about logging.

I recommend you read this article about logging best practices and also consider reading OWASP’s Logging Cheat Sheet at some point to get a wider perspective on this important topic.

Default Logging Configuration

Even if your application doesn’t explicitly log messages the web server will do it for you, unless you disable this all together.

Of course, the events the web server will recognize out of the box are the most basic errors such a file not being readable, an uncaught exception and such.

By default, all of this information will go into the webserver’s error_log file.

The exact location of this file depends on the specific web server software you’re using and it’s configuration.

If you’re using Apache, the path to this file is part of the VirtualHost configuration (specifically the ErrorLog directive).

If you’re using Nginx, this definition will be inside a server block definition.

Another configuration file you need to aware of is php.ini.

In this file you can tweak a few settings to determine which messages will be logged through the webserver.

For instance, the value of error_reporting will determine which kind of error (fatal, warning, notice, etc…) gets logged.

Another important setting is display_errors which will determine whether errors should be sent to standard output or not.

In a production environment you want to pay special attention to these values: you don’t want to be too open to help attackers but neither too opaque to make your own life more complicated when things don’t work exactly as expected.

Make PHP Logging Easier with the Monolog Framework

In the example at the beginning of this post, I showed you how to do basic logging in PHP.

However, my solution didn’t log timestamps and severity levels, which as you now know are two mandatory pieces of information that must be included in each log entry.

Sure, I can add more code and enhance my solution to accommodate more advanced needs, but that would be a waste.

See, I do logging only because I need to—it’s not really part of my application’s functionality. Therefore, each minute I spend on implementing logging solutions is one less minute I have to work on the core functionality of my application.

Luckily, my logging needs are very similar to the needs of millions of other developers, so this problem has already been solved in form of so-called logging frameworks.

These are third-party libraries that I can import and use in my project without having to concern myself with exactly how they work. In other words, there’s no need for me to reinvent the wheel when it comes to logging.

The most popular logging framework for PHP is Monolog. In the next sections, I’ll show you how you can use it in your projects.

php logger

Monolog Fundamentals

The most important concept in Monolog is the Logger class. When working with monolog, you’ll have at least one instance of Logger. Each logger has a channel, or a name, and also one or more handlers.

The handlers—sometimes called “appenders” in other frameworks—are the components that effectively write the log messages to their destinations, such as text files.

When you add a message to the logger, it goes through the entire stack of handlers, which can then decide whether to propagate it to the next handler, or not.

This design allows for very flexible logging setups, since you can create many loggers, and each logger can use various different handlers. Each handler can have a $bubble property, which allows to you define whether they propagate or not the log record if they handle it.

Monolog Formatters

Handlers also have a Formatter, which is used to format and normalize log entries. If you don’t define a Formatter for a Handler, it resorts to the default one, which has sensible default settings.

A Formatter is a class which goal is to take the raw message you want to log and transform it into an improved version (perhaps adding context details, replacing placeholders, etc…).

There are standard Formatters that come with the Monolog package which you can check out here and you also have the ability to create your own.

In order to attach a Formatter to a Handler you need to use the method setFormatter as you’ll see in the example below.

Monolog Logging Levels

Logging levels are a vital concern to any logging framework. How do they work in Monolog?

The first thing you need to know is that Monolog doesn’t offer custom levels. Instead, the eight RFC 5424 levels are available:

  • DEBUG (100) – Detailed information for debugging purposes.
  • INFO (200) – Interesting but commonplace events. Example: User of a project management app creates a new project template.
  • NOTICE (250) – Still normal,  but significant events. Example: User of an investment platform places an order that doesn’t adhere to their investor profile.
  • WARNING (300) – Exceptional occurrences that are signs that something might be wrong, or might generate some future problem.
  • ERROR (400) – Runtime errors that do not require immediate action but probably require monitoring and further investigation.
  • CRITICAL (500) – Critical conditions, more severe than errors. Require action, but not as immediate as alerts.
  • ALERT (550) – Action must be taken immediately. Example: Entire website down. This should wake someone up.
  • EMERGENCY (600) – Emergency: application is unusable.

What if you have the need for more flexibility, such as sorting or more detailed filtering? In such a case, you should add Processors to the Logger. That way, you’ll be able to put additional information (such as tags) into the entries, before the handlers take care of them.

Monolog in Practice

1. Installing Monolog PHP Logging Framework

If you’re developing your application using a PHP framework (e.g., Laravel), chances are this framework already integrates with Monolog. Check your framework’s documentation for details.

Since I’m working with a very simple PHP project, I can show you how to install and use Monolog from scratch.

First, you’ll need to install the PHP Composer package manager. The following commands will achieve that:

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php composer-setup.php --install-dir=/usr/local/bin --filename=composer
$ php -r "unlink('composer-setup.php');"

Once you’ve got Composer on your machine, go to the root directory of your project at /var/www/html/ and execute the following command:

$ composer require monolog/monolog

This command will create vendor/directory and import Monolog files there. The next step will be to make use of Monolog from within the PHP code.

2. Using Monolog

To use Monolog in your code, you’ll need to load it as a dependency. For this purpose, Composer conveniently generated an autoload.php file that you can access from your code.

Put the following code at the top of your PHP script in an index.php file:

require __DIR__ . '/vendor/autoload.php';

And right after that, specify two specific Monolog dependencies that you’re going to use:

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

Logger, as you’ve already seen, is the object that generates logs, and StreamHandler is a special object that Logger uses internally to write logs to files. As you might guess, Monolog supports different handlers that allow you to route the logs into different mediums.

So far, you’ve loaded Monolog and imported specific dependencies. Now instantiate a new Logger and name it SimpleLogger:

$logger = new Logger('SimpleLogger');

Next, use StreamHandler to make sure the logger stores the logs into your existing server.log file:

$logger->pushHandler(new StreamHandler(__DIR__.'/server.log', Logger::DEBUG));

The second parameter in StreamHandler’s constructor defines the lowest severity level it picks up. Since you want it to capture all log entries in this case, specify the lowest severity level, which is DEBUG.

After setting up Logger object, you can replace your old logging implementation with Monolog’s Logger. So, delete this line:

error_log($logMsg, 3, 'servorer.log');

and replace it with the following call:

$logger->info($logMsg);

As you can see, even though you had to do a bit of up-front setup, this process made logging much more concise and simple. Given that in real applications, you might want to log hundreds of events, this conciseness will become very handy.

php logger

All in all, your index.php file should look like this:

<?php

require __DIR__ . '/vendor/autoload.php';

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

$handler = new StreamHandler(__DIR__.'/server.log', Logger::DEBUG); 
$handler->setFormatter(new LineFormatter());

$logger = new Logger('SimpleLogger');
$logger->pushHandler($handler);

if(isset($_POST['submit']))
{
    $name = $_POST['name'];
    $email = $_POST['email'];

    $logMsg = "Subscription; user: " . $name . " email: " . $email . PHP_EOL;
    $logger->info($logMsg);
    
    subscribe($name, $email);
}

function subscribe($name, $email) {
    // subscription logic here
}
?>

<form method="post" style="margin-top: 100px; margin-left: auto; margin-right: auto; text-align: center;">
    <div>
	<label for="name">Name:</label>
	<input type="text" id="name" name="name">
    </div>
    <div>
	<label for="email">E-mail:</label>
	<input type="email" id="email" name="email">
    </div>
    <div>
	<button type="submit" name="submit">Subscribe</button>
    </div>
</form>

3. Reading Monolog Logs

If you go to http://localhost in your browser again, specify the same user details, and click on subscribe, a new log entry will be added to server.log file. Let’s contrast the original log entry from the first example with the one generated by Monolog.

Here’s the original log entry:

Subscription; user: testName email: [email protected]

And here’s what Monolog produced:

[2020-01-08 16:17:01] SimpleLogger.INFO: Subscription; user: testName email: [email protected] [] []

As you see, Monolog automatically added the following information to the log entry:

  • Timestamp
  • Logger name
  • Log severity level

And the best part is that you didn’t need to write all this stuff yourself.

The beauty of using a standardized way to log messages (besides saving you time while writing your code) is the ability to filter the output using simple tools such as grep.

Suppose you want to find messages that have INFO as their severity level. You’d simply issue a command like:

cat server.log | grep INFO

And get the exact result you’re looking for.

The only issue with this log entry is that the timestamp is not UTC. There are at least two ways to fix that:

  1. Specify UTC timezone when instantiating Logger.
  2. Specify UTC timezone for the entire PHP script.

In most cases, you’ll want your entire application to use UTC time, so I suggest adding the following method call at the top of your PHP script:

date_default_timezone_set('UTC');

All date and time function calls following this line will use UTC timezone by default—very useful.

What Now?

I’ve explained to you the basics of logging in PHP, which should be enough to get you started.

Still, you might want to search for more advanced information to solidify your understanding. Simply follow the links in this post, and you’ll be alright.

If you’re going to use Monolog (as you probably should), invest some time into reading its documentation. This logging framework is extremely powerful and configurable, which allows you to support basically any logging requirements.

As your applications grow in size and complexity, and as your log files become more complex, you might find it challenging to make sense of them even if you follow all best practices.

That’s where log aggregation tools, like the one Scalyr makes, can be lifesavers. These tools help you aggregate, process, search, and visualize your logs, making it easier to find what you need in the vast ocean of log entries.

So go ahead and use application logging in your PHP projects! And remember: you should probably err on the side of logging more than you currently think you need.