Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Naoray committed Jan 10, 2025
1 parent 1d8dfcd commit eb6170a
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 19 deletions.
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ A Laravel package that automatically creates GitHub issues from your application

## Features

- 😊 Automatically creates GitHub issues from log entries
- 🔍 Groups similar errors into the same issue
- 💬 Adds comments to existing issues when the same error occurs again
- 🏷️ Customizable labels for better organization
- ✨ Automatically creates GitHub issues from log entries
- 🔍 Intelligently groups similar errors into single issues
- 💬 Adds comments to existing issues for recurring errors
- 🏷️ Supports customizable labels for efficient organization
- 🎯 Smart deduplication to prevent issue spam
- ⚡️ Buffered logging for better performance

## Showcase

When an error occurs in your application, a new GitHub issue is automatically created with detailed error information and stack trace:
When an error occurs in your application, a GitHub issue is automatically created with comprehensive error information and stack trace:

<img src="https://github.com/user-attachments/assets/bd1a7e9b-e1f3-43ed-b779-14fbaa974916" width="800" alt="issue raised">

Expand Down Expand Up @@ -40,12 +42,19 @@ Add the GitHub logging channel to your `config/logging.php`:
// ... other channels ...

'github' => [
// Required configuration
'driver' => 'custom',
'via' => \Naoray\LaravelGithubMonolog\GithubIssueHandlerFactory::class,
'level' => env('LOG_LEVEL', 'error'),
'repo' => env('GITHUB_REPO'), // Format: "username/repository"
'token' => env('GITHUB_TOKEN'), // Your GitHub Personal Access Token
'labels' => ['bug'], // Optional: Additional labels for issues

// Optional configuration
'level' => env('LOG_LEVEL', 'error'),
'labels' => ['bug'],
'deduplication' => [
'store' => storage_path('logs/github-issues-dedup.log'), // Custom path for deduplication storage
'time' => 60, // Time in seconds to consider logs as duplicates
],
],
]
```
Expand All @@ -57,21 +66,25 @@ GITHUB_REPO=username/repository
GITHUB_TOKEN=your-github-personal-access-token
```

You can use the `github` log channel as your default `LOG_CHANNEL` or add it as part of your stack in `LOG_STACK`.

### Getting a GitHub Token

To obtain a Personal Access Token:

