Creating a Laravel logger - Part 2: Creating a Pusher logger package

Introduction

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:

1$ cd packages
2    $ 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:

1{
2        "name": "package/pusher-logger",
3        "description": "A package to distribute logs to Pusher",
4        "type": "library",
5        "authors": [
6            {
7                "name": "Neo Ighodaro",
8                "email": "neo@creativitykills.co"
9            }
10        ],
11        "require": {}
12    }

You should change package in the name 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:

1{
2        // [...]
3        
4        "require": {
5          "php": ">=7.1.3",
6          "illuminate/support": "~5",
7          "monolog/monolog": "^1.24.0",
8          "pusher/pusher-php-server": "^3.2",
9          "pusher/pusher-push-notifications": "^1.0"
10        }
11    }

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:

1{
2        // [...]
3        
4        "autoload": {
5            "psr-4": {
6                "PackageNamespace\\PusherLogger\\": "src/"
7            }
8        },
9        "autoload-dev": {
10            "psr-4": {
11                "PackageNamespace\\PusherLogger\\Tests\\": "tests"
12            }
13        },
14        "extra": {
15            "laravel": {
16                "providers": [
17                    "PackageNamespace\\PusherLogger\\PusherLoggerServiceProvider"
18                ],
19                "aliases": {
20                    "PusherLogger": "PackageNamespace\\PusherLogger\\PusherLogger"
21                }
22            }
23        }
24    }

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:

1<?php // File: ./src/PusherLoggerServiceProvider.php
2    
3    namespace PackageNamespace\PusherLogger;
4    
5    use Pusher\Pusher;
6    use Illuminate\Support\ServiceProvider;
7    use Pusher\PushNotifications\PushNotifications;
8    
9    class PusherLoggerServiceProvider extends ServiceProvider
10    {
11        /**
12         * Bootstrap the application services.
13         *
14         * @return void
15         */
16        public function boot()
17        {
18          //
19        }
20        
21        /**
22         * Register the application services.
23         *
24         * @return void
25         */
26        public function register()
27        {
28          //
29        }
30    }

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:

1<?php // File: ./src/PusherLoggerServiceProvider.php
2    
3    // [...]
4    
5    class PusherLoggerServiceProvider extends ServiceProvider
6    {
7      // [...]
8      
9      public function register()
10      {
11          $this->app->bind('pusher-logger', function () {
12              $config = config('broadcasting.connections.pusher');
13              
14              $key = $config['key'] ?? '';
15              $secret = $config['secret'] ?? '';
16              $app_id = $config['app_id'] ?? '';
17              
18              $pusher = new Pusher($key, $secret, $app_id, [
19                  'useTLS' => true,
20                  'encrypted' => true,
21                  'cluster' => $config\['options'\]['cluster'] ?? '',
22              ]);
23              
24              $beams = new PushNotifications([
25                  'secretKey' => $config\['beams'\]['secret_key'] ?? '',
26                  'instanceId' => $config\['beams'\]['instance_id'] ?? '',
27              ]);
28              
29              return new PusherLogger($pusher, $beams);
30          });
31      }
32    }

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:

1<?php // File: ./src/Facades/PusherLogger.php
2    
3    namespace PackageNamespace\PusherLogger\Facades;
4    
5    use Illuminate\Support\Facades\Facade;
6    
7    class PusherLogger extends Facade
8    {
9        protected static function getFacadeAccessor()
10        {
11            return 'pusher-logger';
12        }
13    }

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:

1<?php // File: ./src/PusherLogger.php
2    
3    namespace PackageNamespace\PusherLogger;
4    
5    use Pusher\Pusher;
6    use Pusher\PushNotifications\PushNotifications;
7    
8    class PusherLogger
9    {
10        /**
11         * @var \Pusher\Pusher
12         */
13        protected $pusher;
14        
15        /**
16         * @var \Pusher\PushNotifications\PushNotifications
17         */
18        protected $beams;
19        
20        /**
21         * @var string
22         */
23        protected $event;
24    
25        /**
26         * @var string
27         */
28        protected $channel;
29    
30        /**
31         * @var string
32         */
33        protected $message;
34    
35        /**
36         * @var string
37         */
38        protected $level;
39    
40        /**
41         * @var array
42         */
43        protected $interests = [];
44    
45        // Log levels
46        const LEVEL_INFO  = 'info';
47        const LEVEL_DEBUG = 'debug';
48        const LEVEL_ERROR = 'error';
49    
50        /**
51         * PusherLogger constructor.
52         *
53         * @param \Pusher\Pusher $pusher
54         * @param \Pusher\PushNotifications\PushNotifications $beams
55         */
56        public function __construct(Pusher $pusher, PushNotifications $beams)
57        {
58            $this->beams = $beams;
59            
60            $this->pusher = $pusher;
61        }
62    }

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:

