nolikein/better-laravel-mattermost-logger

A more detailed mattermost logger for Laravel applications

2.3.0 2024-11-02 19:29 UTC

This package is auto-updated.

Last update: 2024-11-19 09:59:54 UTC


README

Packagist Version Downloads Pipeline Status Gitlab Code Coverage

This librairy extends the thibaud-dauce/laravel-mattermost-logger and allows a better customization of it. By the way, i found that add by default a table that dump the exception context is a good idea. But you can change the table or even reorder the whole message construction if it is what you want.

Installation

With Monolog 3 (latest)

You need to use composer to install the librairy.

composer require nolikein/better-laravel-mattermost-logger ^2.3

With Monolog 2

composer require nolikein/better-laravel-mattermost-logger ^1.1

This version won't be updated anymore.

Laravel commands

Then add the model migration:

artisan make:http-history-entry-migration http_history_entries

Finally, run migrations:

artisan migrate

How to log something

To make it works in your Laravel application, begin to change the config/logging.php file.

use Nolikein\BetterLaravelMattermostLogger\MattermostLogger;

'channels' => [
    // If your LOG_CHANNEL env variable is set to 'stack', so add 'mattermost' to the list of stack channels
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'mattermost'],
        'ignore_exceptions' => false,
    ],

    // And add this part
    'mattermost' => [
        'driver' => 'custom',
        'via' => MattermostLogger::class,
        'webhook' => env('MATTERMOST_WEBHOOK', 'PLEASE SET YOUR MATTERMOST WEBHOOK URL'),
        'channel' => 'channelShortName',
        'title' => "An exception occured.",
        'mentions' => ['@channel'],
    ],
],

Warning, the channel key MUST correspond to the short name of your mattermost channel. It means if you have a channel named "[ My channel ]" you must set "my channel" as channel name.

Then, as described in the channel upside, you must add the MATTERMOST_WEBHOOK environment variable and set as value your webhook link.

MATTERMOST_WEBHOOK=https://set-your-webhook-url-instead-of-me

Note, you can change the name of the env data, from the moment you know what you are doing :).

Options available

As the following link say, You can put options after the driver and via keys in your config/logging.php. See Base documentation. Below, the added option(s):

  • title (An exception occured): The log title

Store your requests and exceptions data

Since the model is retrieved from the Service Container as singleton, you create the model only one time per request. So, it's easy to get the same object in another place then storing it. To store the http entry, i propose you to use a middleware.

To generate our middleware, run the following command :

artisan make:http-history-middleware HttpHistoryMiddleware

Enable middleware

Laravel 10 and bellow

To enable to middleware in your application, open the App\Http\Kernel.php file and fill the $middleware array

protected $middleware = [
    // ...
    \App\Http\Middleware\HttpHistoryMiddleware::class,
];

Since Laravel 11

Open bootstrap/app.php and write:

->withMiddleware(function (Middleware $middleware) {
     $middleware->append(\App\Http\Middleware\HttpHistoryMiddleware::class);
})

Modding

This part can be a bit complicated, but i try my best to simplify it to you ! So please RTFM

Change the table content

The table content is an Eloquent model that the content is dumped as array. The best thing to do is to create a new model and add/remove/rename the property that you'd like to log !

First, create your custom model !

The easy way is to run the following command :

artisan make:http-history-entry-model HttpHistoryEntry

When HttpHistoryEntry is the name of the model to generate.

Overload your model in a Provider !

Inside the boot method of a Service Provider, add the following content :

use Nolikein\BetterLaravelMattermostLogger\Contracts\HttpHistoryEntryInterface;
use Illuminate\Foundation\Application;
use App\Models\MyCustomModel;
use Illuminate\Http\Request;

// Generate a new Http History entry that is the same for the current request
$this->app->singleton(HttpHistoryEntryInterface::class, function (Application $app, array $arguments) {
    return MyCustomModel::createFromRequest(Request::capture(), $arguments['exception']);
});

Change MyCustomModel by the class you just created.

Customize entirely the message content

I allows you a better customisation of the message content. But, what is and what is not a "message content" ? A message content is a class that Thibaud Dauce made and that i overload. The ThibaudDauce\MattermostLogger\Message class.

This class is by the way compatible with my librairy since i use the Mattermost sender that require any class that inherit from this class.

First step, create a custom Message class

Create a basic PHP class that extends the ThibaudDauce\MattermostLogger\Message class. Or start (extends) from my Nolikein\BetterLaravelMattermostLogger\MattermostMessage class.

After that, you should implements the Nolikein\BetterLaravelMattermostLogger\Contracts\HttpHistoryEntryInterface interface that force you to reimplement the fromArrayAndOptions method. In the librairy of Thibaud Dauce, this static method make a self instance, which force any extended class to redefine the method (see late static binding) because the self keywork reference the class that created the method and not the class that call the method. But if you extends my custom Message, you just have to return the parent method because i used static instead of self.

namespace App\Logging;

use Nolikein\BetterLaravelMattermostLogger\MattermostMessage as MessageWithTable;
use Nolikein\BetterLaravelMattermostLogger\Contracts\MattermostMessageInterface;

