First init
This commit is contained in:
92
README.md
Normal file
92
README.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# herrleinIT Log Handler
|
||||||
|
|
||||||
|
This Laravel 12 package captures every log record emitted by your application in real time and forwards it to an external HTTP endpoint while optionally continuing to write to existing log channels (e.g. `single`, `daily`).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Pushes every Monolog record to a configurable HTTP endpoint using Bearer token authentication.
|
||||||
|
- Installs as a stackable log channel so you can combine it with your existing file / daily / slack channels.
|
||||||
|
- Publishes a configuration file with sensible defaults and `.env` overrides for endpoint, token, timeouts, retries, and trace inclusion.
|
||||||
|
- Provides PHPUnit feature coverage to ensure integration behaviour within a Laravel 12 application.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Assuming the package is required locally via Composer path repository (as in this project):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require herrleinit/loghandler:@dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Laravel will auto-discover the `LogHandlerServiceProvider`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. **Publish the config** (optional, but helpful for tweaking in code):
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=loghandler-config
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Environment variables** – add these to your `.env` (defaults shown):
|
||||||
|
```env
|
||||||
|
LOGHANDLER_ENDPOINT=http://test.example/api/error/create
|
||||||
|
LOGHANDLER_TOKEN=your-bearer-token
|
||||||
|
LOGHANDLER_SOURCE="${APP_NAME}"
|
||||||
|
LOGHANDLER_ENABLED=true
|
||||||
|
LOGHANDLER_TIMEOUT_MS=3000
|
||||||
|
LOGHANDLER_RETRY_TIMES=0
|
||||||
|
LOGHANDLER_INCLUDE_TRACE=false
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Logging stack** – ensure the `loghandler` channel participates in your default stack:
|
||||||
|
```env
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single,loghandler
|
||||||
|
```
|
||||||
|
This keeps the usual `storage/logs/laravel.log` file while forwarding every entry to the remote API. You can swap `single` for `daily` or any other channels you need.
|
||||||
|
|
||||||
|
4. **Config cache** – whenever you change logging env values, clear cached configuration:
|
||||||
|
```bash
|
||||||
|
php artisan config:clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Behaviour
|
||||||
|
- The package registers a custom Monolog handler through `Log::extend('loghandler', ...)` so it can be part of any stack or used standalone via `Log::channel('loghandler')`.
|
||||||
|
- When enabled and provided with a token + endpoint, each `LogRecord` is transformed into the required JSON payload:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source": "Project Name",
|
||||||
|
"error": "Stack error",
|
||||||
|
"type": "E_WARNING",
|
||||||
|
"file": "/var/test/testfile.php",
|
||||||
|
"line": "1337",
|
||||||
|
"trace": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Retries are configurable via `retry_times`, and `include_trace` toggles stack traces for non-exception logs.
|
||||||
|
|
||||||
|
## Verifying Connectivity
|
||||||
|
To confirm logs reach your endpoint from a given environment, run:
|
||||||
|
```bash
|
||||||
|
php artisan tinker
|
||||||
|
>>> Log::error('LogHandler connectivity check', ['file' => __FILE__, 'line' => __LINE__]);
|
||||||
|
```
|
||||||
|
If your stack includes `loghandler`, that call will POST to the configured endpoint. Inspect the remote API or monitor network traffic to verify delivery.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
The package ships with PHPUnit feature tests (`tests/Feature/LogForwardingTest.php`) built on [Orchestra Testbench](https://github.com/orchestral/testbench) to verify:
|
||||||
|
- Exception payload forwarding
|
||||||
|
- Disabled / missing token branches
|
||||||
|
- Delivery via the default `stack` channel
|
||||||
|
|
||||||
|
From inside the package directory you can execute:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
vendor/bin/phpunit -c phpunit.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
When the package is installed inside a Laravel application (like this repo), you can also keep using the host app's suite:
|
||||||
|
```bash
|
||||||
|
php artisan test --filter=LogForwardingTest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
Need a different logging topology or additional telemetry fields? Extend `LogForwarder` or create another handler and register it alongside this package.
|
||||||
39
composer.json
Normal file
39
composer.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "herrleinit/loghandler",
|
||||||
|
"description": "Intercept Laravel log entries and forward them to an external API.",
|
||||||
|
"type": "library",
|
||||||
|
"license": "proprietary",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "herrleinIT",
|
||||||
|
"email": "info@herrlein.it",
|
||||||
|
"homepage": "https://www.herrlein.it/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"illuminate/support": "^12.0",
|
||||||
|
"illuminate/log": "^12.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"HerrleinIT\\LogHandler\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"HerrleinIT\\LogHandler\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"orchestra/testbench": "^9.0"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"HerrleinIT\\LogHandler\\LogHandlerServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
config/loghandler.php
Normal file
11
config/loghandler.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'endpoint' => env('LOGHANDLER_ENDPOINT', 'http://test.example/api/error/create'),
|
||||||
|
'token' => env('LOGHANDLER_TOKEN', ''),
|
||||||
|
'source' => env('LOGHANDLER_SOURCE', env('APP_NAME', 'Laravel')),
|
||||||
|
'enabled' => env('LOGHANDLER_ENABLED', true),
|
||||||
|
'timeout_ms' => env('LOGHANDLER_TIMEOUT_MS', 3000),
|
||||||
|
'retry_times' => env('LOGHANDLER_RETRY_TIMES', 0),
|
||||||
|
'include_trace' => env('LOGHANDLER_INCLUDE_TRACE', false),
|
||||||
|
];
|
||||||
12
phpunit.xml
Normal file
12
phpunit.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
cacheResult="false">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="LogHandler Package Test Suite">
|
||||||
|
<directory suffix="Test.php">tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
24
src/Handlers/LogForwardingHandler.php
Normal file
24
src/Handlers/LogForwardingHandler.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace HerrleinIT\LogHandler\Handlers;
|
||||||
|
|
||||||
|
use HerrleinIT\LogHandler\LogForwarder;
|
||||||
|
use Monolog\Handler\AbstractProcessingHandler;
|
||||||
|
use Monolog\Level;
|
||||||
|
use Monolog\LogRecord;
|
||||||
|
|
||||||
|
class LogForwardingHandler extends AbstractProcessingHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly LogForwarder $forwarder,
|
||||||
|
Level $level = Level::Debug,
|
||||||
|
bool $bubble = true,
|
||||||
|
) {
|
||||||
|
parent::__construct($level, $bubble);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function write(LogRecord $record): void
|
||||||
|
{
|
||||||
|
$this->forwarder->forward($record);
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/LogForwarder.php
Normal file
144
src/LogForwarder.php
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace HerrleinIT\LogHandler;
|
||||||
|
|
||||||
|
use ErrorException;
|
||||||
|
use GuzzleHttp\ClientInterface;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Illuminate\Contracts\Config\Repository;
|
||||||
|
use Monolog\LogRecord;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class LogForwarder
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Repository $config,
|
||||||
|
private readonly ClientInterface $httpClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function forward(LogRecord $record): void
|
||||||
|
{
|
||||||
|
$settings = $this->config->get('loghandler', []);
|
||||||
|
|
||||||
|
if (! ($settings['enabled'] ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = (string) ($settings['endpoint'] ?? '');
|
||||||
|
$token = (string) ($settings['token'] ?? '');
|
||||||
|
|
||||||
|
if ($endpoint === '' || $token === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->buildPayload($record, $settings);
|
||||||
|
$retryTimes = (int) ($settings['retry_times'] ?? 0);
|
||||||
|
|
||||||
|
for ($attempt = 0; $attempt <= $retryTimes; $attempt++) {
|
||||||
|
try {
|
||||||
|
$this->httpClient->request('POST', $endpoint, [
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer '.$token,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
],
|
||||||
|
'json' => $payload,
|
||||||
|
]);
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (GuzzleException|Throwable) {
|
||||||
|
if ($attempt === $retryTimes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildPayload(LogRecord $record, array $settings): array
|
||||||
|
{
|
||||||
|
$exception = $this->resolveThrowable($record);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'source' => (string) ($settings['source'] ?? 'Laravel'),
|
||||||
|
'error' => $exception instanceof Throwable ? $exception->getMessage() : (string) $record->message,
|
||||||
|
'type' => $this->resolveType($record, $exception),
|
||||||
|
'file' => $this->resolveFile($exception, $record),
|
||||||
|
'line' => $this->resolveLine($exception, $record),
|
||||||
|
'trace' => $this->resolveTrace($exception, $record, (bool) ($settings['include_trace'] ?? false)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveThrowable(LogRecord $record): ?Throwable
|
||||||
|
{
|
||||||
|
$context = $record->context;
|
||||||
|
$exception = $context['exception'] ?? null;
|
||||||
|
|
||||||
|
return $exception instanceof Throwable ? $exception : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const ERROR_LEVEL_MAP = [
|
||||||
|
E_ERROR => 'E_ERROR',
|
||||||
|
E_WARNING => 'E_WARNING',
|
||||||
|
E_PARSE => 'E_PARSE',
|
||||||
|
E_NOTICE => 'E_NOTICE',
|
||||||
|
E_CORE_ERROR => 'E_CORE_ERROR',
|
||||||
|
E_CORE_WARNING => 'E_CORE_WARNING',
|
||||||
|
E_COMPILE_ERROR => 'E_COMPILE_ERROR',
|
||||||
|
E_COMPILE_WARNING => 'E_COMPILE_WARNING',
|
||||||
|
E_USER_ERROR => 'E_USER_ERROR',
|
||||||
|
E_USER_WARNING => 'E_USER_WARNING',
|
||||||
|
E_USER_NOTICE => 'E_USER_NOTICE',
|
||||||
|
E_STRICT => 'E_STRICT',
|
||||||
|
E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
|
||||||
|
E_DEPRECATED => 'E_DEPRECATED',
|
||||||
|
E_USER_DEPRECATED => 'E_USER_DEPRECATED',
|
||||||
|
];
|
||||||
|
|
||||||
|
private function resolveType(LogRecord $record, ?Throwable $exception): string
|
||||||
|
{
|
||||||
|
if ($exception instanceof ErrorException) {
|
||||||
|
return self::ERROR_LEVEL_MAP[$exception->getSeverity()] ?? 'ErrorException';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exception instanceof Throwable) {
|
||||||
|
return $exception::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'E_'.strtoupper($record->level->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveFile(?Throwable $exception, LogRecord $record): string
|
||||||
|
{
|
||||||
|
if ($exception instanceof Throwable) {
|
||||||
|
return $exception->getFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) ($record->context['file'] ?? 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveLine(?Throwable $exception, LogRecord $record): string
|
||||||
|
{
|
||||||
|
if ($exception instanceof Throwable) {
|
||||||
|
return (string) $exception->getLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($record->context['line'])) {
|
||||||
|
return (string) $record->context['line'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveTrace(?Throwable $exception, LogRecord $record, bool $includeTrace): string
|
||||||
|
{
|
||||||
|
if (! $includeTrace) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exception instanceof Throwable) {
|
||||||
|
return $exception->getTraceAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) ($record->context['trace'] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/LogHandlerServiceProvider.php
Normal file
63
src/LogHandlerServiceProvider.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace HerrleinIT\LogHandler;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use HerrleinIT\LogHandler\Handlers\LogForwardingHandler;
|
||||||
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Monolog\Level;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class LogHandlerServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(__DIR__.'/../config/loghandler.php', 'loghandler');
|
||||||
|
|
||||||
|
$this->app->singleton(LogForwarder::class, function (Application $app): LogForwarder {
|
||||||
|
$settings = $app['config']->get('loghandler', []);
|
||||||
|
$timeoutSeconds = max(0.0, ((int) ($settings['timeout_ms'] ?? 3000)) / 1000);
|
||||||
|
|
||||||
|
$client = new Client([
|
||||||
|
'timeout' => $timeoutSeconds,
|
||||||
|
'connect_timeout' => $timeoutSeconds,
|
||||||
|
'http_errors' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new LogForwarder($app['config'], $client);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__.'/../config/loghandler.php' => $this->app->configPath('loghandler.php'),
|
||||||
|
], 'loghandler-config');
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::extend('loghandler', function (Application $app, array $config = []): Logger {
|
||||||
|
$logger = new Logger($config['name'] ?? 'loghandler');
|
||||||
|
$levelOption = $config['level'] ?? null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$level = $levelOption === null
|
||||||
|
? Level::Debug
|
||||||
|
: Level::fromName(strtoupper((string) $levelOption));
|
||||||
|
} catch (Throwable) {
|
||||||
|
$level = Level::Debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger->pushHandler(new LogForwardingHandler(
|
||||||
|
$app->make(LogForwarder::class),
|
||||||
|
$level,
|
||||||
|
! isset($config['bubble']) || (bool) $config['bubble'],
|
||||||
|
));
|
||||||
|
|
||||||
|
return $logger;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
83
tests/Fakes/FakeLoghandlerClient.php
Normal file
83
tests/Fakes/FakeLoghandlerClient.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace HerrleinIT\LogHandler\Tests\Fakes;
|
||||||
|
|
||||||
|
use BadMethodCallException;
|
||||||
|
use GuzzleHttp\ClientInterface;
|
||||||
|
use GuzzleHttp\Promise\FulfilledPromise;
|
||||||
|
use GuzzleHttp\Promise\PromiseInterface;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use Psr\Http\Message\RequestInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class FakeLoghandlerClient implements ClientInterface
|
||||||
|
{
|
||||||
|
/** @var array<int, array{method:string,uri:string,options:array}> */
|
||||||
|
public array $requests = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, ResponseInterface|Throwable|callable> $queue
|
||||||
|
*/
|
||||||
|
public function __construct(private array $queue = []) {}
|
||||||
|
|
||||||
|
public function request(string $method, $uri, array $options = []): ResponseInterface
|
||||||
|
{
|
||||||
|
$this->requests[] = [
|
||||||
|
'method' => strtoupper($method),
|
||||||
|
'uri' => (string) $uri,
|
||||||
|
'options' => $options,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->queue !== []) {
|
||||||
|
$next = array_shift($this->queue);
|
||||||
|
|
||||||
|
if ($next instanceof Throwable) {
|
||||||
|
throw $next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($next)) {
|
||||||
|
$result = $next($this, $method, $uri, $options);
|
||||||
|
|
||||||
|
if ($result instanceof ResponseInterface) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($next instanceof ResponseInterface) {
|
||||||
|
return $next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requestAsync($method, $uri, array $options = []): PromiseInterface
|
||||||
|
{
|
||||||
|
return new FulfilledPromise($this->request($method, $uri, $options));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(RequestInterface $request, array $options = []): ResponseInterface
|
||||||
|
{
|
||||||
|
return $this->request($request->getMethod(), $request->getUri(), $options + ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
|
||||||
|
{
|
||||||
|
return $this->requestAsync($request->getMethod(), $request->getUri(), $options + ['request' => $request]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfig(?string $option = null): mixed
|
||||||
|
{
|
||||||
|
if ($option !== null) {
|
||||||
|
throw new BadMethodCallException('Fake client does not expose configurable options.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function append(ResponseInterface|Throwable|callable $next): void
|
||||||
|
{
|
||||||
|
$this->queue[] = $next;
|
||||||
|
}
|
||||||
|
}
|
||||||
104
tests/Feature/LogForwardingTest.php
Normal file
104
tests/Feature/LogForwardingTest.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace HerrleinIT\LogHandler\Tests\Feature;
|
||||||
|
|
||||||
|
use ErrorException;
|
||||||
|
use HerrleinIT\LogHandler\Handlers\LogForwardingHandler;
|
||||||
|
use HerrleinIT\LogHandler\LogForwarder;
|
||||||
|
use HerrleinIT\LogHandler\Tests\Fakes\FakeLoghandlerClient;
|
||||||
|
use HerrleinIT\LogHandler\Tests\TestCase;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Testing\Fluent\AssertableJson;
|
||||||
|
|
||||||
|
class LogForwardingTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_exception_payload_is_forwarded(): void
|
||||||
|
{
|
||||||
|
$client = new FakeLoghandlerClient();
|
||||||
|
$this->app->instance(LogForwarder::class, new LogForwarder($this->app['config'], $client));
|
||||||
|
|
||||||
|
$exception = new ErrorException('Testerror', 0, E_WARNING, '/var/test/testfile.php', 1337);
|
||||||
|
|
||||||
|
$channel = Log::channel('loghandler');
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
LogForwardingHandler::class,
|
||||||
|
array_map(fn ($handler) => $handler::class, $channel->getLogger()->getHandlers())
|
||||||
|
);
|
||||||
|
|
||||||
|
$channel->warning('Ignored message', ['exception' => $exception]);
|
||||||
|
|
||||||
|
$this->assertCount(1, $client->requests);
|
||||||
|
|
||||||
|
$request = $client->requests[0];
|
||||||
|
$this->assertSame('POST', $request['method']);
|
||||||
|
$this->assertSame('http://test.example/api/error/create', $request['uri']);
|
||||||
|
$this->assertSame('Bearer test-token', $request['options']['headers']['Authorization']);
|
||||||
|
|
||||||
|
AssertableJson::fromArray($request['options']['json'])
|
||||||
|
->where('source', config('loghandler.source'))
|
||||||
|
->where('error', 'Testerror')
|
||||||
|
->where('type', 'E_WARNING')
|
||||||
|
->where('file', '/var/test/testfile.php')
|
||||||
|
->where('line', '1337')
|
||||||
|
->where('trace', fn ($trace) => is_string($trace) && $trace !== '')
|
||||||
|
->etc();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_disabled_handler_skips_forwarding(): void
|
||||||
|
{
|
||||||
|
$client = new FakeLoghandlerClient();
|
||||||
|
config()->set('loghandler.enabled', false);
|
||||||
|
|
||||||
|
$this->app->instance(LogForwarder::class, new LogForwarder($this->app['config'], $client));
|
||||||
|
|
||||||
|
Log::channel('loghandler')->error('Testerror');
|
||||||
|
|
||||||
|
$this->assertCount(0, $client->requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_missing_token_skips_forwarding(): void
|
||||||
|
{
|
||||||
|
$client = new FakeLoghandlerClient();
|
||||||
|
config()->set('loghandler.token', '');
|
||||||
|
|
||||||
|
$this->app->instance(LogForwarder::class, new LogForwarder($this->app['config'], $client));
|
||||||
|
|
||||||
|
Log::channel('loghandler')->error('Testerror');
|
||||||
|
|
||||||
|
$this->assertCount(0, $client->requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_stack_channel_includes_handler(): void
|
||||||
|
{
|
||||||
|
$client = new FakeLoghandlerClient();
|
||||||
|
$this->app->instance(LogForwarder::class, new LogForwarder($this->app['config'], $client));
|
||||||
|
|
||||||
|
$stack = Log::channel('stack');
|
||||||
|
|
||||||
|
$this->assertContains(
|
||||||
|
LogForwardingHandler::class,
|
||||||
|
array_map(fn ($handler) => $handler::class, $stack->getHandlers())
|
||||||
|
);
|
||||||
|
|
||||||
|
$stack->error('Stack error', [
|
||||||
|
'file' => '/var/test/testfile.php',
|
||||||
|
'line' => 99,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertCount(1, $client->requests);
|
||||||
|
|
||||||
|
$request = $client->requests[0];
|
||||||
|
$this->assertSame('POST', $request['method']);
|
||||||
|
$this->assertSame('http://test.example/api/error/create', $request['uri']);
|
||||||
|
|
||||||
|
AssertableJson::fromArray($request['options']['json'])
|
||||||
|
->where('source', config('loghandler.source'))
|
||||||
|
->where('error', 'Stack error')
|
||||||
|
->where('type', 'E_ERROR')
|
||||||
|
->where('file', '/var/test/testfile.php')
|
||||||
|
->where('line', '99')
|
||||||
|
->where('trace', '')
|
||||||
|
->etc();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
tests/TestCase.php
Normal file
53
tests/TestCase.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace HerrleinIT\LogHandler\Tests;
|
||||||
|
|
||||||
|
use HerrleinIT\LogHandler\LogHandlerServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Orchestra\Testbench\TestCase as Orchestra;
|
||||||
|
|
||||||
|
abstract class TestCase extends Orchestra
|
||||||
|
{
|
||||||
|
protected function getPackageProviders($app): array
|
||||||
|
{
|
||||||
|
return [LogHandlerServiceProvider::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function defineEnvironment($app): void
|
||||||
|
{
|
||||||
|
$app['config']->set('logging.default', 'stack');
|
||||||
|
$app['config']->set('logging.channels.loghandler', [
|
||||||
|
'driver' => 'loghandler',
|
||||||
|
'level' => 'debug',
|
||||||
|
'bubble' => true,
|
||||||
|
]);
|
||||||
|
$app['config']->set('logging.channels.single', [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => 'debug',
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
]);
|
||||||
|
$app['config']->set('logging.channels.stack', [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => ['single', 'loghandler'],
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$app['config']->set('loghandler', [
|
||||||
|
'endpoint' => 'http://test.example/api/error/create',
|
||||||
|
'token' => 'test-token',
|
||||||
|
'source' => 'Project Name',
|
||||||
|
'enabled' => true,
|
||||||
|
'timeout_ms' => 3000,
|
||||||
|
'retry_times' => 0,
|
||||||
|
'include_trace' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
Log::forgetChannel('loghandler');
|
||||||
|
Log::forgetChannel('stack');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user