Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- ⚠️ BREAKING! `ExceptionLog` has been removed
- ⚠️ BREAKING! `LogCommandError` and `LogRequestError` now log the exception type and message to `error`,
instead of logging the exception object to `exception`
- Switched to [Mago](https://mago.carthage.software/) for formatting and analysis

### Added

- Added a `Log::append()` method to allow attaching multiple entries, without using `LogList`
- Added `ToggledWriter` to enable or disable log writing at runtime
- Added `LogFormatter` and `JsonFormatter` to allow customization of log lines

## [0.4.0]

Expand Down
48 changes: 21 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,23 +135,12 @@ The `LogList` maintains insertion order and will encode as a JSON array:

## Exception Logging

Knotlog provides an `ExceptionLog` class to capture exception (`Throwable`) context.

```php
use Knotlog\Misc\ExceptionLog;

try {
// Some code that may throw
} catch (Throwable) {
$log->add('exception', ExceptionLog::fromThrowable($throwable));
}
```

⚠️ _The exception log includes the stack trace, which may be very large and include sensitive information.
It is recommended to set `zend.exception_ignore_args = On` in your `php.ini` to minimize the stack trace._
It is **not** recommended to add exception objects to Knotlog. Exception tracing is better handled by other systems,
such as [Monolog](https://seldaek.github.io/monolog/) or another [PSR-3](https://www.php-fig.org/psr/psr-3/) logger.

For convenience, `Log` provides a `hasError()` method that will return true if either the `exception` or `error`
keys are set. This is particularly useful for determining if the log should be output when sampling is enabled.
keys are set in the log. This is particularly useful for determining if the log should be output when sampling
is enabled.

## HTTP Middleware

Expand All @@ -174,7 +163,7 @@ use Knotlog\Http\ServerErrorResponseFactory;
// The default factory creates a minimal 500 response
$errorFactory = new ServerErrorResponseFactory($responseFactory, $streamFactory);

// Logs uncaught exceptions and outputs an error response
// Logs uncaught exception type and message, and outputs an error response
$stack->add(new LogRequestError($errorFactory, $log));
```

Expand Down Expand Up @@ -210,7 +199,7 @@ use Symfony\Component\Console\ConsoleEvents;
// Logs command metadata on execution
$eventDispatcher->addListener(ConsoleEvents::COMMAND, new LogCommandEvent($log));

// Logs command error context on failure
// Logs uncaught exception type and message
$eventDispatcher->addListener(ConsoleEvents::ERROR, new LogCommandError($log));
```

Expand All @@ -226,11 +215,11 @@ $log->set('console', LogCommand::fromCommand($command, $input));
## Log Writers

Knotlog provides a `LogWriter` interface to enable flexible output destinations for wide log events.
The interface defines a simple contract with a single method `write(Log $log): void`.
The interface defines a contract with a single method `write(Log $log): void`.

### FileWriter

The `FileWriter` writes log events as JSON-encoded lines to a file or stream.
The `FileWriter` writes log events to a file.

```php
use Knotlog\Writer\FileWriter;
Expand All @@ -241,18 +230,10 @@ $writer = new FileWriter();
// Write to a specific file
$writer = new FileWriter('/var/log/app.log');

// Use custom JSON encoding flags
$writer = new FileWriter('php://stdout', JSON_PRETTY_PRINT);

// Write the log event
$writer->write($log);
```

Each log line is prefixed with a status indicator:

- `ERROR` - when the log contains an error or exception
- `INFO` - for all other log events

### LoggerWriter

The `LoggerWriter` outputs log events to any [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger
Expand Down Expand Up @@ -333,6 +314,19 @@ $writer->write($log);
This is useful for conditionally disabling log output based on runtime configuration, such as
feature flags or environment-specific settings.

## Log Formatters

Knotlog provides a `LogFormatter` interface to enable flexible formatting of log lines.
The interface defines a contract with a single method `format(Log $log): string`.

### JsonFormatter

The `JsonFormatter` formats log events as JSON strings. Additional flags may be passed through to `json_encode`:

```php
$formatter = new JsonFormatter(JSON_PRETTY_PRINT);
```

## License

MIT License, see `LICENSE` file for details.
14 changes: 0 additions & 14 deletions captainhook.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,5 @@
]
}
]
},
"commit-msg": {
"enabled": true,
"actions": [
{
"action": "\\CaptainHook\\App\\Hook\\Message\\Action\\Beams",
"options": {
"subjectLength": 50,
"bodyLineLength": 72,
"checkImperativeBeginningOnly": true
},
"conditions": []
}
]
}
}
5 changes: 3 additions & 2 deletions src/Console/LogCommandError.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Knotlog\Console;

