Skip to content

Commit 0b241ee

Browse files
feat: Add listener for inyect audit context in jobs
1 parent 2217ef7 commit 0b241ee

File tree

4 files changed

+186
-3
lines changed

4 files changed

+186
-3
lines changed

app/Audit/AbstractAuditLogFormatter.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ final public function setContext(AuditContext $ctx): void
3434

3535
protected function getUserInfo(): string
3636
{
37-
if (app()->runningInConsole()) {
38-
return 'Worker Job';
39-
}
37+
4038
if (!$this->ctx) {
4139
return 'Unknown (unknown)';
4240
}

app/Audit/AuditEventListener.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use OAuth2\IResourceServerContext;
2727
use OAuth2\Models\IClient;
2828
use Services\OAuth2\ResourceServerContext;
29+
use OpenTelemetry\API\Baggage\Baggage;
2930

3031
/**
3132
* Class AuditEventListener
@@ -99,6 +100,17 @@ private function getAuditStrategy($em): ?IAuditStrategy
99100

100101
private function buildAuditContext(): AuditContext
101102
{
103+
104+
if (app()->runningInConsole()) {
105+
Log::debug('AuditEventListener::buildAuditContext - running in console, attempting to get baggage context');
106+
$contextFromBaggage = $this->buildContextFromBaggage();
107+
if ($contextFromBaggage) {
108+
Log::debug('AuditEventListener::buildAuditContext - context successfully loaded from baggage');
109+
return $contextFromBaggage;
110+
}
111+
Log::debug('AuditEventListener::buildAuditContext - failed to load context from baggage, will use current request context');
112+
}
113+
102114
/***
103115
* here we have 2 cases
104116
* 1. we are connecting to the IDP using an external APi ( under oauth2 ) so the
@@ -157,4 +169,77 @@ private function buildAuditContext(): AuditContext
157169
rawRoute: $rawRoute
158170
);
159171
}
172+
173+
/**
174+
* Rebuild audit context from OpenTelemetry Baggage (propagated from request to job)
175+
*/
176+
private function buildContextFromBaggage(): ?AuditContext
177+
{
178+
try {
179+
$baggage = Baggage::getCurrent();
180+
181+
Log::debug('AuditEventListener::buildContextFromBaggage - baggage obtained', [
182+
'baggage_class' => get_class($baggage),
183+
]);
184+
185+
$userIdEntry = $baggage->getEntry('audit.userId');
186+
Log::debug('AuditEventListener::buildContextFromBaggage - userId entry', [
187+
'entry_exists' => $userIdEntry !== null,
188+
'entry_class' => $userIdEntry ? get_class($userIdEntry) : 'null',
189+
]);
190+
191+
$userId = $userIdEntry ? $userIdEntry->getValue() : null;
192+
193+
Log::debug('AuditEventListener::buildContextFromBaggage - userId value', [
194+
'userId' => $userId,
195+
'userId_type' => gettype($userId),
196+
'isEmpty' => empty($userId),
197+
]);
198+
199+
if (!$userId) {
200+
Log::debug('AuditEventListener: no userId in baggage');
201+
return null;
202+
}
203+
204+
$userEmail = $baggage->getEntry('audit.userEmail')?->getValue();
205+
$userFirstName = $baggage->getEntry('audit.userFirstName')?->getValue();
206+
$userLastName = $baggage->getEntry('audit.userLastName')?->getValue();
207+
$route = $baggage->getEntry('audit.route')?->getValue();
208+
$httpMethod = $baggage->getEntry('audit.httpMethod')?->getValue();
209+
$clientIp = $baggage->getEntry('audit.clientIp')?->getValue();
210+
$userAgent = $baggage->getEntry('audit.userAgent')?->getValue();
211+
212+
Log::debug('AuditEventListener::buildContextFromBaggage - extracted values', [
213+
'userId' => $userId,
214+
'userEmail' => $userEmail,
215+
'userFirstName' => $userFirstName,
216+
'userLastName' => $userLastName,
217+
'route' => $route,
218+
'httpMethod' => $httpMethod,
219+
'clientIp' => $clientIp,
220+
'userAgent' => $userAgent,
221+
]);
222+
223+
$auditContext = new AuditContext(
224+
userId: (int)$userId > 0 ? (int)$userId : null,
225+
userEmail: $userEmail,
226+
userFirstName: $userFirstName,
227+
userLastName: $userLastName,
228+
route: $route,
229+
httpMethod: $httpMethod,
230+
clientIp: $clientIp,
231+
userAgent: $userAgent,
232+
);
233+
234+
Log::debug('AuditEventListener::buildContextFromBaggage - context created successfully');
235+
236+
return $auditContext;
237+
} catch (\Exception $e) {
238+
Log::debug('AuditEventListener: could not build context from baggage', [
239+
'error' => $e->getMessage(),
240+
'trace' => $e->getTraceAsString(),
241+
]);
242+
return null;
243+
}
244+
}
160245
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
namespace App\Listeners;
3+
4+
use App\Audit\AuditContext;
5+
use Illuminate\Queue\Events\JobQueued;
6+
use Illuminate\Support\Facades\Auth;
7+
use Illuminate\Support\Facades\Log;
8+
use OAuth2\IResourceServerContext;
9+
use Auth\Repositories\IUserRepository;
10+
use OpenTelemetry\API\Baggage\Baggage;
11+
12+
class CaptureJobAuditContextListener
13+
{
14+
/**
15+
* Handle the event.
16+
*/
17+
public function handle(JobQueued $event): void
18+
{
19+
try {
20+
$context = $this->buildAuditContextFromCurrentRequest();
21+
22+
if (!$context) {
23+
Log::warning('CaptureJobAuditContextListener: could not build audit context');
24+
return;
25+
}
26+
27+
$this->storeBaggageContext($context);
28+
29+
Log::debug('CaptureJobAuditContextListener: audit context captured for job', [
30+
'user_id' => $context->userId,
31+
'user_email' => $context->userEmail,
32+
]);
33+
} catch (\Exception $e) {
34+
Log::warning('CaptureJobAuditContextListener failed', [
35+
'error' => $e->getMessage(),
36+
'trace' => $e->getTraceAsString(),
37+
]);
38+
}
39+
}
40+
41+
private function buildAuditContextFromCurrentRequest(): ?AuditContext
42+
{
43+
$resource_server_context = app(IResourceServerContext::class);
44+
$oauth2_current_client_id = $resource_server_context->getCurrentClientId();
45+
$userId = $oauth2_current_client_id = 1;
46+
if (!empty($oauth2_current_client_id)) {
47+
$userId = 1;
48+
$user = $userId ? app(IUserRepository::class)->getById($userId) : null;
49+
} else {
50+
$user = Auth::user();
51+
}
52+
if (!$user) {
53+
return null;
54+
}
55+
56+
$req = request();
57+
58+
return new AuditContext(
59+
userId: $user->getId(),
60+
userEmail: $user->getEmail(),
61+
userFirstName: $user->getFirstName(),
62+
userLastName: $user->getLastName(),
63+
route: $req?->path(),
64+
httpMethod: $req?->method(),
65+
clientIp: $req?->ip(),
66+
userAgent: $req?->userAgent(),
67+
);
68+
}
69+
70+
/**
71+
* Store the audit context in OpenTelemetry Baggage for queue propagation
72+
*/
73+
private function storeBaggageContext(AuditContext $context): void
74+
{
75+
try {
76+
$baggage = Baggage::getCurrent()
77+
->toBuilder()
78+
->set('audit.userId', (string)($context->userId ?? ''))
79+
->set('audit.userEmail', $context->userEmail ?? '')
80+
->set('audit.userFirstName', $context->userFirstName ?? '')
81+
->set('audit.userLastName', $context->userLastName ?? '')
82+
->set('audit.route', $context->route ?? '')
83+
->set('audit.httpMethod', $context->httpMethod ?? '')
84+
->set('audit.clientIp', $context->clientIp ?? '')
85+
->set('audit.userAgent', $context->userAgent ?? '')
86+
->build();
87+
88+
$baggage->activate();
89+
90+
Log::debug('CaptureJobAuditContextListener: baggage context stored');
91+
} catch (\Exception $e) {
92+
Log::warning('CaptureJobAuditContextListener: failed to store baggage', [
93+
'error' => $e->getMessage(),
94+
]);
95+
}
96+
}
97+
}

app/Providers/EventServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ final class EventServiceProvider extends ServiceProvider
5757
'Illuminate\Auth\Events\Login' => [
5858
'App\Listeners\OnUserLogin',
5959
],
60+
\Illuminate\Queue\Events\JobQueued::class => [
61+
'App\Listeners\CaptureJobAuditContextListener',
62+
]
6063
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
6164
// ... other providers
6265
'SocialiteProviders\\Facebook\\FacebookExtendSocialite@handle',

0 commit comments

Comments
 (0)