Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Naoray committed Jan 12, 2025
1 parent ffa8aa4 commit a7905b2
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 41 deletions.
13 changes: 13 additions & 0 deletions src/Contracts/SignatureGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Naoray\LaravelGithubMonolog\Contracts;

use Monolog\LogRecord;

interface SignatureGenerator
{
/**
* Generate a unique signature for the log record
*/
public function generate(LogRecord $record): string;
}
48 changes: 48 additions & 0 deletions src/DefaultSignatureGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Naoray\LaravelGithubMonolog;

use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Contracts\SignatureGenerator;
use Throwable;

class DefaultSignatureGenerator implements SignatureGenerator
{
/**
* Generate a unique signature for the log record
*/
public function generate(LogRecord $record): string
{
$exception = $record->context['exception'] ?? null;

if (!$exception instanceof Throwable) {
return $this->generateFromMessage($record);
}

return $this->generateFromException($exception);
}

/**
* Generate a signature from a message and context
*/
private function generateFromMessage(LogRecord $record): string
{
return md5($record->message . json_encode($record->context));
}

/**
* Generate a signature from an exception
*/
private function generateFromException(Throwable $exception): string
{
$trace = $exception->getTrace();
$firstFrame = !empty($trace) ? $trace[0] : null;

return md5(implode(':', [
$exception::class,
$exception->getFile(),
$exception->getLine(),
$firstFrame ? ($firstFrame['file'] ?? '') . ':' . ($firstFrame['line'] ?? '') : '',
]));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Naoray\LaravelGithubMonolog;
namespace Naoray\LaravelGithubMonolog\Formatters;

class GithubIssueFormatted
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Naoray\LaravelGithubMonolog;
namespace Naoray\LaravelGithubMonolog\Formatters;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -54,7 +54,7 @@ public function formatBatch(array $records): array
private function generateSignature(LogRecord $record, ?Throwable $exception): string
{
if (! $exception) {
return md5($record->message.json_encode($record->context));
return md5($record->message . json_encode($record->context));
}

$trace = $exception->getTrace();
Expand All @@ -64,7 +64,7 @@ private function generateSignature(LogRecord $record, ?Throwable $exception): st
$exception::class,
$exception->getFile(),
$exception->getLine(),
$firstFrame ? ($firstFrame['file'] ?? '').':'.($firstFrame['line'] ?? '') : '',
$firstFrame ? ($firstFrame['file'] ?? '') . ':' . ($firstFrame['line'] ?? '') : '',
]));
}

Expand Down Expand Up @@ -110,7 +110,7 @@ private function formatTitle(LogRecord $record, ?Throwable $exception = null): s
private function formatContent(LogRecord $record, ?Throwable $exception): string
{
return Str::of('')
->when($record->message, fn ($str, $message) => $str->append("**Message:**\n{$message}\n\n"))
->when($record->message, fn($str, $message) => $str->append("**Message:**\n{$message}\n\n"))
->when(
$exception,
function (Stringable $str, Throwable $exception) {
Expand All @@ -120,8 +120,8 @@ function (Stringable $str, Throwable $exception) {
);
}
)
->when(! $exception && ! empty($record->context), fn ($str, $context) => $str->append("**Context:**\n```json\n".json_encode($record->context, JSON_PRETTY_PRINT)."\n```\n\n"))
->when(! empty($record->extra), fn ($str, $extra) => $str->append("**Extra Data:**\n```json\n".json_encode($record->extra, JSON_PRETTY_PRINT)."\n```\n"))
->when(! $exception && ! empty($record->context), fn($str, $context) => $str->append("**Context:**\n```json\n" . json_encode($record->context, JSON_PRETTY_PRINT) . "\n```\n\n"))
->when(! empty($record->extra), fn($str, $extra) => $str->append("**Extra Data:**\n```json\n" . json_encode($record->extra, JSON_PRETTY_PRINT) . "\n```\n"))
->toString();
}

