nolikein / better-laravel-mattermost-logger
A more detailed mattermost logger for Laravel applications
Requires
- php: ^8.0
- monolog/monolog: ^3.3
- thibaud-dauce/laravel-mattermost-logger: ^1.8
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.16
- illuminate/database: ^10.38
- mockery/mockery: ^1.6
- pestphp/pest: ^3.5
- phpstan/phpstan: ^1.10
- romanzipp/php-cs-fixer-config: ^3.1
README
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);
});