class CustomMattermostMessage extends MessageWithTable implements MattermostMessageInterface
{
    public static function fromArrayAndOptions($record, $options): \ThibaudDauce\Mattermost\Message
    {
        return parent::fromArrayAndOptions($record, $options);
    }
}

Then add your custom Message class in a provider !

Inside the boot method of a Service Provider, add the following content :

use Nolikein\BetterLaravelMattermostLogger\Contracts\MattermostMessageInterface;
use App\Logging\CustomMattermostMessage;

// Generate a new Mattermost message
$this->app->bind(MattermostMessageInterface::class, function (Application $app, array $arguments) {
    return CustomMattermostMessage::fromArrayAndOptions($arguments['record'], $arguments['options']);
});

Change CustomMattermostMessage by the class you just created.

FAQ

What if i have to use a Proxy

In your logging.php configuration, add the proxy option like bellow:

'mattermost' => [
    // ...
    'proxy' => [
        'http' => env('MATTERMOST_PROXY_URL'), // Use this proxy with "http"
        'https' => env('MATTERMOST_PROXY_URL'), // Use this proxy with "https",
        'no' => ['localhost'], // Don't use a proxy when the error is caused by localhost
    ],
],

Then add MATTERMOST_PROXY_URL in your .env file and set a url like http://localhost:8125.

Note, this configuration use the Guzzle proxy configuration. For more precisions, look the official documentation of Guzzle https://docs.guzzlephp.org/en/stable/request-options.html#proxy

What if i don't want to use a model to represent my http data ?

You can use any class that implements the HttpHistoryEntryInterface. This interface will give you the required method that your class must have to be used by the librairy.

How to specify the order for my attributes ?

As the model is a container, and behind the scene, an array, when you add a property by the createFromRequest method, the order you fill the properties defines the key order. So, if you'd like to show first the ip address then the user id, fill the model with the ip address property first then the user id property.

Why using a middleware when you could include it in the logger ?

Because this is not the role of a logger. A logger log a request somewhere, a middleware intercept a response then perform some actions from it.

Contributions

Thanks to Matrisis for the idea of registering requests, logging them to mattermost then storing them !

Thanks also to people that helped me to fix errors in this documentation.

Testing

Run tests with coverage

displayed as html in tests/Coverage directory

dcrun php ./vendor/bin/pest --coverage-html tests/Coverage
sudo chown $USER:$USER -R tests/Coverage

Creating your own tests

Everything is testable and for that, Mockable. You can be inspirated from existing content foundable in the gitlab repository.

We will take an example. We'd like to check if our custom Message works, but in an older world that would mean running Monolog. This is why we Mock it.

The following example use the Pest testing Framework:

use App\Logging\MattermostMessage;
use App\Models\HttpHistory;
use Illuminate\Config\Repository;
use Illuminate\Http\Request;
use Nolikein\BetterLaravelMattermostLogger\Contracts\HttpHistoryEntryInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use ThibaudDauce\Mattermost\Message as LowLevelMessage;

it('can create a message', function (string $url): void {

    /** @var \Mockery\MockInterface&Repository $config */
    $config = Mockery::mock(Repository::class);
    $config->shouldReceive('get')->with('app.env', 'local')->andReturn('local');
    $config->shouldReceive('get')->with('app.url')->andReturn($url);

    /** @var \Mockery\MockInterface&HttpHistoryEntryInterface $entry */
    $entry = Mockery::mock(HttpHistoryEntryInterface::class);
    $entry->shouldReceive('toArray')->andReturn(['hello' => 'world']);

    $message = MattermostMessage::fromArrayAndOptions(
        record: [
            'level_name' => 'ERROR',
            'message' => 'world',
            'level' => 'ERROR',
        ],
        options: [
            'mentions' => ['@me'],
            'channel' => 'hello',
            'username' => 'hello',
            'icon_url' => null,
            'title' => 'Title',
            'level_mention' => 'ERROR',
        ],
        config: $config,
        entry: $entry
    );

    expect($message)->toBeInstanceOf(LowLevelMessage::class);
})->with([
    'http://localhost',
    'http://staging.com',
]);

it('can create model without exception', function (): void {
    /** @var \Mockery\MockInterface&Request $request */
    $request = Mockery::mock(Request::class);
    $request->shouldReceive('method', 'getUri', 'route', 'header', 'ip', 'fullUrl', 'url')->andReturn('');

    $model = HttpHistory::createFromRequest(
        request: $request,
        exception: null,
        environment: 'production',
        userId: 0
    );

    expect($model)->toBeInstanceOf(HttpHistory::class);
});

it('can create model with exception', function (): void {
    /** @var \Mockery\MockInterface&Request $request */
    $request = Mockery::mock(Request::class);
    $request->shouldReceive('method', 'getUri', 'route', 'header', 'ip', 'fullUrl', 'url')->andReturn('');

    $model = HttpHistory::createFromRequest(
        request: $request,
        exception: new HttpException(500, 'test'),
        environment: 'production',
        userId: 0
    );

    expect($model)->toBeInstanceOf(HttpHistory::class);
});

Licence

MIT