use Knotlog\Log;
use Knotlog\Misc\ExceptionLog;
use Symfony\Component\Console\Event\ConsoleErrorEvent;

final readonly class LogCommandError
Expand All @@ -16,6 +15,8 @@ public function __construct(

public function __invoke(ConsoleErrorEvent $event): void
{
$this->log->set('exception', ExceptionLog::fromThrowable($event->getError()));
$throwable = $event->getError();

$this->log->set('error', sprintf('%s %s', $throwable::class, $throwable->getMessage()));
}
}
3 changes: 1 addition & 2 deletions src/Http/LogRequestError.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Knotlog\Http;

use Knotlog\Log;
use Knotlog\Misc\ExceptionLog;
use Override;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand All @@ -29,7 +28,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
try {
return $handler->handle($request);
} catch (Throwable $throwable) {
$this->log->set('exception', ExceptionLog::fromThrowable($throwable));
$this->log->set('error', sprintf('%s %s', $throwable::class, $throwable->getMessage()));

return $this->errorResponseFactory->createErrorResponse($throwable);
}
Expand Down
31 changes: 0 additions & 31 deletions src/Misc/ExceptionLog.php

This file was deleted.

13 changes: 0 additions & 13 deletions src/Misc/ExceptionSource.php

This file was deleted.

14 changes: 4 additions & 10 deletions src/Writer/FileWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,20 @@
use Override;
use Symfony\Component\Filesystem\Filesystem;

use function json_encode;

use const JSON_THROW_ON_ERROR;

final readonly class FileWriter implements LogWriter
{
public function __construct(
private string $path = 'php://stderr',
private int $flags = 0,
private Filesystem $filesystem = new Filesystem(),
private LogFormatter $formatter = new JsonFormatter(),
private string $path = 'php://stderr',
) {}

#[Override]
public function write(Log $log): void
{
$status = $log->hasError() ? 'ERROR' : 'INFO';

$line = json_encode($log, JSON_THROW_ON_ERROR | $this->flags);
$line = $this->formatter->format($log);

// @mago-ignore analysis:unhandled-thrown-type
$this->filesystem->appendToFile($this->path, "{$status} {$line}\n");
$this->filesystem->appendToFile($this->path, "{$line}\n");
}
}
21 changes: 21 additions & 0 deletions src/Writer/JsonFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Knotlog\Writer;

use Knotlog\Log;
use Override;

final readonly class JsonFormatter implements LogFormatter
{
public function __construct(
private int $flags = 0,
) {}

#[Override]
public function format(Log $log): string
{
return json_encode($log, JSON_THROW_ON_ERROR | $this->flags);
}
}
12 changes: 12 additions & 0 deletions src/Writer/LogFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Knotlog\Writer;

use Knotlog\Log;

interface LogFormatter
{
public function format(Log $log): string;
}
8 changes: 6 additions & 2 deletions tests/Console/LogCommandErrorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Knotlog\Console\LogCommandError;
use Knotlog\Log;
use Knotlog\Misc\ExceptionLog;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -35,6 +34,11 @@
$listener($event);