1. Go to [Generate a new token](https://github.com/settings/tokens/new?description=Laravel%20GitHub%20Issue%20Logger&scopes=repo) (this link pre-selects the required scopes)
2. Review the pre-selected scopes (you should see `repo` checked)
2. Review the pre-selected scopes (the `repo` scope should be checked)
3. Click "Generate token"
4. Copy the token immediately (you won't be able to see it again!)
4. Copy the token immediately (you won't be able to access it again after leaving the page)
5. Add it to your `.env` file as `GITHUB_TOKEN`

> **Note**: The token needs the `repo` scope to create issues in both public and private repositories.
> **Note**: The token requires the `repo` scope to create issues in both public and private repositories.
## Usage

Use it like any other Laravel logging channel:
Whenever an exception is thrown it will be logged as an issue to your repository.

You can also use it like any other Laravel logging channel:

```php
// Single channel
Expand All @@ -81,7 +94,21 @@ Log::channel('github')->error('Something went wrong!');
Log::stack(['daily', 'github'])->error('Something went wrong!');
```

Each unique error will create a new GitHub issue. If the same error occurs again, it will be added as a comment to the existing issue instead of creating a duplicate.
### Deduplication

The package includes smart deduplication to prevent your repository from being flooded with duplicate issues:

1. **Time-based Deduplication**: Similar errors within the configured time window (default: 60 seconds) are considered duplicates
2. **Intelligent Grouping**: Uses error signatures to group similar errors, even if the exact details differ
3. **Automatic Storage**: Deduplication data is automatically stored in your Laravel logs directory
4. **Configurable**: Customize both the storage location and deduplication time window

For example, if your application encounters the same error multiple times in quick succession:
- First occurrence: Creates a new GitHub issue
- Subsequent occurrences within the deduplication window: No new issues created
- After the deduplication window: Creates a new issue or adds a comment to the existing one

This helps keep your GitHub issues organized and prevents notification spam during error storms.

## Testing

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"php": "^8.3",
"illuminate/contracts": "^10.0||^11.0",
"illuminate/http": "^10.0||^11.0",
"illuminate/support": "^10.0||^11.0"
"illuminate/support": "^10.0||^11.0",
"illuminate/filesystem": "^10.0||^11.0"
},
"require-dev": {
"laravel/pint": "^1.14",
Expand Down
47 changes: 46 additions & 1 deletion src/GithubIssueHandlerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@
namespace Naoray\LaravelGithubMonolog;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use InvalidArgumentException;
use Monolog\Handler\DeduplicationHandler;
use Monolog\Level;
use Monolog\Logger;

class GithubIssueHandlerFactory
{
public function __invoke(array $config): Logger
{
$this->validateConfig($config);

$handler = $this->createBaseHandler($config);
$deduplicationHandler = $this->wrapWithDeduplication($handler, $config);

return new Logger('github', [$deduplicationHandler]);
}

protected function validateConfig(array $config): void
{
if (! Arr::has($config, 'repo')) {
throw new InvalidArgumentException('GitHub repository is required');
Expand All @@ -18,7 +30,10 @@ public function __invoke(array $config): Logger
if (! Arr::has($config, 'token')) {
throw new InvalidArgumentException('GitHub token is required');
}
}

protected function createBaseHandler(array $config): GithubIssueLoggerHandler
{
$handler = new GithubIssueLoggerHandler(
repo: $config['repo'],
token: $config['token'],
Expand All @@ -29,6 +44,36 @@ public function __invoke(array $config): Logger

$handler->setFormatter(new GithubIssueFormatter);

return new Logger('github', [$handler]);
return $handler;
}

protected function wrapWithDeduplication(GithubIssueLoggerHandler $handler, array $config): DeduplicationHandler
{
return new DeduplicationHandler(
$handler,
deduplicationStore: $this->getDeduplicationStore($config),
deduplicationLevel: Arr::get($config, 'level', Level::Error),
time: $this->getDeduplicationTime($config),
bubble: true
);
}

protected function getDeduplicationStore(array $config): string
{
$deduplication = Arr::get($config, 'deduplication', []);

if ($store = Arr::get($deduplication, 'store')) {
return $store;
}

$store = storage_path('logs/github-issues-dedup.log');
File::ensureDirectoryExists(dirname($store));

return $store;
}

protected function getDeduplicationTime(array $config): int
{
return (int) Arr::get($config, 'deduplication.time', 60);
}
}
66 changes: 61 additions & 5 deletions tests/GithubIssueHandlerFactoryTest.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
<?php

use Monolog\Handler\DeduplicationHandler;
use Monolog\Level;
use Monolog\Logger;
use Naoray\LaravelGithubMonolog\GithubIssueFormatter;
use Naoray\LaravelGithubMonolog\GithubIssueHandlerFactory;
use Naoray\LaravelGithubMonolog\GithubIssueLoggerHandler;
use ReflectionProperty;

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L11.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L11.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L10.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L10.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L11.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L11.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L10.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L10.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L11.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L11.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L10.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.4 - L10.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L11.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L11.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L10.* - prefer-lowest - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

Check warning on line 9 in tests/GithubIssueHandlerFactoryTest.php

View workflow job for this annotation

GitHub Actions / P8.3 - L10.* - prefer-stable - ubuntu-latest

The use statement with non-compound name 'ReflectionProperty' has no effect

function getWrappedHandler(DeduplicationHandler $handler): GithubIssueLoggerHandler
{
$reflection = new ReflectionProperty($handler, 'handler');
$reflection->setAccessible(true);

return $reflection->getValue($handler);
}

function getDeduplicationStore(DeduplicationHandler $handler): string
{
$reflection = new ReflectionProperty($handler, 'deduplicationStore');
$reflection->setAccessible(true);

return $reflection->getValue($handler);
}

test('it creates logger with correct configuration', function () {
$config = [
Expand All @@ -21,7 +39,13 @@
->toBeInstanceOf(Logger::class)
->and($logger->getName())->toBe('github')
->and($logger->getHandlers()[0])
->toBeInstanceOf(GithubIssueLoggerHandler::class);
->toBeInstanceOf(DeduplicationHandler::class);

/** @var DeduplicationHandler $deduplicationHandler */
$deduplicationHandler = $logger->getHandlers()[0];
$handler = getWrappedHandler($deduplicationHandler);

expect($handler)->toBeInstanceOf(GithubIssueLoggerHandler::class);
});

test('it throws exception for missing required config', function () {
Expand All @@ -42,14 +66,20 @@

$factory = new GithubIssueHandlerFactory;
$logger = $factory($config);
/** @var GithubIssueLoggerHandler $handler */
$handler = $logger->getHandlers()[0];

/** @var DeduplicationHandler $deduplicationHandler */
$deduplicationHandler = $logger->getHandlers()[0];
$handler = getWrappedHandler($deduplicationHandler);

expect($handler)
->toBeInstanceOf(GithubIssueLoggerHandler::class)
->and($handler->getLevel())->toBe(Level::Error)
->and($handler->getBubble())->toBeTrue()
->and($handler->getFormatter())->toBeInstanceOf(GithubIssueFormatter::class);

// Verify default deduplication store path
expect(getDeduplicationStore($deduplicationHandler))
->toBe(storage_path('logs/github-issues-dedup.log'));
});

test('it accepts custom log level', function () {
Expand All @@ -61,8 +91,34 @@

$factory = new GithubIssueHandlerFactory;
$logger = $factory($config);
/** @var GithubIssueLoggerHandler $handler */
$handler = $logger->getHandlers()[0];

/** @var DeduplicationHandler $deduplicationHandler */
$deduplicationHandler = $logger->getHandlers()[0];
$handler = getWrappedHandler($deduplicationHandler);

expect($handler->getLevel())->toBe(Level::Debug);
});

test('it allows custom deduplication configuration', function () {
$config = [
'repo' => 'test/repo',
'token' => 'fake-token',
'deduplication' => [
'store' => '/custom/path/dedup.log',
'time' => 120,
],
];

$factory = new GithubIssueHandlerFactory;
$logger = $factory($config);

/** @var DeduplicationHandler $deduplicationHandler */
$deduplicationHandler = $logger->getHandlers()[0];

expect(getDeduplicationStore($deduplicationHandler))
->toBe('/custom/path/dedup.log');

$reflection = new ReflectionProperty($deduplicationHandler, 'time');
$reflection->setAccessible(true);
expect($reflection->getValue($deduplicationHandler))->toBe(120);
});

0 comments on commit eb6170a

Please sign in to comment.