Guides
Best PHP Logging Libraries

The Best PHP Logging Libraries

Author's avatar
Ayooluwa Isaiah
Updated on August 31, 2023

PHP logs go beyond just errors. They're handy for tracking API and function performance, as well as keeping count of significant events in your application. No matter the project you're working on, implementing a logging strategy will help keep you in the loop about what's happening in your code.

Unlike languages like Go, Python, or Ruby, a dedicated logging solution does not exist in its standard library, so you must choose one of the available logging frameworks developed by the community. This article will present the top three options for logging in PHP and discuss their unique features, advantages, and use cases.

Let's get started!

Logtail dashboard

🔭 Want to centralize and monitor your PHP application logs?

Head over to Better Stack and start ingesting your logs in 5 minutes.

1. Monolog

monolog.png

Monolog is the go-to logging framework for PHP applications. It provides a flexible and powerful way to log events and route the entries to various destinations, such as files, databases, email, or even external services. Getting started with Monolog is straight-forward via Composer:

 
composer require monolog/monolog

You can subsequently import various Monolog classes into your project and use them as follows:

 
<?php

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

// create a log channel
$logger = new Logger("example");

$stream_handler = new StreamHandler("php://stdout");
$logger->pushHandler($stream_handler);

$logger->debug("database query executed");
$logger->info("user signed in");
$logger->notice("a notice message");
$logger->warning("disk space is 95% full");
$logger->error("unexpected error while backing up database");
$logger->critical("a critical error occurred");
$logger->alert("an alert level massage");
$logger->emergency("fatal error: exiting program");
Output
[2023-08-31T08:52:18.777327+00:00] example.DEBUG: database query executed [] []
[2023-08-31T08:52:18.779741+00:00] example.INFO: user signed in [] []
[2023-08-31T08:52:18.779773+00:00] example.NOTICE: a notice message [] []
[2023-08-31T08:52:18.779794+00:00] example.WARNING: disk space is 95% full [] []
[2023-08-31T08:52:18.779851+00:00] example.ERROR: unexpected error while backing up database [] []
[2023-08-31T08:52:18.779872+00:00] example.CRITICAL: a critical error occurred [] []
[2023-08-31T08:52:18.779900+00:00] example.ALERT: an alert level massage [] []
[2023-08-31T08:52:18.779915+00:00] example.EMERGENCY: fatal error: exiting program [] []

Monolog exposes a Logger class with a name and a stack of handlers. In the above example, the StreamHandler class is used to create a handler that logs to the standard output, and this handler is attached to the $logger.

