generated from spatie/package-skeleton-laravel
-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
1,123 additions
and
321 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
namespace Naoray\LaravelGithubMonolog\DeduplicationStores; | ||
|
||
use Monolog\LogRecord; | ||
|
||
interface DeduplicationStoreInterface | ||
{ | ||
/** | ||
* Get all stored deduplication entries | ||
* | ||
* @return array<string> | ||
*/ | ||
public function get(): array; | ||
|
||
/** | ||
* Add a new deduplication entry | ||
*/ | ||
public function add(LogRecord $record, string $signature): void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?php | ||
|
||
namespace Naoray\LaravelGithubMonolog\Handlers; | ||
|
||
use Monolog\Handler\AbstractProcessingHandler; | ||
use Monolog\Handler\HandlerInterface; | ||
use Monolog\Level; | ||
use Monolog\LogRecord; | ||
use Naoray\LaravelGithubMonolog\Contracts\SignatureGenerator; | ||
use Naoray\LaravelGithubMonolog\DefaultSignatureGenerator; | ||
use Naoray\LaravelGithubMonolog\DeduplicationStores\DeduplicationStoreInterface; | ||
use Naoray\LaravelGithubMonolog\DeduplicationStores\RedisDeduplicationStore; | ||
|
||
class SignatureDeduplicationHandler extends AbstractProcessingHandler | ||
{ | ||
private SignatureGenerator $signatureGenerator; | ||
private HandlerInterface $handler; | ||
private DeduplicationStoreInterface $store; | ||
private int $time; | ||
|
||
public function __construct( | ||
HandlerInterface $handler, | ||
?DeduplicationStoreInterface $store = null, | ||
int|string|Level $level = Level::Error, | ||
int $time = 60, | ||
bool $bubble = true, | ||
?SignatureGenerator $signatureGenerator = null, | ||
) { | ||
parent::__construct($level, $bubble); | ||
|
||
$this->handler = $handler; | ||
$this->time = $time; | ||
$this->store = $store ?? new RedisDeduplicationStore(time: $time); | ||
$this->signatureGenerator = $signatureGenerator ?? new DefaultSignatureGenerator; | ||
} | ||
|
||
protected function write(LogRecord $record): void | ||
{ | ||
$signature = $this->signatureGenerator->generate($record); | ||
|
||
if ($this->isDuplicate($record, $signature)) { | ||
return; | ||
} | ||
|
||
$this->store->add($record, $signature); | ||
$this->handler->handle($record); | ||
} | ||
|
||
protected function isDuplicate(LogRecord $record, string $signature): bool | ||
{ | ||
$store = $this->store->get(); | ||
$timestampValidity = time() - $this->time; | ||
|
||
foreach ($store as $entry) { | ||
[$timestamp, $storedSignature] = explode(':', $entry, 2); | ||
if ($storedSignature === $signature && (int) $timestamp > $timestampValidity) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
|
||
namespace Naoray\LaravelGithubMonolog\DeduplicationStores; | ||
|
||
abstract class AbstractDeduplicationStore implements DeduplicationStoreInterface | ||
{ | ||
protected string $prefix; | ||
protected int $time; | ||
|
||
public function __construct(string $prefix = 'github-monolog:', int $time = 60) | ||
{ | ||
$this->prefix = $prefix; | ||
$this->time = $time; | ||
} | ||
|
||
protected function formatEntry(string $signature, int $timestamp): string | ||
{ | ||
return $timestamp . ':' . $signature; | ||
} | ||
|
||
protected function isExpired(int $timestamp): bool | ||
{ | ||
return $timestamp < time() - $this->time; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?php | ||
|
||
namespace Naoray\LaravelGithubMonolog\DeduplicationStores; | ||
|
||
use Illuminate\Database\Schema\Blueprint; | ||
use Illuminate\Support\Facades\DB; | ||
use Illuminate\Support\Facades\Schema; | ||
use Monolog\LogRecord; | ||
|
||
class DatabaseDeduplicationStore extends AbstractDeduplicationStore | ||
{ | ||
private string $table; | ||
private string $connection; | ||
|
||
public function __construct( | ||
string $connection = 'default', | ||
string $table = 'github_monolog_deduplication', | ||
string $prefix = 'github-monolog:', | ||
int $time = 60 | ||
) { | ||
parent::__construct($prefix, $time); | ||
|
||
$this->connection = $connection === 'default' ? config('database.default') : $connection; | ||
$this->table = $table; | ||
|
||
$this->ensureTableExists(); | ||
} | ||
|
||
public function get(): array | ||
{ | ||
$this->cleanup(); | ||
|
||
return DB::connection($this->connection) | ||
->table($this->table) | ||
->where('prefix', $this->prefix) | ||
->get() | ||
->map(fn($row) => $this->formatEntry($row->signature, $row->created_at)) | ||
->all(); | ||
} | ||
|
||
public function add(LogRecord $record, string $signature): void | ||
{ | ||
DB::connection($this->connection) | ||
->table($this->table) | ||
->insert([ | ||
'prefix' => $this->prefix, | ||
'signature' => $signature, | ||
'created_at' => time(), | ||
]); | ||
} | ||
|
||
private function cleanup(): void | ||
{ | ||
DB::connection($this->connection) | ||
->table($this->table) | ||
->where('prefix', $this->prefix) | ||
->where('created_at', '<', time() - $this->time) | ||
->delete(); | ||
} | ||
|
||
private function ensureTableExists(): void | ||
{ | ||
if (! Schema::connection($this->connection)->hasTable($this->table)) { | ||
Schema::connection($this->connection)->create($this->table, function (Blueprint $table) { | ||
$table->id(); | ||
$table->string('prefix')->index(); | ||
$table->string('signature'); | ||
$table->integer('created_at')->index(); | ||
|
||
$table->index(['prefix', 'signature', 'created_at']); | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
<?php | ||
|
||
namespace Naoray\LaravelGithubMonolog\DeduplicationStores; | ||
|
||
use Illuminate\Support\Facades\File; | ||
use Monolog\LogRecord; | ||
use RuntimeException; | ||
|
||
class FileDeduplicationStore extends AbstractDeduplicationStore | ||
{ | ||
private string $path; | ||
private $handle = null; | ||
|
||
public function __construct( | ||
string $path, | ||
string $prefix = 'github-monolog:', | ||
int $time = 60 | ||
) { | ||
parent::__construct($prefix, $time); | ||
|
||
$this->path = $path; | ||
$this->ensureDirectoryExists(); | ||
} | ||
|
||
public function get(): array | ||
{ | ||
$this->acquireLock(); | ||
|
||
try { | ||
$content = $this->readContent(); | ||
$entries = array_filter(explode(PHP_EOL, $content)); | ||
|
||
// Filter out expired entries and rebuild file | ||
$valid = array_filter($entries, function ($entry) { | ||
if (!str_contains($entry, ':')) { | ||
return false; | ||
} | ||
[$timestamp] = explode(':', $entry, 2); | ||
return is_numeric($timestamp) && !$this->isExpired((int) $timestamp); | ||
}); | ||
|
||
if (count($valid) !== count($entries)) { | ||
$this->writeContent(implode(PHP_EOL, $valid)); | ||
} | ||
|
||
return $valid; | ||
} catch (RuntimeException $e) { | ||
// If we can't read the file, assume no entries | ||
return []; | ||
} finally { | ||
$this->releaseLock(); | ||
} | ||
} | ||
|
||
public function add(LogRecord $record, string $signature): void | ||
{ | ||
$this->acquireLock(); | ||
|
||
try { | ||
$entry = $this->formatEntry($signature, time()); | ||
$content = $this->readContent(); | ||
|
||
$this->writeContent( | ||
($content ? $content . PHP_EOL : '') . $entry | ||
); | ||
} finally { | ||
$this->releaseLock(); | ||
} | ||
} | ||
|
||
private function acquireLock(): void | ||
{ | ||
if ($this->handle !== null) { | ||
return; // Already have a lock | ||
} | ||
|
||
if (!$this->handle = fopen($this->path, 'c+')) { | ||
throw new RuntimeException("Cannot open file: {$this->path}"); | ||
} | ||
|
||
$attempts = 3; | ||
while ($attempts--) { | ||
if (flock($this->handle, LOCK_EX | LOCK_NB)) { | ||
return; | ||
} | ||
if ($attempts) { | ||
usleep(100000); // Sleep for 100ms between attempts | ||
} | ||
} | ||
|
||
fclose($this->handle); | ||
$this->handle = null; | ||
throw new RuntimeException("Cannot acquire lock on file: {$this->path}"); | ||
} | ||
|
||
private function releaseLock(): void | ||
{ | ||
if ($this->handle) { | ||
flock($this->handle, LOCK_UN); | ||
fclose($this->handle); | ||
$this->handle = null; | ||
} | ||
} | ||
|
||
private function readContent(): string | ||
{ | ||
if (!$this->handle) { | ||
throw new RuntimeException('File handle not initialized'); | ||
} | ||
|
||
fseek($this->handle, 0); | ||
return stream_get_contents($this->handle) ?: ''; | ||
} | ||
|
||
private function writeContent(string $content): void | ||
{ | ||
if (!$this->handle) { | ||
throw new RuntimeException('File handle not initialized'); | ||
} | ||
|
||
ftruncate($this->handle, 0); | ||
fseek($this->handle, 0); | ||
fwrite($this->handle, $content); | ||
fflush($this->handle); | ||
} | ||
|
||
private function ensureDirectoryExists(): void | ||
{ | ||
$directory = dirname($this->path); | ||
if (!File::exists($directory)) { | ||
File::makeDirectory($directory, 0755, true); | ||
} | ||
} | ||
|
||
public function __destruct() | ||
{ | ||
$this->releaseLock(); | ||
} | ||
} |
Oops, something went wrong.