Expand All @@ -141,7 +141,7 @@ private function formatBody(LogRecord $record, string $signature, ?Throwable $ex
private function cleanStackTrace(string $stackTrace): string
{
return collect(explode("\n", $stackTrace))
->filter(fn ($line) => ! empty(trim($line)))
->filter(fn($line) => ! empty(trim($line)))
->map(function ($line) {
if (trim($line) === '"}') {
return '';
Expand Down Expand Up @@ -217,8 +217,8 @@ private function formatExceptionDetails(Throwable $exception): array

return [
'message' => $exception->getMessage(),
'stack_trace' => $header."\n[stacktrace]\n".$this->cleanStackTrace($exception->getTraceAsString()),
'full_stack_trace' => $header."\n[stacktrace]\n".$exception->getTraceAsString(),
'stack_trace' => $header . "\n[stacktrace]\n" . $this->cleanStackTrace($exception->getTraceAsString()),
'full_stack_trace' => $header . "\n[stacktrace]\n" . $exception->getTraceAsString(),
];
}

Expand Down
12 changes: 7 additions & 5 deletions src/GithubIssueHandlerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\File;
use InvalidArgumentException;
use Monolog\Handler\DeduplicationHandler;
use Monolog\Level;
use Monolog\Logger;
use Naoray\LaravelGithubMonolog\Formatters\GithubIssueFormatter;
use Naoray\LaravelGithubMonolog\Handlers\SignatureDeduplicationHandler;
use Naoray\LaravelGithubMonolog\Handlers\IssueLogHandler;

class GithubIssueHandlerFactory
{
Expand All @@ -32,9 +34,9 @@ protected function validateConfig(array $config): void
}
}

protected function createBaseHandler(array $config): GithubIssueLoggerHandler
protected function createBaseHandler(array $config): IssueLogHandler
{
$handler = new GithubIssueLoggerHandler(
$handler = new IssueLogHandler(
repo: $config['repo'],
token: $config['token'],
labels: $config['labels'] ?? [],
Expand All @@ -47,9 +49,9 @@ protected function createBaseHandler(array $config): GithubIssueLoggerHandler
return $handler;
}

protected function wrapWithDeduplication(GithubIssueLoggerHandler $handler, array $config): DeduplicationHandler
protected function wrapWithDeduplication(IssueLogHandler $handler, array $config): SignatureDeduplicationHandler
{
return new DeduplicationHandler(
return new SignatureDeduplicationHandler(
$handler,
deduplicationStore: $this->getDeduplicationStore($config),
deduplicationLevel: Arr::get($config, 'level', Level::Error),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
<?php

namespace Naoray\LaravelGithubMonolog;
namespace Naoray\LaravelGithubMonolog\Handlers;

use Illuminate\Support\Facades\Http;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Formatters\GithubIssueFormatted;

class GithubIssueLoggerHandler extends AbstractProcessingHandler
class IssueLogHandler extends AbstractProcessingHandler
{
private string $repo;

private string $token;

private array $labels;

private const DEFAULT_LABEL = 'github-issue-logger';

/**
Expand Down Expand Up @@ -66,11 +64,11 @@ private function findExistingIssue(string $signature): ?array
{
$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: {$signature}\"",
'q' => "repo:{$this->repo} is:issue is:open label:" . self::DEFAULT_LABEL . " \"Signature: {$signature}\"",
]);

if ($response->failed()) {
throw new \RuntimeException('Failed to search GitHub issues: '.$response->body());
throw new \RuntimeException('Failed to search GitHub issues: ' . $response->body());
}

return $response->json('items.0', null);
Expand All @@ -87,7 +85,7 @@ private function commentOnIssue(int $issueNumber, GithubIssueFormatted $formatte
]);

if ($response->failed()) {
throw new \RuntimeException('Failed to comment on GitHub issue: '.$response->body());
throw new \RuntimeException('Failed to comment on GitHub issue: ' . $response->body());
}
}

Expand All @@ -104,7 +102,7 @@ private function createIssue(GithubIssueFormatted $formatted): void
]);

if ($response->failed()) {
throw new \RuntimeException('Failed to create GitHub issue: '.$response->body());
throw new \RuntimeException('Failed to create GitHub issue: ' . $response->body());
}
}
}
54 changes: 54 additions & 0 deletions src/Handlers/SignatureDeduplicationHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Naoray\LaravelGithubMonolog\Handlers;

use Monolog\Handler\DeduplicationHandler;
use Monolog\Handler\HandlerInterface;
use Monolog\Level;
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\Contracts\SignatureGenerator;
use Naoray\LaravelGithubMonolog\DefaultSignatureGenerator;

class SignatureDeduplicationHandler extends DeduplicationHandler
{
private SignatureGenerator $signatureGenerator;

public function __construct(
HandlerInterface $handler,
?string $deduplicationStore = null,
int|string|Level $deduplicationLevel = Level::Error,
int $time = 60,
bool $bubble = true,
?SignatureGenerator $signatureGenerator = null,
) {
parent::__construct($handler, $deduplicationStore, $deduplicationLevel, $time, $bubble);
$this->signatureGenerator = $signatureGenerator ?? new DefaultSignatureGenerator();
}

/**
* Override isDuplicate to use our signature-based deduplication
*/
protected function isDuplicate(array $store, LogRecord $record): bool
{
$timestampValidity = $record->datetime->getTimestamp() - $this->time;
$signature = $this->signatureGenerator->generate($record);

foreach ($store as $entry) {
[$timestamp, $storedSignature] = explode(':', $entry, 2);

if ($storedSignature === $signature && $timestamp > $timestampValidity) {
return true;
}
}

return false;
}

/**
* Override store entry format to use our signature
*/
protected function buildDeduplicationStoreEntry(LogRecord $record): string
{
return $record->datetime->getTimestamp() . ':' . $this->signatureGenerator->generate($record);
}
}
87 changes: 87 additions & 0 deletions tests/DefaultSignatureGeneratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

use Monolog\Level;
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\DefaultSignatureGenerator;

beforeEach(function () {
$this->generator = new DefaultSignatureGenerator();
});

test('generates signature from message', function () {
$record = new LogRecord(
datetime: new \DateTimeImmutable(),
channel: 'test',
level: Level::Error,
message: 'Test message',
context: ['foo' => 'bar'],
extra: [],
);

$signature1 = $this->generator->generate($record);
expect($signature1)->toBeString();

// Same message and context should generate same signature
$record2 = new LogRecord(
datetime: new \DateTimeImmutable(),
channel: 'different-channel',
level: Level::Warning,
message: 'Test message',
context: ['foo' => 'bar'],
extra: ['something' => 'else'],
);
$signature2 = $this->generator->generate($record2);
expect($signature2)->toBe($signature1);

// Different message should generate different signature
$record3 = new LogRecord(
datetime: new \DateTimeImmutable(),
channel: 'test',
level: Level::Error,
message: 'Different message',
context: ['foo' => 'bar'],
extra: [],
);
$signature3 = $this->generator->generate($record3);
expect($signature3)->not->toBe($signature1);
});

test('generates signature from exception', function () {
$exception = new \Exception('Test exception');
$record = new LogRecord(
datetime: new \DateTimeImmutable(),
channel: 'test',
level: Level::Error,
message: 'Test message',
context: ['exception' => $exception],
extra: [],
);

$signature1 = $this->generator->generate($record);
expect($signature1)->toBeString();

// Same exception should generate same signature
$record2 = new LogRecord(
datetime: new \DateTimeImmutable(),
channel: 'different-channel',
level: Level::Warning,
message: 'Different message',
context: ['exception' => $exception],
extra: ['something' => 'else'],
);
$signature2 = $this->generator->generate($record2);
expect($signature2)->toBe($signature1);

// Different exception should generate different signature
$differentException = new \Exception('Different exception');
$record3 = new LogRecord(
datetime: new \DateTimeImmutable(),
channel: 'test',
level: Level::Error,
message: 'Test message',
context: ['exception' => $differentException],
extra: [],
);
$signature3 = $this->generator->generate($record3);
expect($signature3)->not->toBe($signature1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use Monolog\Level;
use Monolog\LogRecord;
use Naoray\LaravelGithubMonolog\GithubIssueFormatted;
use Naoray\LaravelGithubMonolog\GithubIssueFormatter;
use Naoray\LaravelGithubMonolog\Formatters\GithubIssueFormatted;
use Naoray\LaravelGithubMonolog\Formatters\GithubIssueFormatter;

test('it formats basic log records', function () {
$formatter = new GithubIssueFormatter;
Expand Down
Loading

0 comments on commit a7905b2

Please sign in to comment.