Creating a Laravel logger - Part 2: Creating a Pusher logger package
For this part of the series, you will need PHP 7.13+, Laravel 5.7+ and Composer installed on your machine.
In this tutorial, we will build a Laravel package from scratch to help us dispatch logs to Pusher. All logs will be sent to Pusher Channels but error logs will be sent both to Pusher Channels and Pusher Beams. Let’s dig in!
This is the second part of our six-part series on building a logging monitoring system. In the first part, we created the shell for the Laravel application. The application has the UI that will help us manually dispatch logs.
Requirements
To follow along with this series you need the following things:
- Completed previous parts of the series.
- Laravel installed on your local machine. Installation guide.
- Knowledge of PHP and the Laravel framework.
- Composer installed on your local machine. Installation guide.
- Android Studio >= 3.x installed on your machine (If you are building for Android).
- Knowledge of Kotlin and the Android Studio IDE.
- Xcode >= 10.x installed on your machine (If you are building for iOS).
- Knowledge of the Swift programming language and the Xcode IDE.
Creating our Laravel package
The first thing we will do is create a new folder to store the package. To do this cd
to the project you started from the first part and create a new packages
folder by running this on your terminal:
$ mkdir packages
This creates a new folder named packages
. Next, we will create the main package folder based on the name of our package. Our package will be called PusherLogger
. Create the folder like this:
$ cd packages
$ mkdir PusherLogger
After creating the folder, we will start adding files to the folder. The first file we need is the composer.json
file. This file will contain information about our package like the name, description, dependencies, and other properties. To generate this file, cd
to the PusherLogger
directory and run this command:
$ composer init
This initiates the composer config generator that will request information about your package. Follow the wizard to provide information about your package. At the end of it, your composer.json
file should look similar to this:
{
"name": "package/pusher-logger",
"description": "A package to distribute logs to Pusher",
"type": "library",
"authors": [
{
"name": "Neo Ighodaro",
"email": "neo@creativitykills.co"
}
],
"require": {}
}
You should change
package
in thename
property to your own name.
Next, we will add dependencies needed for package. We will add it to the the require
object of the composer.json
file. Add the dependencies like so:
{
// [...]
"require": {
"php": ">=7.1.3",
"illuminate/support": "~5",
"monolog/monolog": "^1.24.0",
"pusher/pusher-php-server": "^3.2",
"pusher/pusher-push-notifications": "^1.0"
}
}
For this package, we require PHP 7.1.3
and up, the Pusher channel package, and Pusher push notifications package to help us broadcast the logs to Pusher.
Next, let’s instruct the package where it should load the files from. Add the snippet below to the composer.json
file:
{
// [...]
"autoload": {
"psr-4": {
"PackageNamespace\\PusherLogger\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PackageNamespace\\PusherLogger\\Tests\\": "tests"
}
},
"extra": {
"laravel": {
"providers": [
"PackageNamespace\\PusherLogger\\PusherLoggerServiceProvider"
],
"aliases": {
"PusherLogger": "PackageNamespace\\PusherLogger\\PusherLogger"
}
}
}
}
You can use a different camelcase namespace from
PackageNamespace
if you wish. Just remember to replace the namespace everywhere you changed it below.
Now we have instructed Composer’s autoloader how to load files from a certain namespace. This tells the package to look out for the /src
directory for the package files. This directory is not available yet so create the folder in your PusherLogger
folder. You can do that by running this command:
$ mkdir src
Navigate to the src
folder and create two files. First we will create the PusherLoggerServiceProvider.php
file and paste the following into it:
<?php // File: ./src/PusherLoggerServiceProvider.php
namespace PackageNamespace\PusherLogger;
use Pusher\Pusher;
use Illuminate\Support\ServiceProvider;
use Pusher\PushNotifications\PushNotifications;
class PusherLoggerServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
//
}
}
You can replace
PackageNamespace
with your own namespace.
Service providers are the central place of all Laravel application bootstrapping. Your own application, as well as all of Laravel’s core services are bootstrapped via service providers. - Laravel documentation
This class extends the Illuminate\Support\ServiceProvider
class. Our class contains two methods - register
and boot
. The boot
method loads event listeners, routes, or any other piece of functionality while the register
method only bind things into the service container.
Inside a service provider class, the app
container can be accessed via the $app
property. So in our PusherLoggerServiceProvider
class we will bind an alias pusher-logger
to the PusherLogger
class. Update the register
method like this:
<?php // File: ./src/PusherLoggerServiceProvider.php
// [...]
class PusherLoggerServiceProvider extends ServiceProvider
{
// [...]
public function register()
{
$this->app->bind('pusher-logger', function () {
$config = config('broadcasting.connections.pusher');
$key = $config['key'] ?? '';
$secret = $config['secret'] ?? '';
$app_id = $config['app_id'] ?? '';
$pusher = new Pusher($key, $secret, $app_id, [
'useTLS' => true,
'encrypted' => true,
'cluster' => $config\['options'\]['cluster'] ?? '',
]);
$beams = new PushNotifications([
'secretKey' => $config\['beams'\]['secret_key'] ?? '',
'instanceId' => $config\['beams'\]['instance_id'] ?? '',
]);
return new PusherLogger($pusher, $beams);
});
}
}
Above, we are binding pusher-logger
to the Closure
above. Inside the Closure
, we are registering an instance of a PusherLogger
class, which we will create later. This class receives an instance of a configured Pusher
object, and a configured PushNotifications
object. Since we are using Laravel’s service container, it means anytime we try to use the pusher-logger
service, we will get a PusherLogger
instance with both Pusher and Push Notifications configured.
Next, let us create our second class. The class will be a Facade. A Facade is one of the architecture concepts Laravel provides. It is a static interface to classes that are available in the application’s service container, meaning that our Facade classes represent another class bound in the service container.
To create this class, first make a directory named Facades
in the src
directory and then create the PusherLogger.php
file inside it. When you have created the file, paste the following code into the file:
<?php // File: ./src/Facades/PusherLogger.php
namespace PackageNamespace\PusherLogger\Facades;
use Illuminate\Support\Facades\Facade;
class PusherLogger extends Facade
{
protected static function getFacadeAccessor()
{
return 'pusher-logger';
}
}
In the getFacadeAccessor
method of the class above, we returned pusher-logger
which corresponds to the alias we bound to the PusherLogger
class earlier in the service provider.
Now we can use the PusherLogger
Facade as a proxy to the original PusherLogger
class with the package logic. Let’s create the original PusherLogger
class. In the src
directory, create a new file named PusherLogger.php
and paste the following code into it:
<?php // File: ./src/PusherLogger.php
namespace PackageNamespace\PusherLogger;
use Pusher\Pusher;
use Pusher\PushNotifications\PushNotifications;
class PusherLogger
{
/**
* @var \Pusher\Pusher
*/
protected $pusher;
/**
* @var \Pusher\PushNotifications\PushNotifications
*/
protected $beams;
/**
* @var string
*/
protected $event;
/**
* @var string
*/
protected $channel;
/**
* @var string
*/
protected $message;
/**
* @var string
*/
protected $level;
/**
* @var array
*/
protected $interests = [];
// Log levels
const LEVEL_INFO = 'info';
const LEVEL_DEBUG = 'debug';
const LEVEL_ERROR = 'error';
/**
* PusherLogger constructor.
*
* @param \Pusher\Pusher $pusher
* @param \Pusher\PushNotifications\PushNotifications $beams
*/
public function __construct(Pusher $pusher, PushNotifications $beams)
{
$this->beams = $beams;
$this->pusher = $pusher;
}
}
In the snippet above, we declared some variables we will use later in the class. We also declared the class constructor to receive instances of the Pusher
object and the PushNotifications
object just as we did in the service provider binding above.
We also have some properties and constants for the class. We can use the constants outside the class when specifying the log level. This would make it easy to change the values later on if we wanted to.
In the same class, let’s define a some methods, which will be how we will set the other protected
class properties. In the same file, paste the following code:
// File: ./src/PusherLogger.php
// [...]
/**
* Sets the log message.
*
* @param string $message
* @return self
*/
public function setMessage(string $message): self
{
$this->message = $message;
return $this;
}
/**
* Sets the log level.
*
* @param string $level
* @return self
*/
public function setLevel(string $level): self
{
$this->level = $level;
return $this;
}
/**
* Sets the Pusher channel.
*
* @param string $channel
* @return self
*/
public function setChannel(string $channel): self
{
$this->channel = $channel;
return $this;
}
/**
* Sets the event name.
*
* @param string $event
* @return self
*/
public function setEvent(string $event): self
{
$this->event = $event;
return $this;
}
/**
* Set the interests for Push notifications.
*
* @param array $interests
* @return self
*/
public function setInterests(array $interests): self
{
$this->interests = $interests;
return $this;
}
// [...]
Above, we have defined some similar methods. They just set the corresponding protected
class properties and then return the class instance so they are chainable.
Next, we will add other helper methods to be used in the package. Add the following methods to the PusherLogger.php
class:
// File: ./src/PusherLogger.php
// [...]
/**
* Quickly log a message.
*
* @param string $message
* @param string $level
* @return self
*/
public static function log(string $message, string $level): self
{
return app('pusher-logger')
->setMessage($message)
->setLevel($level);
}
/**
* Dispatch a log message.
*
* @return bool
*/
public function send(): bool
{
$this->pusher->trigger($this->channel, $this->event, $this->toPushHttp());
if ($this->level === static::LEVEL_ERROR) {
$this->beams->publishToInterests($this->interests, $this->toPushBeam());
}
return true;
}
// [...]
The first method is a quick shorthand we can use when dispatching log messages and the second method is the method that dispatches log messages to the Pusher clients. In the send
method, we are checking to see if the log level is an error level. If it is, we will also send a push notification so the administrator can be aware that an error has occurred.
When creating a log, we need to set the channel, events and interests (when using Pusher Beams) in which the log would be sent to. Here’s an example of how we can use the logger:
use PackageNamespace\PusherLogger\PusherLogger;
PusherLogger::log('Winter is Coming', PusherLogger::LEVEL_WARNING)
->setEvent('log-event')
->setChannel('log-channel')
->setInterests(['log-interest'])
->send()
The final function in the snippet is the function that sends the data to Pusher. In the function, all logs are sent to Pusher Channels, but error logs are also sent to Pusher Beams so that the client can receive a notification.
While defining the send
function, we used two new methods that composes the data to be sent to Pusher Channels and Pusher Beams respectively. Add the methods to the same class like so:
// File: ./src/PusherLogger.php
// [...]
public function toPushHttp()
{
return [
'title' => 'PusherLogger' . ' '. ucwords($this->level),
'message' => $this->message,
'loglevel' => $this->level
];
}
public function toPushBeam()
{
return [
'apns' => [
'aps' => [
'alert' => [
'title' => 'PusherLogger' . ' '. ucwords($this->level),
'body' => $this->message,
'loglevel' => $this->level
],
],
],
'fcm' => [
'notification' => [
'title' => 'PusherLogger' . ' '. ucwords($this->level),
'body' => $this->message,
'loglevel' => $this->level
],
],
];
}
Creating a log handler
Laravel uses Monolog, which is a powerful logging package for PHP. We can create custom handlers for Monolog and so let’s do one that will be for our Pusher logger.
Create a new file in the src
directory of the package called PusherLoggerHandler.php
and paste the following code:
<?php // File: ./src/PusherLoggerHandler.php
namespace PackageNamespace\PusherLogger;
use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
class PusherLoggerHandler extends AbstractProcessingHandler
{
protected function write(array $record): void
{
$level = strtolower(Logger::getLevelName($record['level']));
PusherLogger::log($record['message'], $level)
->setEvent('log-event')
->setChannel('log-channel')
->setInterests(['log-interest'])
->send();
}
}
Above, we have the custom handler that will be hooked into our Laravel Monolog instance. When we do, logs will be automatically pushed to our Pusher application as needed. We will do that in the next part.
That’s all.
Conclusion
In this part of the series, we have been able to set up the logic we need to be able to push logs to Pusher. In the next part of the series, we will integrate this package with our Laravel application and see how everything will work together.
The source code is available on GitHub.
21 March 2019
by Neo Ighodaro