Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Naoray committed Jan 13, 2025
1 parent 52e361d commit 2c4591c
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 126 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Consider other storage options in these Laravel-specific scenarios:
- Running async queue jobs (file storage won't work across processes)
- Using Laravel Horizon for queue management
- Running multiple application instances behind a load balancer

```php
'deduplication' => [
'store' => 'redis',
Expand All @@ -113,6 +114,7 @@ Consider other storage options in these Laravel-specific scenarios:
- Running queue jobs but Redis isn't available
- Need to persist deduplication data across deployments
- Want to query/debug deduplication history via database

```php
'deduplication' => [
'store' => 'database',
Expand Down Expand Up @@ -142,6 +144,33 @@ When buffering is active:
- With `flush_on_overflow = true`: All records are flushed
- With `flush_on_overflow = false`: Only the oldest record is removed

#### Signature Generator

Control how errors are grouped by customizing the signature generator. By default, the package uses a generator that creates signatures based on exception details or log message content.

```php
'github' => [
// ... basic config from above ...
'signature_generator' => \Naoray\LaravelGithubMonolog\Deduplication\DefaultSignatureGenerator::class,
]
```

You can implement your own signature generator by implementing the `SignatureGeneratorInterface`:

```php
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Deduplication\SignatureGeneratorInterface;

class CustomSignatureGenerator implements SignatureGeneratorInterface
{
public function generate(LogRecord $record): string
{
// Your custom logic to generate a signature
return md5($record->message);
}
}
```

### Getting a GitHub Token

To obtain a Personal Access Token:
Expand Down
10 changes: 2 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,8 @@
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"extra": {
"laravel": {
"providers": [
"Naoray\\LaravelGithubMonolog\\GithubMonologServiceProvider"
]
"pestphp/pest-plugin": true,
"phpstan/extension-installer": true
}
},
"minimum-stability": "dev",
Expand Down
3 changes: 3 additions & 0 deletions src/Deduplication/DeduplicationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public function flush(): void
->map(function (LogRecord $record) {
$signature = $this->signatureGenerator->generate($record);

// Create new record with signature in extra data
$record = $record->with(extra: ['github_issue_signature' => $signature] + $record->extra);

// If the record is a duplicate, we don't want to add it to the store
if ($this->store->isDuplicate($record, $signature)) {
return null;
Expand Down
21 changes: 14 additions & 7 deletions src/GithubIssueHandlerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use InvalidArgumentException;
use Monolog\Level;
use Monolog\Logger;
use Naoray\LaravelGithubMonolog\Deduplication\DefaultSignatureGenerator;
use Naoray\LaravelGithubMonolog\Deduplication\Stores\DatabaseStore;
use Naoray\LaravelGithubMonolog\Deduplication\Stores\FileStore;
use Naoray\LaravelGithubMonolog\Deduplication\Stores\RedisStore;
Expand All @@ -17,10 +18,6 @@

class GithubIssueHandlerFactory
{
public function __construct(
private readonly SignatureGeneratorInterface $signatureGenerator,
) {}

public function __invoke(array $config): Logger
{
$this->validateConfig($config);
Expand All @@ -47,23 +44,33 @@ protected function createBaseHandler(array $config): Handler
$handler = new Handler(
repo: $config['repo'],
token: $config['token'],
signatureGenerator: $this->signatureGenerator,
labels: Arr::get($config, 'labels', []),
level: Arr::get($config, 'level', Level::Error),
bubble: Arr::get($config, 'bubble', true)
);

$handler->setFormatter(new Formatter($this->signatureGenerator));
$handler->setFormatter(new Formatter());

return $handler;
}

protected function wrapWithDeduplication(Handler $handler, array $config): DeduplicationHandler
{
$signatureGeneratorClass = Arr::get($config, 'signature_generator', DefaultSignatureGenerator::class);

if (! is_subclass_of($signatureGeneratorClass, SignatureGeneratorInterface::class)) {
throw new InvalidArgumentException(
sprintf('Signature generator class [%s] must implement %s', $signatureGeneratorClass, SignatureGeneratorInterface::class)
);
}

/** @var SignatureGeneratorInterface $signatureGenerator */
$signatureGenerator = new $signatureGeneratorClass();

return new DeduplicationHandler(
handler: $handler,
store: $this->createStore($config),
signatureGenerator: $this->signatureGenerator,
signatureGenerator: $signatureGenerator,
level: Arr::get($config, 'level', Level::Error),
bubble: true,
bufferLimit: Arr::get($config, 'buffer.limit', 0),
Expand Down
15 changes: 0 additions & 15 deletions src/GithubMonologServiceProvider.php

This file was deleted.

12 changes: 5 additions & 7 deletions src/Issues/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
use Illuminate\Support\Stringable;
use Monolog\Formatter\FormatterInterface;
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Deduplication\SignatureGeneratorInterface;
use ReflectionClass;
use Throwable;

Expand All @@ -20,10 +19,6 @@ class Formatter implements FormatterInterface

private const VENDOR_FRAME_PLACEHOLDER = '[Vendor frames]';

public function __construct(
private readonly SignatureGeneratorInterface $signatureGenerator,
) {}

/**
* Formats a log record.
*
Expand All @@ -32,12 +27,15 @@ public function __construct(
*/
public function format(LogRecord $record): Formatted
{
if (! isset($record->extra['github_issue_signature'])) {
throw new \RuntimeException('Record is missing github_issue_signature in extra data. Make sure the DeduplicationHandler is configured correctly.');
}

$exception = $this->getException($record);
$signature = $this->signatureGenerator->generate($record);

return new Formatted(
title: $this->formatTitle($record, $exception),
body: $this->formatBody($record, $signature, $exception),
body: $this->formatBody($record, $record->extra['github_issue_signature'], $exception),
comment: $this->formatComment($record, $exception),
);
}
Expand Down
11 changes: 6 additions & 5 deletions src/Issues/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Deduplication\SignatureGeneratorInterface;

class Handler extends AbstractProcessingHandler
{
Expand All @@ -15,15 +14,13 @@ class Handler extends AbstractProcessingHandler
/**
* @param string $repo The GitHub repository in "owner/repo" format
* @param string $token Your GitHub Personal Access Token
* @param SignatureGeneratorInterface $signatureGenerator The signature generator to use
* @param array $labels Labels to be applied to GitHub issues (default: ['github-issue-logger'])
* @param int|string|Level $level Log level (default: ERROR)
* @param bool $bubble Whether the messages that are handled can bubble up the stack
*/
public function __construct(
private string $repo,
private string $token,
protected SignatureGeneratorInterface $signatureGenerator,
protected array $labels = [],
int|string|Level $level = Level::Error,
bool $bubble = true,
Expand Down Expand Up @@ -61,9 +58,13 @@ protected function write(LogRecord $record): void
*/
private function findExistingIssue(LogRecord $record): ?array
{
if (! isset($record->extra['github_issue_signature'])) {
throw new \RuntimeException('Record is missing github_issue_signature in extra data. Make sure the DeduplicationHandler is configured correctly.');
}

$response = Http::withToken($this->token)
->get('https://api.github.com/search/issues', [
'q' => "repo:{$this->repo} is:issue is:open label:" . self::DEFAULT_LABEL . " \"Signature: {$this->signatureGenerator->generate($record)}\"",
'q' => "repo:{$this->repo} is:issue is:open label:" . self::DEFAULT_LABEL . " \"Signature: {$record->extra['github_issue_signature']}\"",
]);

if ($response->failed()) {
Expand All @@ -80,7 +81,7 @@ private function commentOnIssue(int $issueNumber, Formatted $formatted): void
{
$response = Http::withToken($this->token)
->post("https://api.github.com/repos/{$this->repo}/issues/{$issueNumber}/comments", [
'body' => $formatted->comment, // TODO: fix
'body' => $formatted->comment,
]);

if ($response->failed()) {
Expand Down
19 changes: 9 additions & 10 deletions tests/GithubIssueHandlerFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,22 @@ function getSignatureGenerator(DeduplicationHandler $handler): mixed
});

test('it uses same signature generator across components', function () {
$logger = ($this->factory)($this->config);
$factory = new GithubIssueHandlerFactory(new DefaultSignatureGenerator());
$logger = $factory([
'repo' => 'test/repo',
'token' => 'test-token',
]);

/** @var DeduplicationHandler $deduplicationHandler */
/** @var \Naoray\LaravelGithubMonolog\Deduplication\DeduplicationHandler $deduplicationHandler */
$deduplicationHandler = $logger->getHandlers()[0];
$handler = getWrappedHandler($deduplicationHandler);
$formatter = $handler->getFormatter();

$handlerGenerator = (new ReflectionProperty($handler, 'signatureGenerator'))->getValue($handler);
$formatterGenerator = (new ReflectionProperty($formatter, 'signatureGenerator'))->getValue($formatter);
// Only check the deduplication handler's signature generator since other components no longer use it
$deduplicationGenerator = getSignatureGenerator($deduplicationHandler);

expect($handlerGenerator)
->toBe($this->signatureGenerator)
->and($formatterGenerator)
->toBe($this->signatureGenerator)
->and($deduplicationGenerator)
->toBe($this->signatureGenerator);
expect($deduplicationGenerator)
->toBeInstanceOf(DefaultSignatureGenerator::class);
});

test('it throws exception for invalid deduplication time', function () {
Expand Down
21 changes: 10 additions & 11 deletions tests/Issues/FormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Issues\Formatted;
use Naoray\LaravelGithubMonolog\Issues\Formatter;
use Naoray\LaravelGithubMonolog\Deduplication\DefaultSignatureGenerator;

test('it formats basic log records', function () {
$formatter = new Formatter(new DefaultSignatureGenerator);
$formatter = new Formatter();
$record = new LogRecord(
datetime: new DateTimeImmutable,
channel: 'test',
level: Level::Error,
message: 'Test error message',
context: [],
extra: []
extra: ['github_issue_signature' => 'test-signature']
);

$formatted = $formatter->format($record);
Expand All @@ -27,15 +26,15 @@
});

test('it formats exceptions with file and line information', function () {
$formatter = new Formatter(new DefaultSignatureGenerator);
$formatter = new Formatter();
$exception = new RuntimeException('Test exception');
$record = new LogRecord(
datetime: new DateTimeImmutable,
channel: 'test',
level: Level::Error,
message: 'Error occurred',
context: ['exception' => $exception],
extra: []
extra: ['github_issue_signature' => 'test-signature']
);

$formatted = $formatter->format($record);
Expand All @@ -49,15 +48,15 @@
});

test('it truncates long titles', function () {
$formatter = new Formatter(new DefaultSignatureGenerator);
$formatter = new Formatter();
$longMessage = str_repeat('a', 90);
$record = new LogRecord(
datetime: new DateTimeImmutable,
channel: 'test',
level: Level::Error,
message: $longMessage,
context: [],
extra: []
extra: ['github_issue_signature' => 'test-signature']
);

$formatted = $formatter->format($record);
Expand All @@ -66,14 +65,14 @@
});

test('it includes context data in formatted output', function () {
$formatter = new Formatter(new DefaultSignatureGenerator);
$formatter = new Formatter();
$record = new LogRecord(
datetime: new DateTimeImmutable,
channel: 'test',
level: Level::Error,
message: 'Test message',
context: ['user_id' => 123, 'action' => 'login', 'exception' => new RuntimeException('Test exception')],
extra: []
extra: ['github_issue_signature' => 'test-signature']
);

$formatted = $formatter->format($record);
Expand All @@ -84,7 +83,7 @@
});

test('it formats stack traces with collapsible vendor frames', function () {
$formatter = new Formatter(new DefaultSignatureGenerator);
$formatter = new Formatter();

$exception = new Exception('Test exception');
$reflection = new ReflectionClass($exception);
Expand Down Expand Up @@ -125,7 +124,7 @@
level: Level::Error,
message: 'Error occurred',
context: ['exception' => $exception],
extra: []
extra: ['github_issue_signature' => 'test-signature']
);

$formatted = $formatter->format($record);
Expand Down
Loading

0 comments on commit 2c4591c

Please sign in to comment.