$this->assertTrue($log->hasError());
$this->assertInstanceOf(ExceptionLog::class, $log->all()['exception']);

$msg = $log->all()['error'];

Check warning on line 38 in tests/Console/LogCommandErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.4)

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

Check warning on line 38 in tests/Console/LogCommandErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.4)

possibly-undefined-string-array-index

Possibly undefined array key `string('error')` accessed on `array<string, mixed>`. >Key `string('error')` might not exist. The analysis indicates this specific key might not be set when this access occurs. Help: Ensure the key string('error') is always set before accessing it, or use `isset()` or the null coalesce operator (`??`) to handle potential missing keys.

Check warning on line 38 in tests/Console/LogCommandErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.5)

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

Check warning on line 38 in tests/Console/LogCommandErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.5)

possibly-undefined-string-array-index

Possibly undefined array key `string('error')` accessed on `array<string, mixed>`. >Key `string('error')` might not exist. The analysis indicates this specific key might not be set when this access occurs. Help: Ensure the key string('error') is always set before accessing it, or use `isset()` or the null coalesce operator (`??`) to handle potential missing keys.

$this->assertTrue(is_string($msg));
$this->assertStringContainsString($exception::class, $msg);
$this->assertStringContainsString($exception->getMessage(), $msg);
}
}
12 changes: 8 additions & 4 deletions tests/Http/LogRequestErrorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

namespace Knotlog\Tests\Http;

use Exception;
use Knotlog\Http\ErrorResponseFactory;
use Knotlog\Http\LogRequestError;
use Knotlog\Log;
use Knotlog\Misc\ExceptionLog;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\ServerRequest;
use Override;
Expand All @@ -18,6 +16,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use Throwable;

#[CoversClass(LogRequestError::class)]
Expand Down Expand Up @@ -49,14 +48,19 @@
$middleware = new LogRequestError($errorFactory, $log);

$request = new ServerRequest('GET', 'https://api.example.com/error');
$exception = new Exception('Something went wrong');
$exception = new RuntimeException('Something went wrong');

$handler = $this->createThrowingHandler($exception);

$middleware->process($request, $handler);

$this->assertTrue($log->hasError());
$this->assertInstanceOf(ExceptionLog::class, $log->all()['exception']);

$msg = $log->all()['error'];

Check warning on line 59 in tests/Http/LogRequestErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.4)

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

Check warning on line 59 in tests/Http/LogRequestErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.4)

possibly-undefined-string-array-index

Possibly undefined array key `string('error')` accessed on `array<string, mixed>`. >Key `string('error')` might not exist. The analysis indicates this specific key might not be set when this access occurs. Help: Ensure the key string('error') is always set before accessing it, or use `isset()` or the null coalesce operator (`??`) to handle potential missing keys.

Check warning on line 59 in tests/Http/LogRequestErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.5)

mixed-assignment

Assigning `mixed` type to a variable may lead to unexpected behavior. >Assigning `mixed` type here. Using `mixed` can lead to runtime errors if the variable is used in a way that assumes a specific type. Help: Consider using a more specific type to avoid potential issues.

Check warning on line 59 in tests/Http/LogRequestErrorTest.php

View workflow job for this annotation

GitHub Actions / tests (8.5)

possibly-undefined-string-array-index

Possibly undefined array key `string('error')` accessed on `array<string, mixed>`. >Key `string('error')` might not exist. The analysis indicates this specific key might not be set when this access occurs. Help: Ensure the key string('error') is always set before accessing it, or use `isset()` or the null coalesce operator (`??`) to handle potential missing keys.

$this->assertTrue(is_string($msg));
$this->assertStringContainsString($exception::class, $msg);
$this->assertStringContainsString($exception->getMessage(), $msg);
}

private function createMockErrorFactory(): ErrorResponseFactory
Expand Down
32 changes: 0 additions & 32 deletions tests/Misc/ExceptionLogTest.php

This file was deleted.

Loading
Loading