Once an event is logged, each attached handler will decide whether to output the record to the configured destination and how to format it. The default format for Monolog handlers contains the timestamp, logger name, level, log message, then some context and extra information ([] []).

 
[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n

You can customize this format through any of the built-in formatter classes such as the JsonFormatter. Once you create a formatter instance, you must attach it to a handler instance as shown below:

 
<?php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
// create a log channel $logger = new Logger("example");
$formatter = new JsonFormatter();
$stream_handler = new StreamHandler("php://stdout");
$stream_handler->setFormatter($formatter);
$logger->pushHandler($stream_handler); . . .

You will now observe the following structured output:

Output
{"message":"fatal error: exiting program","context":{},"level":600,"level_name":"EMERGENCY","channel":"example","datetime":"2023-08-31T09:06:26.351676+00:00","extra":{}}

Monolog defaults to logging at the DEBUG level, but you can customize this through the Level class. To give maximum flexibility, the default level is defined on the handler instances so that attached handlers can log at different levels if desired:

 
use Monolog\Level;
. . .

$stdout_handler = new StreamHandler("php://stdout", Level::Info);
$stderr_handler = new StreamHandler("php://stderr", Level::Error);
. . .

To add contextual data to your log entries, you can pass an array of attributes as follows:

 
$logger->info("user signed in", ["username" => "johndoe", "user_id" => 123456]);

These data will be placed in the context property as shown below (assuming you're using JsonFormatter):

Output
{
  "message": "user signed in",
  "context": {
    "username": "johndoe",
    "user_id": 123456
  },
  "level": 200,
  "level_name": "INFO",
  "channel": "example",
  "datetime": "2023-08-31T09:17:27.322331+00:00",
  "extra": {}
}

A second way to add data to all log records created by a logger is through processors. For example, you can use the IntrospectionProcessor to add the originating line number, file name, method, or class of the logged event:

 
use Monolog\Processor\IntrospectionProcessor;

$logger = new Logger("example");

$processor = new IntrospectionProcessor();
$logger->pushProcessor($processor);

. . .

You will observe that the extra field is now populated:

Output
{
  "message": "user signed in",
  "context": {
    "username": "johndoe",
    "user_id": 123456
  },
  "level": 200,
  "level_name": "INFO",
  "channel": "example",
  "datetime": "2023-08-31T09:26:00.980933+00:00",
  "extra": {
    "file": "/home/ayo/dev/betterstack/demo/php-logging/main.php",
    "line": 24,
    "class": null,
    "callType": null,
    "function": "myFunc"
  }
}

When it comes to logging exceptions, Monolog can automatically catch and log uncaught exceptions before your application exits once you override the default exception handling behavior with set_exception_handler():

 
<?php

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

use Monolog\Level;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;

function exception_handler(Throwable $e)
{
    $logger = new Logger('uncaught');
    $stream_handler = new StreamHandler("php://stdout");
    $stream_handler->setFormatter(new JsonFormatter());
    $logger->pushHandler($stream_handler);
    $logger->error("Uncaught exception", array('exception' => $e));
    exit(1)
}

set_exception_handler("exception_handler");

class emptyClass
{
    // This class is empty
};

// Try to access a property that does not exist
emptyClass::one();

Details about the exception will be placed in the event's context property:

Output
{
  "message": "Uncaught exception",
  "context": {
    "exception": {
      "class": "Error",
      "message": "Call to undefined method emptyClass::one()",
      "code": 0,
      "file": "/home/ayo/dev/betterstack/demo/php-logging/main.php:27"
    }
  },
  "level": 400,
  "level_name": "ERROR",
  "channel": "uncaught",
  "datetime": "2023-08-31T09:33:21.349825+00:00",
  "extra": {}
}

Ensure to check out our dedicated Monolog guide for a deep dive on its concepts, features, and how it enhances logging practices in PHP applications.

2. Klogger

KLogger.png

Klogger is a simple PSR-3 compliant library for writing event logs in PHP programs. It aims to be a solution that you could quickly incorporate into you project and have working right away without a convoluted configuration process. You may install it using the command below:

 
composer require katzgrau/klogger:dev-master

KLogger supports the log levels defined in the PSR-3 interface, and it defaults to the DEBUG level:

 
<?php

$logger = new Katzgrau\KLogger\Logger("php://stdout");

$logger->debug("database query executed");
$logger->info("user signed in");
$logger->notice("a notice message");
$logger->warning("disk space is 95% full");
$logger->error("unexpected error while backing up database");
$logger->critical("a critical error occurred");
$logger->alert("an alert level massage");
$logger->emergency("fatal error: exiting program");
?>
Output
[2023-08-31 13:37:05.179491] [debug] database query executed
[2023-08-31 13:23:05.179521] [info] user signed in
[2023-08-31 13:37:05.179591] [notice] a notice message
[2023-08-31 13:37:05.179604] [warning] disk space is 95% full
[2023-08-31 13:37:05.179611] [error] unexpected error while backing up database
[2023-08-31 13:37:05.179620] [critical] a critical error occurred
[2023-08-31 13:37:05.179626] [alert] an alert level massage
[2023-08-31 13:37:05.179634] [emergency] fatal error: exiting program

You may modify the default level using the Psr\Log\LogLevel constants like this:

 
$logger = new Katzgrau\KLogger\Logger("php://stdout",  Psr\Log\LogLevel::WARNING);
Output
[2023-08-31 13:45:04.008550] [warning] disk space is 95% full
[2023-08-31 13:45:04.008656] [error] unexpected error while backing up database
[2023-08-31 13:45:04.008668] [critical] a critical error occurred
[2023-08-31 13:45:04.008677] [alert] an alert level massage
[2023-08-31 13:45:04.008683] [emergency] fatal error: exiting program

The KLogger constructor also supports other options via a third argument. A useful one is logFormat which defines the format of the log entries. JSON formatted log entries can be achieved through the following configuration:

 
$logFormat = json_encode([
    'ts' => '{date}',
    'level' => '{level}',
    'msg'  => '{message}',
    'ctx'  => '{context}',
]);

$logger = new Katzgrau\KLogger\Logger("php://stdout",  Psr\Log\LogLevel::WARNING, array (
  'logFormat' => $logFormat,
  'appendContext' => false
));

This yields log entries with the following structure:

Output
{"ts":"2023-08-31 13:59:18.217924","level":"WARNING","msg":"disk space is 95% full","ctx":"[]"}
{"ts":"2023-08-31 13:59:18.218084","level":"ERROR","msg":"unexpected error while backing up database","ctx":"[]"}

You can also add contextual attributes at log point like this:

 
$logger->info("user signed in", ["username" => "johndoe", "user_id" => 123456]);
Output
{"ts":"2023-08-31 14:05:20.802109","level":"INFO","msg":"user signed in","ctx":"{"username":"johndoe","user_id":123456}"}

Logging to a file is also fully supported like this:

 
$logger = new Katzgrau\KLogger\Logger(".",  Psr\Log\LogLevel::INFO);

The first argument to the constructor signifies the directory to place the log file. Once you execute the program, you should see a file that looks like this in the current working directory:

 
log_2023-08-31.txt

Please see the KLogger documentation to learn how to customize this filename and the options it offers.

3. Analog

analog.png

Analog's main goal is to provide a lightweight and simple logging solution similar Klogger, but without sacrificing flexibility regarding the logging destinations. It bundles a large number of handlers for this purpose, including the following:

  • Syslog: output to syslog.
  • Redis: send log entries to Redis.
  • Post: send log entries over HTTP.
  • Slackbot post log messages to Slack.
  • Mongo: store logs in a MongoDB collection.
  • and many more!

You can install the library through Composer like this:

 
composer require analog/analog

Afterward, it may be utilized as follows through the provided PSR-3-compliant Logger interface:

 
<?php
use Analog\Logger;
use Analog\Handler\EchoConsole;

$logger = new Logger;

$logger->handler (EchoConsole::init ());

$logger->debug("database query executed");
$logger->info("user signed in");
$logger->notice("a notice message");
$logger->warning("disk space is 95% full");
$logger->error("unexpected error while backing up database");
$logger->critical("a critical error occurred");
$logger->alert("an alert level massage");
$logger->emergency("fatal error: exiting program");
?>
Output
localhost - 2023-08-31 14:36:36 - 7 - database query executed
localhost - 2023-08-31 14:36:36 - 6 - user signed in
localhost - 2023-08-31 14:36:36 - 5 - a notice message
localhost - 2023-08-31 14:36:36 - 4 - disk space is 95% full
localhost - 2023-08-31 14:36:36 - 3 - unexpected error while backing up database
localhost - 2023-08-31 14:36:36 - 2 - a critical error occurred
localhost - 2023-08-31 14:36:36 - 1 - an alert level massage
localhost - 2023-08-31 14:36:36 - 0 - fatal error: exiting program

The default format is shown below:

 
machine - date - level - message

It only allows limited customization through the $format option:

 
Analog::$format = "machine=%s time=%s level=%s msg=%s\n";
Output
machine=localhost time=2023-08-31 15:14:06 level=7 msg=database query executed
machine=localhost time=2023-08-31 15:14:06 level=6 msg=user signed in

You can also convert log level numbers to names in the output through the LevelName handler as follows:

 
<?php

use Analog\Logger;
use Analog\Handler\EchoConsole;
use Analog\Handler\LevelName;

$logger = new Logger;

Analog::$format = "machine=%s time=%s level=%s msg=%s\n";

$logger->handler (LevelName::init (
  EchoConsole::init ()
));

. . .

This yields:

Output
machine=localhost time=2023-08-31 15:16:46 level=DEBUG msg=database query executed
machine=localhost time=2023-08-31 15:16:46 level=INFO msg=user signed in

Please see the examples directory on GitHub for more usage examples. Note that Analog doesn't appear to be actively maintained at the time of writing given its last commit in late 2021.

Final thoughts

In the PHP ecosystem, the spectrum of libraries for effective logging is somewhat narrower when juxtaposed with the offerings available in other programming languages. Monolog is pretty much the only standout option, and it's also the default solution in popular frameworks such as Laravel and Symfony. We recommend that read our dedicated Monolog guide to learn how to best set up a production-ready configuration in your projects.

Thanks for reading, and happy logging!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is the Head of Content at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he’s not writing or coding, he loves to travel, bike, and play tennis.
Centralize all your logs into one place.
Analyze, correlate and filter logs with SQL.
Create actionable
dashboards.
Share and comment with built-in collaboration.
Got an article suggestion? Let us know
Next article
How To Start Logging With MySQL
Learn how to start logging with MySQL and go from basics to best practices in no time.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.