Skip to content

Commit b2ce67f

Browse files
authored
Merge pull request #12 from Naoray/feat/add-tracing-capabilities
feat: add tracing capabilities
2 parents cb89b17 + 836724c commit b2ce67f

File tree

7 files changed

+279
-41
lines changed

7 files changed

+279
-41
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Automatically create GitHub issues from your Laravel exceptions & logs. Perfect
2222
- 🎯 Smart deduplication to prevent issue spam
2323
- ⚡️ Buffered logging for better performance
2424
- 📝 Customizable issue templates
25+
- 🕵🏻‍♂️ Tracing Support (Request & User)
2526

2627
## Showcase
2728

@@ -168,6 +169,42 @@ When buffering is active:
168169
- With `flush_on_overflow = true`: All records are flushed
169170
- With `flush_on_overflow = false`: Only the oldest record is removed
170171

172+
### Tracing
173+
174+
The package includes optional tracing capabilities that allow you to track requests and user data in your logs. Enable this feature through your configuration:
175+
176+
```php
177+
'tracing' => [
178+
'enabled' => true, // Master switch for all tracing
179+
'requests' => true, // Enable request tracing
180+
'user' => true, // Enable user tracing
181+
]
182+
```
183+
184+
#### Request Tracing
185+
When request tracing is enabled, the following data is automatically logged:
186+
- URL
187+
- HTTP Method
188+
- Route information
189+
- Headers (filtered to remove sensitive data)
190+
- Request body
191+
192+
#### User Tracing
193+
By default, user tracing only logs the user identifier to comply with GDPR regulations. However, you can customize the user data being logged by setting your own resolver:
194+
195+
```php
196+
use Naoray\LaravelGithubMonolog\Tracing\UserDataCollector;
197+
198+
UserDataCollector::setUserDataResolver(function ($user) {
199+
return [
200+
'username' => $user->username,
201+
// Add any other user fields you want to log
202+
];
203+
});
204+
```
205+
206+
> **Note:** When customizing user data collection, ensure you comply with relevant privacy regulations and only collect necessary information.
207+
171208
### Signature Generator
172209

173210
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.

src/GithubMonologServiceProvider.php

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,20 @@
22

33
namespace Naoray\LaravelGithubMonolog;
44

5+
use Illuminate\Support\Facades\Event;
56
use Illuminate\Support\ServiceProvider;
6-
use Naoray\LaravelGithubMonolog\Issues\Formatters\ExceptionFormatter;
7-
use Naoray\LaravelGithubMonolog\Issues\Formatters\IssueFormatter;
8-
use Naoray\LaravelGithubMonolog\Issues\Formatters\PreviousExceptionFormatter;
9-
use Naoray\LaravelGithubMonolog\Issues\Formatters\StackTraceFormatter;
10-
use Naoray\LaravelGithubMonolog\Issues\StubLoader;
11-
use Naoray\LaravelGithubMonolog\Issues\TemplateRenderer;
12-
use Naoray\LaravelGithubMonolog\Issues\TemplateSectionCleaner;
7+
use Naoray\LaravelGithubMonolog\Tracing\EventHandler;
138

