Skip to content

Commit 97d563d

Browse files
authored
Merge pull request #2 from Naoray/feature/added-deduplication-stores
Feature/added deduplication stores
2 parents 4b9f78a + 252b882 commit 97d563d

29 files changed

+1356
-674
lines changed

.github/workflows/run-tests.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ jobs:
1313
test:
1414
runs-on: ${{ matrix.os }}
1515
timeout-minutes: 5
16+
17+
services:
18+
redis:
19+
image: redis
20+
ports:
21+
- 6379:6379
22+
options: >-
23+
--health-cmd "redis-cli ping"
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
1628
strategy:
1729
fail-fast: true
1830
matrix:
@@ -34,6 +46,7 @@ jobs:
3446
uses: shivammathur/setup-php@v2
3547
with:
3648
php-version: ${{ matrix.php }}
49+
extensions: redis
3750
coverage: none
3851

3952
- name: Setup problem matchers
@@ -51,3 +64,6 @@ jobs:
5164

5265
- name: Execute tests
5366
run: vendor/bin/pest --ci
67+
env:
68+
REDIS_HOST: localhost
69+
REDIS_PORT: 6379

README.md

Lines changed: 103 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/naoray/laravel-github-monolog/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/naoray/laravel-github-monolog/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain)
66
[![Total Downloads](https://img.shields.io/packagist/dt/naoray/laravel-github-monolog.svg?style=flat-square)](https://packagist.org/packages/naoray/laravel-github-monolog)
77

8-
A Laravel package that automatically creates GitHub issues from your application logs. Perfect for smaller projects where full-featured logging services like Sentry or Bugsnag might be overkill, but you still want to track bugs effectively.
8+
Automatically create GitHub issues from your Laravel logs. Perfect for smaller projects without the need for full-featured logging services.
99

1010
## Requirements
1111

@@ -15,10 +15,10 @@ A Laravel package that automatically creates GitHub issues from your application
1515

1616
## Features
1717

18-
- ✨ Automatically creates GitHub issues from log entries
19-
- 🔍 Intelligently groups similar errors into single issues
20-
- 💬 Adds comments to existing issues for recurring errors
21-
- 🏷️ Supports customizable labels for efficient organization
18+
- ✨ Automatically create GitHub issues from logs
19+
- 🔍 Group similar errors into single issues
20+
- 💬 Add comments to existing issues for recurring errors
21+
- 🏷️ Support customizable labels
2222
- 🎯 Smart deduplication to prevent issue spam
2323
- ⚡️ Buffered logging for better performance
2424

@@ -38,15 +38,15 @@ If the same error occurs again, instead of creating a duplicate, a new comment i
3838

3939
## Installation
4040

41-
Install the package via composer:
41+
Install with Composer:
4242

4343
```bash
4444
composer require naoray/laravel-github-monolog
4545
```
4646

4747
## Configuration
4848

49-
Add the GitHub logging channel to your `config/logging.php`:
49+
Add the GitHub logging channel to `config/logging.php`:
5050

5151
```php
5252
'channels' => [
@@ -62,10 +62,6 @@ Add the GitHub logging channel to your `config/logging.php`:
6262
// Optional configuration
6363
'level' => env('LOG_LEVEL', 'error'),
6464
'labels' => ['bug'],
65-
'deduplication' => [
66-
'store' => storage_path('logs/github-issues-dedup.log'), // Custom path for deduplication storage
67-
'time' => 60, // Time in seconds to consider logs as duplicates
68-
],
6965
],
7066
]
7167
```
@@ -79,6 +75,102 @@ GITHUB_TOKEN=your-github-personal-access-token
7975

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

78+
### Advanced Configuration
79+
80+
Deduplication and buffering are enabled by default to enhance logging. Customize these features to suit your needs.
81+
82+
#### Deduplication
83+
84+
Group similar errors to avoid duplicate issues. By default, the package uses file-based storage. Customize the storage and time window to fit your application.
85+
86+
```php
87+
'github' => [
88+
// ... basic config from above ...
89+
'deduplication' => [
90+
'store' => 'file', // Default store
91+
'time' => 60, // Time window in seconds
92+
],
93+
]
94+
```
95+
96+
##### Alternative Storage Options
97+
98+
Consider other storage options in these Laravel-specific scenarios:
99+
100+
- **Redis Store**: Use when:
101+
- Running async queue jobs (file storage won't work across processes)
102+
- Using Laravel Horizon for queue management
103+
- Running multiple application instances behind a load balancer
104+
105+
```php
106+
'deduplication' => [
107+
'store' => 'redis',
108+
'prefix' => 'github-monolog:',
109+
'connection' => 'default', // Uses your Laravel Redis connection
110+
],
111+
```
112+
113+
- **Database Store**: Use when:
114+
- Running queue jobs but Redis isn't available
115+
- Need to persist deduplication data across deployments
116+
- Want to query/debug deduplication history via database
117+
118+
```php
119+
'deduplication' => [
120+
'store' => 'database',
121+
'table' => 'github_monolog_deduplication',
122+
'connection' => null, // Uses your default database connection
123+
],
124+
```
125+
126+
#### Buffering
127+
128+
Buffer logs to reduce GitHub API calls. Customize the buffer size and overflow behavior to optimize performance:
129+
130+
```php
131+
'github' => [
132+
// ... basic config from above ...
133+
'buffer' => [
134+
'limit' => 0, // Maximum records in buffer (0 = unlimited, flush on shutdown)
135+
'flush_on_overflow' => true, // When limit is reached: true = flush all, false = remove oldest
136+
],
137+
]
138+
```
139+
140+
When buffering is active:
141+
- Logs are collected in memory until flushed
142+
- Buffer is automatically flushed on application shutdown
143+
- When limit is reached:
144+
- With `flush_on_overflow = true`: All records are flushed
145+
- With `flush_on_overflow = false`: Only the oldest record is removed
146+
147+
#### Signature Generator
148+
149+
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.
150+
151+
```php
152+
'github' => [
153+
// ... basic config from above ...
154+
'signature_generator' => \Naoray\LaravelGithubMonolog\Deduplication\DefaultSignatureGenerator::class,
155+
]
156+
```
157+
158+
You can implement your own signature generator by implementing the `SignatureGeneratorInterface`:
159+
160+
```php
161+
use Monolog\LogRecord;
162+
use Naoray\LaravelGithubMonolog\Deduplication\SignatureGeneratorInterface;
163+
164+
class CustomSignatureGenerator implements SignatureGeneratorInterface
165+
{
166+
public function generate(LogRecord $record): string
167+
{
168+
// Your custom logic to generate a signature
169+
return md5($record->message);
170+
}
171+
}
172+
```
173+
82174
### Getting a GitHub Token
83175

84176
To obtain a Personal Access Token:
@@ -105,22 +197,6 @@ Log::channel('github')->error('Something went wrong!');
105197
Log::stack(['daily', 'github'])->error('Something went wrong!');
106198
```
107199

108-
### Deduplication
109-
110-
The package includes smart deduplication to prevent your repository from being flooded with duplicate issues:
111-
112-
1. **Time-based Deduplication**: Similar errors within the configured time window (default: 60 seconds) are considered duplicates
113-
2. **Intelligent Grouping**: Uses error signatures to group similar errors, even if the exact details differ
114-
3. **Automatic Storage**: Deduplication data is automatically stored in your Laravel logs directory
115-
4. **Configurable**: Customize both the storage location and deduplication time window
116-
117-
For example, if your application encounters the same error multiple times in quick succession:
118-
- First occurrence: Creates a new GitHub issue
119-
- Subsequent occurrences within the deduplication window: No new issues created
120-
- After the deduplication window: Creates a new issue or adds a comment to the existing one
121-
122-
This helps keep your GitHub issues organized and prevents notification spam during error storms.
123-
124200
## Testing
125201

126202
```bash

composer.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"keywords": [
55
"Krishan Koenig",
66
"laravel",
7-
"laravel-github-monolog"
7+
"monolog",
8+
"github",
9+
"logging"
810
],
911
"homepage": "https://github.com/naoray/laravel-github-monolog",
1012
"license": "MIT",
@@ -37,13 +39,12 @@
3739
},
3840
"autoload": {
3941
"psr-4": {
40-
"Naoray\\LaravelGithubMonolog\\": "src/"
42+
"Naoray\\LaravelGithubMonolog\\": "src"
4143
}
4244
},
4345
"autoload-dev": {
4446
"psr-4": {
45-
"Naoray\\GithubMonolog\\Tests\\": "tests/",
46-
"Workbench\\App\\": "workbench/app/"
47+
"Naoray\\LaravelGithubMonolog\\Tests\\": "tests"
4748
}
4849
},
4950
"scripts": {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Naoray\LaravelGithubMonolog\Deduplication;
4+
5+
use Illuminate\Support\Collection;
6+
use Monolog\Handler\BufferHandler;
7+
use Monolog\Handler\HandlerInterface;
8+
use Monolog\Level;
9+
use Monolog\LogRecord;
10+
use Naoray\LaravelGithubMonolog\Deduplication\Stores\StoreInterface;
11+
12+
class DeduplicationHandler extends BufferHandler
13+
{
14+
public function __construct(
15+
HandlerInterface $handler,
16+
protected StoreInterface $store,
17+
protected SignatureGeneratorInterface $signatureGenerator,
18+
int|string|Level $level = Level::Error,
19+
int $bufferLimit = 0,
20+
bool $bubble = true,
21+
bool $flushOnOverflow = false,
22+
) {
23+
parent::__construct(
24+
handler: $handler,
25+
bufferLimit: $bufferLimit,
26+
level: $level,
27+
bubble: $bubble,
28+
flushOnOverflow: $flushOnOverflow,
29+
);
30+
}
31+
32+
public function flush(): void
33+
{
34+
if ($this->bufferSize === 0) {
35+
return;
36+
}
37+
38+
collect($this->buffer)
39+
->map(function (LogRecord $record) {
40+
$signature = $this->signatureGenerator->generate($record);
41+
42+
// Create new record with signature in extra data
43+
$record = $record->with(extra: ['github_issue_signature' => $signature] + $record->extra);
44+
45+
// If the record is a duplicate, we don't want to add it to the store
46+
if ($this->store->isDuplicate($record, $signature)) {
47+
return null;
48+
}
49+
50+
$this->store->add($record, $signature);
51+
52+
return $record;
53+
})
54+
->filter()
55+
->pipe(fn (Collection $records) => $this->handler->handleBatch($records->toArray()));
56+
57+
$this->clear();
58+
}
59+
}

src/DefaultSignatureGenerator.php renamed to src/Deduplication/DefaultSignatureGenerator.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<?php
22

3-
namespace Naoray\LaravelGithubMonolog;
3+
namespace Naoray\LaravelGithubMonolog\Deduplication;
44

55
use Monolog\LogRecord;
6-
use Naoray\LaravelGithubMonolog\Contracts\SignatureGenerator;
76
use Throwable;
87

9-
class DefaultSignatureGenerator implements SignatureGenerator
8+
class DefaultSignatureGenerator implements SignatureGeneratorInterface
109
{
1110
/**
1211
* Generate a unique signature for the log record

src/Contracts/SignatureGenerator.php renamed to src/Deduplication/SignatureGeneratorInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<?php
22

3-
namespace Naoray\LaravelGithubMonolog\Contracts;
3+
namespace Naoray\LaravelGithubMonolog\Deduplication;
44

55
use Monolog\LogRecord;
66

7-
interface SignatureGenerator
7+
interface SignatureGeneratorInterface
88
{
99
/**
1010
* Generate a unique signature for the log record
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Naoray\LaravelGithubMonolog\Deduplication\Stores;
4+
5+
use Illuminate\Support\Carbon;
6+
use Monolog\LogRecord;
7+
8+
abstract class AbstractStore implements StoreInterface
9+
{
10+
public function __construct(
11+
protected int $time = 60
12+
) {}
13+
14+
protected function buildEntry(string $signature, int $timestamp): string
15+
{
16+
return $timestamp.':'.$signature;
17+
}
18+
19+
public function isDuplicate(LogRecord $record, string $signature): bool
20+
{
21+
$foundDuplicate = false;
22+
23+
foreach ($this->get() as $entry) {
24+
[$timestamp, $storedSignature] = explode(':', $entry, 2);
25+
$timestamp = (int) $timestamp;
26+
27+
if ($this->isExpired($timestamp)) {
28+
continue;
29+
}
30+
31+
if ($storedSignature === $signature) {
32+
$foundDuplicate = true;
33+
}
34+
}
35+
36+
return $foundDuplicate;
37+
}
38+
39+
protected function isExpired(int $timestamp): bool
40+
{
41+
return $this->getTimestampValidity() > $timestamp;
42+
}
43+
44+
protected function getTimestampValidity(): int
45+
{
46+
return $this->getTimestamp() - $this->time;
47+
}
48+
49+
protected function getTimestamp(): int
50+
{
51+
return Carbon::now()->timestamp;
52+
}
53+
}

0 commit comments

Comments
 (0)