1// File: ./src/PusherLogger.php
2    // [...]
3    
4    /**
5     * Sets the log message.
6     *
7     * @param  string $message
8     * @return self
9     */
10    public function setMessage(string $message): self
11    {
12        $this->message = $message;
13        
14        return $this;
15    }
16    
17    /**
18     * Sets the log level.
19     *
20     * @param  string $level
21     * @return self
22     */
23    public function setLevel(string $level): self
24    {
25        $this->level = $level;
26        
27        return $this;
28    }
29    
30    /**
31     * Sets the Pusher channel.
32     *
33     * @param  string $channel
34     * @return self
35     */
36    public function setChannel(string $channel): self
37    {
38        $this->channel = $channel;
39    
40        return $this;
41    }
42    
43    /**
44     * Sets the event name.
45     *
46     * @param  string $event
47     * @return self
48     */
49    public function setEvent(string $event): self
50    {
51        $this->event = $event;
52    
53        return $this;
54    }
55    
56    /**
57     * Set the interests for Push notifications.
58     *
59     * @param  array $interests
60     * @return self
61     */
62    public function setInterests(array $interests): self
63    {
64        $this->interests = $interests;
65    
66        return $this;
67    }
68    
69    // [...]

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:

1// File: ./src/PusherLogger.php
2    // [...]
3    
4    /**
5     * Quickly log a message.
6     *
7     * @param string $message
8     * @param string $level
9     * @return self
10     */
11    public static function log(string $message, string $level): self
12    {
13        return app('pusher-logger')
14            ->setMessage($message)
15            ->setLevel($level);
16    }
17    
18    /**
19     * Dispatch a log message.
20     *
21     * @return bool
22     */
23    public function send(): bool
24    {
25        $this->pusher->trigger($this->channel, $this->event, $this->toPushHttp());
26        
27        if ($this->level === static::LEVEL_ERROR) {
28            $this->beams->publishToInterests($this->interests, $this->toPushBeam());
29        }
30    
31        return true;
32    }
33    
34    // [...]

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:

1use PackageNamespace\PusherLogger\PusherLogger;
2    
3    PusherLogger::log('Winter is Coming', PusherLogger::LEVEL_WARNING)
4            ->setEvent('log-event')
5            ->setChannel('log-channel')
6            ->setInterests(['log-interest'])
7            ->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:

1// File: ./src/PusherLogger.php
2    // [...]
3    
4    public function toPushHttp()
5     {
6         return [
7             'title' => 'PusherLogger' . ' '. ucwords($this->level),
8             'message' => $this->message,
9             'loglevel' => $this->level
10         ];
11     }
12    
13     public function toPushBeam()
14     {
15         return [
16             'apns' => [
17                 'aps' => [
18                     'alert' => [
19                         'title' => 'PusherLogger' . ' '. ucwords($this->level),
20                         'body' => $this->message,
21                         'loglevel' => $this->level
22                     ],
23                 ],
24             ],
25             'fcm' => [
26                 'notification' => [
27                     'title' => 'PusherLogger' . ' '. ucwords($this->level),
28                     'body' => $this->message,
29                     'loglevel' => $this->level
30                 ],
31             ],
32         ];
33    }

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:

1<?php // File: ./src/PusherLoggerHandler.php
2    
3    namespace PackageNamespace\PusherLogger;
4    
5    use Monolog\Logger;
6    use Monolog\Handler\AbstractProcessingHandler;
7    
8    class PusherLoggerHandler extends AbstractProcessingHandler
9    {
10        protected function write(array $record): void
11        {
12            $level = strtolower(Logger::getLevelName($record['level']));
13    
14            PusherLogger::log($record['message'], $level)
15                ->setEvent('log-event')
16                ->setChannel('log-channel')
17                ->setInterests(['log-interest'])
18                ->send();
19        }
20    }

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.