149
class GithubMonologServiceProvider extends ServiceProvider
1510
{
16-
public function register(): void
11+
public function boot(): void
1712
{
18-
$this->app->bind(StackTraceFormatter::class);
19-
$this->app->bind(StubLoader::class);
20-
$this->app->bind(TemplateSectionCleaner::class);
21-
22-
$this->app->bind(ExceptionFormatter::class, function ($app) {
23-
return new ExceptionFormatter(
24-
stackTraceFormatter: $app->make(StackTraceFormatter::class),
25-
);
26-
});
27-
28-
$this->app->bind(PreviousExceptionFormatter::class, function ($app) {
29-
return new PreviousExceptionFormatter(
30-
exceptionFormatter: $app->make(ExceptionFormatter::class),
31-
stubLoader: $app->make(StubLoader::class),
32-
);
33-
});
34-
35-
$this->app->singleton(TemplateRenderer::class, function ($app) {
36-
return new TemplateRenderer(
37-
exceptionFormatter: $app->make(ExceptionFormatter::class),
38-
previousExceptionFormatter: $app->make(PreviousExceptionFormatter::class),
39-
sectionCleaner: $app->make(TemplateSectionCleaner::class),
40-
stubLoader: $app->make(StubLoader::class),
41-
);
42-
});
13+
$config = config('logging.channels.github.tracing');
4314

44-
$this->app->singleton(IssueFormatter::class, function ($app) {
45-
return new IssueFormatter(
46-
templateRenderer: $app->make(TemplateRenderer::class),
47-
);
48-
});
49-
}
15+
if (isset($config['enabled']) && $config['enabled']) {
16+
Event::subscribe(EventHandler::class);
17+
}
5018

51-
public function boot(): void
52-
{
5319
if ($this->app->runningInConsole()) {
5420
$this->publishes([
5521
__DIR__.'/../resources/views' => resource_path('views/vendor/github-monolog'),

src/Tracing/EventHandler.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Naoray\LaravelGithubMonolog\Tracing;
4+
5+
use Illuminate\Auth\Events\Authenticated;
6+
use Illuminate\Events\Dispatcher;
7+
use Illuminate\Routing\Events\RouteMatched;
8+
9+
class EventHandler
10+
{
11+
public function subscribe(Dispatcher $events)
12+
{
13+
$config = config('logging.channels.github.tracing');
14+
15+
if (isset($config['requests']) && $config['requests']) {
16+
$events->listen(RouteMatched::class, RequestDataCollector::class);
17+
}
18+
19+
if (isset($config['user']) && $config['user']) {
20+
$events->listen(Authenticated::class, UserDataCollector::class);
21+
}
22+
}
23+
}

src/Tracing/RequestDataCollector.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Naoray\LaravelGithubMonolog\Tracing;
4+
5+
use Illuminate\Routing\Events\RouteMatched;
6+
use Illuminate\Support\Facades\Context;
7+
use Illuminate\Support\Str;
8+
9+
class RequestDataCollector
10+
{
11+
public function __invoke(RouteMatched $event): void
12+
{
13+
$route = $event->route;
14+
$request = $event->request;
15+
16+
Context::add('request', [
17+
'url' => $request->url(),
18+
'method' => $request->method(),
19+
'route' => $route->getName(),
20+
'headers' => $this->filterSensitiveHeaders($request->headers->all()),
21+
'body' => $request->all(),
22+
]);
23+
}
24+
25+
private function filterSensitiveHeaders(array $headers): array
26+
{
27+
$sensitiveHeaders = $this->getSensitiveHeaders();
28+
29+
return collect($headers)
30+
->map(function ($value, $key) use ($sensitiveHeaders) {
31+
if (Str::is($sensitiveHeaders, $key, true)) {
32+
return ['[FILTERED]'];
33+
}
34+
35+
return $value;
36+
})
37+
->toArray();
38+
}
39+
40+
private function getSensitiveHeaders(): array
41+
{
42+
$sensitiveHeaders = [
43+
config('session.cookie'),
44+
'remember_*',
45+
'XSRF-TOKEN',
46+
'cookie',
47+
];
48+
49+
return collect($sensitiveHeaders)
50+
->merge(collect($sensitiveHeaders)->map(fn ($header) => str($header)->replace('_', '-'))->toArray())
51+
->unique()
52+
->toArray();
53+
}
54+
}

src/Tracing/UserDataCollector.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Naoray\LaravelGithubMonolog\Tracing;
4+
5+
use Illuminate\Auth\Events\Authenticated;
6+
use Illuminate\Contracts\Auth\Authenticatable;
7+
use Illuminate\Support\Facades\Context;
8+
9+
class UserDataCollector
10+
{
11+
private static $userDataResolver = null;
12+
13+
public static function setUserDataResolver(callable $resolver): void
14+
{
15+
self::$userDataResolver = $resolver;
16+
}
17+
18+
public function __invoke(Authenticated $event): void
19+
{
20+
Context::add(
21+
'user',
22+
$this->getUserDataResolver()($event->user)
23+
);
24+
}
25+
26+
public function getUserDataResolver(): ?callable
27+
{
28+
return self::$userDataResolver
29+
?? fn (Authenticatable $user) => ['id' => $user->getAuthIdentifier()];
30+
}
31+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
use Illuminate\Http\Request;
4+
use Illuminate\Routing\Events\RouteMatched;
5+
use Illuminate\Routing\Route;
6+
use Illuminate\Support\Facades\Context;
7+
use Naoray\LaravelGithubMonolog\Tracing\RequestDataCollector;
8+
use Symfony\Component\HttpFoundation\HeaderBag;
9+
10+
beforeEach(function () {
11+
$this->collector = new RequestDataCollector;
12+
});
13+
14+
afterEach(function () {
15+
Context::flush();
16+
});
17+
18+
it('collects request data', function () {
19+
// Arrange
20+
$request = Mockery::mock(Request::class);
21+
$route = Mockery::mock(Route::class);
22+
$headers = new HeaderBag([
23+
'accept' => ['application/json'],
24+
'cookie' => ['sensitive-cookie'],
25+
'x-custom' => ['custom-value'],
26+
]);
27+
28+
$request->headers = $headers;
29+
$request->shouldReceive('url')->once()->andReturn('https://example.com/test');
30+
$request->shouldReceive('method')->once()->andReturn('POST');
31+
$request->shouldReceive('all')->once()->andReturn(['key' => 'value']);
32+
$route->shouldReceive('getName')->once()->andReturn('test.route');
33+
34+
$event = new RouteMatched($route, $request);
35+
36+
// Act
37+
($this->collector)($event);
38+
39+
// Assert
40+
expect(Context::get('request'))->toBe([
41+
'url' => 'https://example.com/test',
42+
'method' => 'POST',
43+
'route' => 'test.route',
44+
'headers' => [
45+
'accept' => ['application/json'],
46+
'cookie' => ['[FILTERED]'],
47+
'x-custom' => ['custom-value'],
48+
],
49+
'body' => ['key' => 'value'],
50+
]);
51+
});
52+
53+
it('filters sensitive headers', function () {
54+
// Arrange
55+
$request = Mockery::mock(Request::class);
56+
$route = Mockery::mock(Route::class);
57+
$headers = new HeaderBag([
58+
'XSRF-TOKEN' => ['token123'],
59+
'remember_web_123' => ['sensitive'],
60+
'laravel_session' => ['session123'],
61+
'safe-header' => ['value'],
62+
]);
63+
64+
$request->headers = $headers;
65+
$request->shouldReceive('url')->once()->andReturn('https://example.com/test');
66+
$request->shouldReceive('method')->once()->andReturn('GET');
67+
$request->shouldReceive('all')->once()->andReturn([]);
68+
$route->shouldReceive('getName')->once()->andReturn('test.route');
69+
70+
config(['session.cookie' => 'laravel_session']);
71+
$event = new RouteMatched($route, $request);
72+
73+
// Act
74+
($this->collector)($event);
75+
76+
// Assert
77+
expect(Context::get('request')['headers'])->toBe([
78+
'xsrf-token' => ['[FILTERED]'],
79+
'remember-web-123' => ['[FILTERED]'],
80+
'laravel-session' => ['[FILTERED]'],
81+
'safe-header' => ['value'],
82+
]);
83+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
use Illuminate\Auth\Events\Authenticated;
4+
use Illuminate\Contracts\Auth\Authenticatable;
5+
use Illuminate\Support\Facades\Context;
6+
use Naoray\LaravelGithubMonolog\Tracing\UserDataCollector;
7+
8+
beforeEach(function () {
9+
$this->collector = new UserDataCollector;
10+
});
11+
12+
afterEach(function () {
13+
// Reset to default resolver
14+
UserDataCollector::setUserDataResolver(fn (Authenticatable $user) => ['id' => $user->getAuthIdentifier()]);
15+
Context::flush();
16+
});
17+
18+
it('collects default user data', function () {
19+
// Arrange
20+
$user = Mockery::mock(Authenticatable::class);
21+
$user->shouldReceive('getAuthIdentifier')->once()->andReturn(1);
22+
$event = new Authenticated('web', $user);
23+
24+
// Act
25+
($this->collector)($event);
26+
27+
// Assert
28+
expect(Context::get('user'))->toBe(['id' => 1]);
29+
});
30+
31+
it('uses custom user data resolver', function () {
32+
// Arrange
33+
$user = Mockery::mock(Authenticatable::class);
34+
$user->shouldReceive('getAuthIdentifier')->never();
35+
$event = new Authenticated('web', $user);
36+
37+
UserDataCollector::setUserDataResolver(fn ($user) => ['custom' => 'data']);
38+
39+
// Act
40+
($this->collector)($event);
41+
42+
// Assert
43+
expect(Context::get('user'))->toBe(['custom' => 'data']);
44+
});

0 commit comments

Comments
 (0)