From c94fc680defeeb07ddd0b46a31b5cd8a5160428e Mon Sep 17 00:00:00 2001 From: smarcet Date: Thu, 2 Oct 2025 13:23:36 -0300 Subject: [PATCH 1/6] chore: add to table SummitAttendeeTicket fild summit id to improve performance chore: add needed IDX --- .../Attendees/SummitAttendeeTicket.php | 3 ++ ...DoctrineSummitAttendeeTicketRepository.php | 2 +- .../model/Version20251002160949.php | 47 +++++++++++++++++++ .../model/Version20251002160950.php | 38 +++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 database/migrations/model/Version20251002160949.php create mode 100644 database/migrations/model/Version20251002160950.php diff --git a/app/Models/Foundation/Summit/Registration/Attendees/SummitAttendeeTicket.php b/app/Models/Foundation/Summit/Registration/Attendees/SummitAttendeeTicket.php index 5f03c50a8..9116f8bfe 100644 --- a/app/Models/Foundation/Summit/Registration/Attendees/SummitAttendeeTicket.php +++ b/app/Models/Foundation/Summit/Registration/Attendees/SummitAttendeeTicket.php @@ -39,6 +39,8 @@ class SummitAttendeeTicket extends SilverstripeBaseModel implements IQREntity { + use SummitOwned; + use FinancialTrait; /** @@ -360,6 +362,7 @@ public function getOrder(): SummitOrder public function setOrder(SummitOrder $order): void { $this->order = $order; + $this->setSummit($order->getSummit()); } /** diff --git a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php index e17b23ef1..1dc65ce47 100644 --- a/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php +++ b/app/Repositories/Summit/DoctrineSummitAttendeeTicketRepository.php @@ -53,7 +53,7 @@ public function __construct(EntityManagerInterface $em, ClassMetadata $class) /** @var array}> */ private array $joinCatalog = [ 'o' => ['e.order', 'join', []], - 's' => ['o.summit', 'join', ['o']], + 's' => ['e.summit', 'join', []], 'ord_m' => ['o.owner', 'leftJoin', ['o']], 'a' => ['e.owner', 'leftJoin', []], 'a_c' => ['a.company', 'leftJoin', ['a']], diff --git a/database/migrations/model/Version20251002160949.php b/database/migrations/model/Version20251002160949.php new file mode 100644 index 000000000..7cbc7c451 --- /dev/null +++ b/database/migrations/model/Version20251002160949.php @@ -0,0 +1,47 @@ +hasTable("SummitAttendeeTicket") && !$builder->hasColumn("SummitAttendeeTicket", "SummitID")) { + $builder->table("SummitAttendeeTicket", function (Table $table) { + $table->integer("SummitID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("SummitID", "IDX_SummitAttendeeTicket_SummitID"); + $table->index(["SummitID","ID",], "IDX_SummitAttendeeTicket_SummitID_ID"); + $table->index(["OrderID","ID",], "IDX_SummitAttendeeTicket_OrderID_ID"); + $table->foreign("Summit", "SummitID", "ID", ["onDelete" => "CASCADE"], "FK_SummitAttendeeTicket_Summit"); + }); + } + + } + + public function down(Schema $schema): void + { + $builder = new Builder($schema); + if($schema->hasTable("SummitAttendeeTicket") && $builder->hasColumn("SummitAttendeeTicket", "SummitID")) { + $builder->table("SummitAttendeeTicket", function (Table $table) { + $table->dropColumn('SummitID'); + }); + } + + } +} diff --git a/database/migrations/model/Version20251002160950.php b/database/migrations/model/Version20251002160950.php new file mode 100644 index 000000000..f3b86eba1 --- /dev/null +++ b/database/migrations/model/Version20251002160950.php @@ -0,0 +1,38 @@ +addSql($sql); + + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + + } +} From 7ad4b4c58cc4263c6c7b82cb0352721c90129748 Mon Sep 17 00:00:00 2001 From: Jose Andres Tejerina Date: Mon, 29 Sep 2025 20:13:26 -0300 Subject: [PATCH 2/6] feat: add log audit strategy for replace audit in BBDD --- .env.example | 2 +- app/Audit/AuditEventListener.php | 71 ++++-- app/Audit/AuditLogOtlpStrategy.php | 217 ++++++++++++++++++ app/Audit/Interfaces/IAuditStrategy.php | 21 ++ .../opentelemetry/otel-collector-config.yaml | 2 +- 5 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 app/Audit/AuditLogOtlpStrategy.php create mode 100644 app/Audit/Interfaces/IAuditStrategy.php diff --git a/.env.example b/.env.example index 81c62d235..0ccc2184e 100644 --- a/.env.example +++ b/.env.example @@ -246,7 +246,7 @@ OTEL_TRACES_SAMPLER_PARENT=false # OTEL_INSTRUMENTATION_VIEW=true # OTEL_INSTRUMENTATION_LIVEWIRE=true # OTEL_INSTRUMENTATION_CONSOLE=true - +AUDIT_ELASTICSEARCH_INDEX=logs-audit # SWAGGER CONFIG L5_SWAGGER_CONST_HOST=${APP_URL} diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index 0410f51e7..dd536afe4 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -15,36 +15,79 @@ * limitations under the License. **/ +use App\Audit\AuditLogOtlpStrategy; use Doctrine\ORM\Event\OnFlushEventArgs; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Log; /** * Class AuditEventListener * @package App\Audit */ -class AuditEventListener { +class AuditEventListener +{ public function onFlush(OnFlushEventArgs $eventArgs): void { $em = $eventArgs->getObjectManager(); $uow = $em->getUnitOfWork(); + // Strategy selection based on environment configuration + $strategy = $this->getAuditStrategy($em); - $strategy = new AuditLogStrategy($em); - - foreach ($uow->getScheduledEntityInsertions() as $entity) { - $strategy->audit($entity, null, $strategy::EVENT_ENTITY_CREATION); + if (!$strategy) { + return; // No audit strategy enabled } - foreach ($uow->getScheduledEntityUpdates() as $entity) { - $change_set = $uow->getEntityChangeSet($entity); - $strategy->audit($entity, $change_set, $strategy::EVENT_ENTITY_UPDATE); - } + try { + foreach ($uow->getScheduledEntityInsertions() as $entity) { + + $strategy->audit($entity, [], $strategy::EVENT_ENTITY_CREATION); + } - foreach ($uow->getScheduledEntityDeletions() as $entity) { - $strategy->audit($entity, null, $strategy::EVENT_ENTITY_DELETION); + foreach ($uow->getScheduledEntityUpdates() as $entity) { + $change_set = $uow->getEntityChangeSet($entity); + $strategy->audit($entity, $change_set, $strategy::EVENT_ENTITY_UPDATE); + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + $strategy->audit($entity, [], $strategy::EVENT_ENTITY_DELETION); + } + + foreach ($uow->getScheduledCollectionUpdates() as $col) { + $strategy->audit($col, [], $strategy::EVENT_COLLECTION_UPDATE); + } + } catch (\Exception $e) { + Log::error('Audit event listener failed', [ + 'error' => $e->getMessage(), + 'strategy_class' => get_class($strategy), + 'trace' => $e->getTraceAsString(), + ]); } + } - foreach ($uow->getScheduledCollectionUpdates() as $col) { - $strategy->audit($col, null, $strategy::EVENT_COLLECTION_UPDATE); + /** + * Get the appropriate audit strategy based on environment configuration + */ + private function getAuditStrategy($em) + { + + // Check if OTLP audit is enabled + if (env('OTEL_SERVICE_ENABLED', false)) { + try { + return App::make(AuditLogOtlpStrategy::class); + } catch (\Exception $e) { + Log::warning('Failed to create OTLP audit strategy, falling back to database', [ + 'error' => $e->getMessage() + ]); + // Fall through to database strategy + } + } else { + return new AuditLogStrategy($em); } + + + Log::warning('No audit strategy enabled'); + + return null; } -} \ No newline at end of file +} diff --git a/app/Audit/AuditLogOtlpStrategy.php b/app/Audit/AuditLogOtlpStrategy.php new file mode 100644 index 000000000..e6e08cf9c --- /dev/null +++ b/app/Audit/AuditLogOtlpStrategy.php @@ -0,0 +1,217 @@ +enabled = env('OTEL_SERVICE_ENABLED', false); + + $this->elasticIndex = env('AUDIT_ELASTICSEARCH_INDEX', 'logs-audit'); + } + + public function audit($subject, array $change_set, string $event_type): void + { + if (!$this->enabled) { + return; + } + + try { + $entity = $this->resolveAuditableEntity($subject); + if (is_null($entity)) return; + + $resource_server_ctx = App::make(\models\oauth2\IResourceServerContext::class); + $user_id = $resource_server_ctx->getCurrentUserId(); + $user_email = $resource_server_ctx->getCurrentUserEmail(); + + $formatter = null; + switch ($event_type) { + case self::EVENT_COLLECTION_UPDATE: + $child_entity = null; + if (count($subject) > 0) { + $child_entity = $subject[0]; + } + if (is_null($child_entity) && count($subject->getSnapshot()) > 0) { + $child_entity = $subject->getSnapshot()[0]; + } + $child_entity_formatter = $child_entity != null ? ChildEntityFormatterFactory::build($child_entity) : null; + $formatter = new EntityCollectionUpdateAuditLogFormatter($child_entity_formatter); + break; + case self::EVENT_ENTITY_CREATION: + $formatter = new EntityCreationAuditLogFormatter(); + break; + case self::EVENT_ENTITY_DELETION: + $child_entity_formatter = ChildEntityFormatterFactory::build($subject); + $formatter = new EntityDeletionAuditLogFormatter($child_entity_formatter); + break; + case self::EVENT_ENTITY_UPDATE: + $child_entity_formatter = ChildEntityFormatterFactory::build($subject); + $formatter = new EntityUpdateAuditLogFormatter($child_entity_formatter); + break; + } + + $description = null; + if ($formatter) { + $description = $formatter->format($subject, $change_set); + } + + $auditData = $this->buildAuditLogData($entity, $subject, $change_set, $event_type, $user_id, $user_email); + if (!empty($description)) { + $auditData['audit.description'] = $description; + } + Logger::info($this->getLogMessage($event_type), $auditData); + + } catch (\Exception $ex) { + Logger::warning('OTEL audit logging error: ' . $ex->getMessage(), [ + 'exception' => $ex, + 'subject_class' => get_class($subject), + 'event_type' => $event_type, + ]); + } + } + + private function resolveAuditableEntity($subject) + { + if ($subject instanceof SummitEvent) return $subject; + + if ($subject instanceof PersistentCollection && $subject->getOwner() instanceof SummitEvent) { + return $subject->getOwner(); + } + + if ($subject instanceof PresentationAction || $subject instanceof PresentationExtraQuestionAnswer) { + return $subject->getPresentation(); + } + + if ($subject instanceof SummitAttendeeBadgePrint) { + return $subject->getBadge(); + } + + return null; + } + + private function buildAuditLogData($entity, $subject, array $change_set, string $event_type, ?string $user_id, ?string $user_email): array + { + $auditData = [ + 'audit.action' => $this->mapEventTypeToAction($event_type), + 'audit.entity' => class_basename($entity), + 'audit.entity_id' => (string) (method_exists($entity, 'getId') ? $entity->getId() : 'unknown'), + 'audit.entity_class' => get_class($entity), + 'audit.timestamp' => now()->toISOString(), + 'audit.event_type' => $event_type, + 'auth.user.id' => $user_id, + 'auth.user.email' => $user_email, + 'elasticsearch.index' => $this->elasticIndex, + ]; + + switch ($event_type) { + case self::EVENT_ENTITY_CREATION: + $auditData['audit.created_data'] = $this->getEntityData($entity); + break; + case self::EVENT_ENTITY_DELETION: + $auditData['audit.deleted_data'] = $this->getEntityData($subject); + break; + case self::EVENT_COLLECTION_UPDATE: + if ($subject instanceof PersistentCollection) { + $auditData['audit.collection_type'] = $this->getCollectionType($subject); + $auditData['audit.collection_count'] = count($subject); + $auditData['audit.collection_changes'] = $this->getCollectionChanges($subject, $change_set); + } + break; + } + + return $auditData; + } + + private function getEntityData($entity): array + { + $data = ['class' => get_class($entity)]; + + if (method_exists($entity, 'getId')) { + $data['id'] = $entity->getId(); + } + + if (method_exists($entity, 'getTitle')) { + $data['title'] = $entity->getTitle(); + } + + if (method_exists($entity, 'getName')) { + $data['name'] = $entity->getName(); + } + + if (method_exists($entity, 'getSlug')) { + $data['slug'] = $entity->getSlug(); + } + + return $data; + } + + private function getCollectionType(PersistentCollection $collection): string + { + if (empty($collection) && empty($collection->getSnapshot())) { + return 'unknown'; + } + + $item = !empty($collection) ? $collection->first() : $collection->getSnapshot()[0]; + return class_basename($item); + } + + private function getCollectionChanges(PersistentCollection $collection, array $change_set): array + { + return [ + 'current_count' => count($collection), + 'snapshot_count' => count($collection->getSnapshot()), + 'is_dirty' => $collection->isDirty(), + ]; + } + + private function mapEventTypeToAction(string $event_type): string + { + return match($event_type) { + self::EVENT_ENTITY_CREATION => 'create', + self::EVENT_ENTITY_UPDATE => 'update', + self::EVENT_ENTITY_DELETION => 'delete', + self::EVENT_COLLECTION_UPDATE => 'collection_update', + default => 'unknown' + }; + } + + private function getLogMessage(string $event_type): string + { + return match($event_type) { + self::EVENT_ENTITY_CREATION => 'audit.entity.created', + self::EVENT_ENTITY_UPDATE => 'audit.entity.updated', + self::EVENT_ENTITY_DELETION => 'audit.entity.deleted', + self::EVENT_COLLECTION_UPDATE => 'audit.collection.updated', + default => 'audit.entity.changed' + }; + } + + +} \ No newline at end of file diff --git a/app/Audit/Interfaces/IAuditStrategy.php b/app/Audit/Interfaces/IAuditStrategy.php new file mode 100644 index 000000000..78b018bce --- /dev/null +++ b/app/Audit/Interfaces/IAuditStrategy.php @@ -0,0 +1,21 @@ + [old, new]) + * @param string $event_type Type of audit event (create, update, delete, collection_update) + * @return void + */ + public function audit($subject, array $change_set, string $event_type): void; + +} \ No newline at end of file diff --git a/docker-compose/opentelemetry/otel-collector-config.yaml b/docker-compose/opentelemetry/otel-collector-config.yaml index 74d050717..9b277012e 100755 --- a/docker-compose/opentelemetry/otel-collector-config.yaml +++ b/docker-compose/opentelemetry/otel-collector-config.yaml @@ -130,7 +130,7 @@ service: logs: receivers: [otlp] processors: [memory_limiter, resource, attributes, batch] - exporters: [debug, file] + exporters: [debug, file, elasticsearch] # Telemetry configuration for the collector itself telemetry: From 5b430dd9b7965e59e5d7ae8e3569db77268ea6a3 Mon Sep 17 00:00:00 2001 From: Jose Andres Tejerina Date: Wed, 1 Oct 2025 12:32:05 -0300 Subject: [PATCH 3/6] feat: Add unit test for Audit Otlp Strategy --- .env.example | 2 +- app/Audit/AuditLogOtlpStrategy.php | 2 +- tests/OpenTelemetry/AuditEventTypesTest.php | 119 ++++++++++++ tests/OpenTelemetry/AuditOtlpStrategyTest.php | 181 ++++++++++++++++++ tests/OpenTelemetry/OpenTelemetryTestCase.php | 2 + 5 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 tests/OpenTelemetry/AuditEventTypesTest.php create mode 100644 tests/OpenTelemetry/AuditOtlpStrategyTest.php diff --git a/.env.example b/.env.example index 0ccc2184e..09fd3d1d3 100644 --- a/.env.example +++ b/.env.example @@ -246,7 +246,7 @@ OTEL_TRACES_SAMPLER_PARENT=false # OTEL_INSTRUMENTATION_VIEW=true # OTEL_INSTRUMENTATION_LIVEWIRE=true # OTEL_INSTRUMENTATION_CONSOLE=true -AUDIT_ELASTICSEARCH_INDEX=logs-audit +OTEL_AUDIT_ELASTICSEARCH_INDEX=logs-audit # SWAGGER CONFIG L5_SWAGGER_CONST_HOST=${APP_URL} diff --git a/app/Audit/AuditLogOtlpStrategy.php b/app/Audit/AuditLogOtlpStrategy.php index e6e08cf9c..4c2ac8d5e 100644 --- a/app/Audit/AuditLogOtlpStrategy.php +++ b/app/Audit/AuditLogOtlpStrategy.php @@ -34,7 +34,7 @@ public function __construct() $this->enabled = env('OTEL_SERVICE_ENABLED', false); - $this->elasticIndex = env('AUDIT_ELASTICSEARCH_INDEX', 'logs-audit'); + $this->elasticIndex = env('OTEL_AUDIT_ELASTICSEARCH_INDEX', 'logs-audit'); } public function audit($subject, array $change_set, string $event_type): void diff --git a/tests/OpenTelemetry/AuditEventTypesTest.php b/tests/OpenTelemetry/AuditEventTypesTest.php new file mode 100644 index 000000000..d7886ac2f --- /dev/null +++ b/tests/OpenTelemetry/AuditEventTypesTest.php @@ -0,0 +1,119 @@ +auditStrategy = $this->app->make(AuditLogOtlpStrategy::class); + } + + public function testEntityCreationAudit(): void + { + if (!$this->isOpenTelemetryEnabled()) { + $this->markTestSkipped('OpenTelemetry is disabled'); + } + + $tracer = $this->app->make(\OpenTelemetry\API\Trace\TracerInterface::class); + $span = $tracer->spanBuilder('test.audit.creation')->startSpan(); + $spanScope = $span->activate(); + + try { + $mockEntity = (object) ['id' => 999, 'title' => 'Test Entity']; + $data = ['title' => 'New Entity', 'type' => 'test']; + + $this->auditStrategy->audit( + $mockEntity, + $data, + AuditLogOtlpStrategy::EVENT_ENTITY_CREATION + ); + + $span->setStatus(StatusCode::STATUS_OK, 'Creation audit completed'); + $this->assertTrue(true); + } catch (\Exception $e) { + $span->recordException($e); + throw $e; + } finally { + $span->end(); + $spanScope->detach(); + } + } + + public function testEntityUpdateAudit(): void + { + if (!$this->isOpenTelemetryEnabled()) { + $this->markTestSkipped('OpenTelemetry is disabled'); + } + + $tracer = $this->app->make(\OpenTelemetry\API\Trace\TracerInterface::class); + $span = $tracer->spanBuilder('test.audit.update')->startSpan(); + $spanScope = $span->activate(); + + try { + $mockEntity = (object) ['id' => 999, 'title' => 'Test Entity']; + $data = ['title' => ['Old Title', 'New Title']]; + + $this->auditStrategy->audit( + $mockEntity, + $data, + AuditLogOtlpStrategy::EVENT_ENTITY_UPDATE + ); + + $span->setStatus(StatusCode::STATUS_OK, 'Update audit completed'); + $this->assertTrue(true); + } catch (\Exception $e) { + $span->recordException($e); + throw $e; + } finally { + $span->end(); + $spanScope->detach(); + } + } + + public function testEntityDeletionAudit(): void + { + if (!$this->isOpenTelemetryEnabled()) { + $this->markTestSkipped('OpenTelemetry is disabled'); + } + + $tracer = $this->app->make(\OpenTelemetry\API\Trace\TracerInterface::class); + $span = $tracer->spanBuilder('test.audit.deletion')->startSpan(); + $spanScope = $span->activate(); + + try { + $mockEntity = (object) ['id' => 999, 'title' => 'Test Entity']; + $data = ['deleted_id' => 999, 'reason' => 'Test cleanup']; + + $this->auditStrategy->audit( + $mockEntity, + $data, + AuditLogOtlpStrategy::EVENT_ENTITY_DELETION + ); + + $span->setStatus(StatusCode::STATUS_OK, 'Deletion audit completed'); + $this->assertTrue(true); + } catch (\Exception $e) { + $span->recordException($e); + throw $e; + } finally { + $span->end(); + $spanScope->detach(); + } + } + + /** + * Check if OpenTelemetry is enabled + */ + private function isOpenTelemetryEnabled(): bool + { + return getenv('OTEL_SERVICE_ENABLED') === 'true'; + } +} diff --git a/tests/OpenTelemetry/AuditOtlpStrategyTest.php b/tests/OpenTelemetry/AuditOtlpStrategyTest.php new file mode 100644 index 000000000..f50d01a14 --- /dev/null +++ b/tests/OpenTelemetry/AuditOtlpStrategyTest.php @@ -0,0 +1,181 @@ +auditStrategy = $this->app->make(AuditLogOtlpStrategy::class); + } + + protected function tearDown(): void + { + self::clearSummitTestData(); + self::clearMemberTestData(); + parent::tearDown(); + } + + public function testAuditSummitChangeWithOtlp(): void + { + $this->skipIfOpenTelemetryDisabled(); + + $tracer = $this->app->make(TracerInterface::class); + $span = $tracer->spanBuilder('test.audit.summit_change')->startSpan(); + $spanScope = $span->activate(); + + try { + $span->addEvent('test.started', [ + 'summit_id' => self::$summit->getId(), + 'summit_name' => self::$summit->getName() + ]); + + $simulatedChangeSet = $this->createSummitChangeSet(); + + $this->auditStrategy->audit( + self::$summit, + $simulatedChangeSet, + AuditLogOtlpStrategy::EVENT_ENTITY_UPDATE + ); + + $span->setStatus(StatusCode::STATUS_OK, 'Summit audit completed'); + $this->assertTrue(true); + + } catch (\Exception $e) { + $span->recordException($e); + $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); + throw $e; + } finally { + $span->end(); + $spanScope->detach(); + } + } + + public function testAuditSummitEventChangeWithOtlp(): void + { + $this->skipIfOpenTelemetryDisabled(); + + $tracer = $this->app->make(TracerInterface::class); + $span = $tracer->spanBuilder('test.audit.summit_event_change')->startSpan(); + $spanScope = $span->activate(); + + try { + $summitEvent = self::$summit->getEvents()[0]; + $simulatedChangeSet = $this->createSummitEventChangeSet($summitEvent); + + $this->auditStrategy->audit( + $summitEvent, + $simulatedChangeSet, + AuditLogOtlpStrategy::EVENT_ENTITY_UPDATE + ); + + $span->setStatus(StatusCode::STATUS_OK, 'SummitEvent audit completed'); + $this->assertTrue(true); + + } catch (\Exception $e) { + $span->recordException($e); + $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); + throw $e; + } finally { + $span->end(); + $spanScope->detach(); + } + } + + public function testAuditStrategyWithoutActiveSpan(): void + { + $this->skipIfOpenTelemetryDisabled(); + + $simulatedChangeSet = ['name' => ['Old Name', 'New Name']]; + + $this->auditStrategy->audit( + self::$summit, + $simulatedChangeSet, + AuditLogOtlpStrategy::EVENT_ENTITY_UPDATE + ); + + $this->assertTrue(true); + } + + public function testAuditStrategyWithEmptyChangeSet(): void + { + $this->skipIfOpenTelemetryDisabled(); + + $tracer = $this->app->make(TracerInterface::class); + $span = $tracer->spanBuilder('test.audit.empty_changeset')->startSpan(); + $spanScope = $span->activate(); + + try { + $this->auditStrategy->audit( + self::$summit, + [], + AuditLogOtlpStrategy::EVENT_ENTITY_UPDATE + ); + + $span->setStatus(StatusCode::STATUS_OK, 'Empty changeset audit completed'); + $this->assertTrue(true); + + } catch (\Exception $e) { + $span->recordException($e); + $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); + throw $e; + } finally { + $span->end(); + $spanScope->detach(); + } + } + + private function skipIfOpenTelemetryDisabled(): void + { + if (!$this->isOpenTelemetryEnabled()) { + $this->markTestSkipped('OpenTelemetry is disabled'); + } + } + + private function createSummitChangeSet(): array + { + return [ + 'name' => [self::$summit->getName(), self::$summit->getName() . ' [TEST]'], + 'description' => ['Original', 'Updated for test'] + ]; + } + + private function createSummitEventChangeSet($summitEvent): array + { + return [ + 'title' => [$summitEvent->getTitle(), $summitEvent->getTitle() . ' [TEST]'] + ]; + } + + private function isOpenTelemetryEnabled(): bool + { + return getenv('OTEL_SERVICE_ENABLED') === 'true'; + } +} diff --git a/tests/OpenTelemetry/OpenTelemetryTestCase.php b/tests/OpenTelemetry/OpenTelemetryTestCase.php index bf9aff8ad..ab3809ab1 100644 --- a/tests/OpenTelemetry/OpenTelemetryTestCase.php +++ b/tests/OpenTelemetry/OpenTelemetryTestCase.php @@ -55,6 +55,8 @@ protected function setOpenTelemetryEnvironmentVariables(): void putenv('OTEL_INSTRUMENTATION_CONSOLE=false'); putenv('OTEL_TRACES_EXPORTER=none'); putenv('OTEL_METRICS_EXPORTER=none'); + putenv('OTEL_LOGS_EXPORTER=none'); + } protected function tearDown(): void From de4d0632fb861b0dac67bcf93f92130ef42fdceb Mon Sep 17 00:00:00 2001 From: Jose Andres Tejerina Date: Fri, 3 Oct 2025 18:49:49 -0300 Subject: [PATCH 4/6] fix: add null checks to EventListener, fix in getAuditStrategy --- app/Audit/AuditEventListener.php | 10 +++------- app/Audit/AuditLogOtlpStrategy.php | 15 ++++++++++++--- tests/OpenTelemetry/AuditOtlpStrategyTest.php | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index dd536afe4..83c0afa15 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -79,15 +79,11 @@ private function getAuditStrategy($em) Log::warning('Failed to create OTLP audit strategy, falling back to database', [ 'error' => $e->getMessage() ]); - // Fall through to database strategy } - } else { - return new AuditLogStrategy($em); } + + // Use database strategy (either as default or fallback) + return new AuditLogStrategy($em); - - Log::warning('No audit strategy enabled'); - - return null; } } diff --git a/app/Audit/AuditLogOtlpStrategy.php b/app/Audit/AuditLogOtlpStrategy.php index 4c2ac8d5e..5e6240adc 100644 --- a/app/Audit/AuditLogOtlpStrategy.php +++ b/app/Audit/AuditLogOtlpStrategy.php @@ -154,11 +154,17 @@ private function getEntityData($entity): array $data = ['class' => get_class($entity)]; if (method_exists($entity, 'getId')) { - $data['id'] = $entity->getId(); + $id = $entity->getId(); + if ($id !== null) { + $data['id'] = $id; + } } if (method_exists($entity, 'getTitle')) { - $data['title'] = $entity->getTitle(); + $title = $entity->getTitle(); + if ($title !== null) { + $data['title'] = $title; + } } if (method_exists($entity, 'getName')) { @@ -166,7 +172,10 @@ private function getEntityData($entity): array } if (method_exists($entity, 'getSlug')) { - $data['slug'] = $entity->getSlug(); + $slug = $entity->getSlug(); + if ($slug !== null) { + $data['slug'] = $slug; + } } return $data; diff --git a/tests/OpenTelemetry/AuditOtlpStrategyTest.php b/tests/OpenTelemetry/AuditOtlpStrategyTest.php index f50d01a14..9d6d98041 100644 --- a/tests/OpenTelemetry/AuditOtlpStrategyTest.php +++ b/tests/OpenTelemetry/AuditOtlpStrategyTest.php @@ -167,7 +167,7 @@ private function createSummitChangeSet(): array ]; } - private function createSummitEventChangeSet($summitEvent): array + private function createSummitEventChangeSet(object $summitEvent): array { return [ 'title' => [$summitEvent->getTitle(), $summitEvent->getTitle() . ' [TEST]'] From 9c4bf521f77b74437bb24fc53923d0934fbdda37 Mon Sep 17 00:00:00 2001 From: Jose Andres Tejerina Date: Fri, 3 Oct 2025 19:29:39 -0300 Subject: [PATCH 5/6] fix: change env function by config function --- app/Audit/AuditEventListener.php | 3 +-- app/Audit/AuditLogOtlpStrategy.php | 11 ++++++----- config/opentelemetry.php | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index 83c0afa15..2ea45d7a3 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -70,9 +70,8 @@ public function onFlush(OnFlushEventArgs $eventArgs): void */ private function getAuditStrategy($em) { - // Check if OTLP audit is enabled - if (env('OTEL_SERVICE_ENABLED', false)) { + if (config('opentelemetry.enabled', false)) { try { return App::make(AuditLogOtlpStrategy::class); } catch (\Exception $e) { diff --git a/app/Audit/AuditLogOtlpStrategy.php b/app/Audit/AuditLogOtlpStrategy.php index 5e6240adc..dcba3c7a7 100644 --- a/app/Audit/AuditLogOtlpStrategy.php +++ b/app/Audit/AuditLogOtlpStrategy.php @@ -31,10 +31,8 @@ class AuditLogOtlpStrategy implements IAuditStrategy public function __construct() { - - $this->enabled = env('OTEL_SERVICE_ENABLED', false); - - $this->elasticIndex = env('OTEL_AUDIT_ELASTICSEARCH_INDEX', 'logs-audit'); + $this->enabled = config('opentelemetry.enabled', false); + $this->elasticIndex = config('opentelemetry.logs.elasticsearch_index', 'logs-audit'); } public function audit($subject, array $change_set, string $event_type): void @@ -168,7 +166,10 @@ private function getEntityData($entity): array } if (method_exists($entity, 'getName')) { - $data['name'] = $entity->getName(); + $name = $entity->getName(); + if ($name !== null) { + $data['name'] = $name; + } } if (method_exists($entity, 'getSlug')) { diff --git a/config/opentelemetry.php b/config/opentelemetry.php index d79d40af2..d64d227ac 100644 --- a/config/opentelemetry.php +++ b/config/opentelemetry.php @@ -85,6 +85,8 @@ * Context field name for trace id */ 'trace_id_field' => 'traceid', + + 'elasticsearch_index' => env('OTEL_LOGS_ELASTICSEARCH_INDEX', 'logs-audit'), ], /** From 2072284e32bc0a706cf369ae5867a65a843c1d85 Mon Sep 17 00:00:00 2001 From: Jose Andres Tejerina Date: Fri, 3 Oct 2025 20:56:16 -0300 Subject: [PATCH 6/6] fix: add improvements in the use of constants, remove code unnecessary --- app/Audit/AuditEventListener.php | 10 ++-- app/Audit/AuditLogOtlpStrategy.php | 77 ++++++++++-------------------- 2 files changed, 29 insertions(+), 58 deletions(-) diff --git a/app/Audit/AuditEventListener.php b/app/Audit/AuditEventListener.php index 2ea45d7a3..85f082ac4 100644 --- a/app/Audit/AuditEventListener.php +++ b/app/Audit/AuditEventListener.php @@ -40,21 +40,19 @@ public function onFlush(OnFlushEventArgs $eventArgs): void try { foreach ($uow->getScheduledEntityInsertions() as $entity) { - - $strategy->audit($entity, [], $strategy::EVENT_ENTITY_CREATION); + $strategy->audit($entity, [], AuditLogOtlpStrategy::EVENT_ENTITY_CREATION); } foreach ($uow->getScheduledEntityUpdates() as $entity) { - $change_set = $uow->getEntityChangeSet($entity); - $strategy->audit($entity, $change_set, $strategy::EVENT_ENTITY_UPDATE); + $strategy->audit($entity, $uow->getEntityChangeSet($entity), AuditLogOtlpStrategy::EVENT_ENTITY_UPDATE); } foreach ($uow->getScheduledEntityDeletions() as $entity) { - $strategy->audit($entity, [], $strategy::EVENT_ENTITY_DELETION); + $strategy->audit($entity, [], AuditLogOtlpStrategy::EVENT_ENTITY_DELETION); } foreach ($uow->getScheduledCollectionUpdates() as $col) { - $strategy->audit($col, [], $strategy::EVENT_COLLECTION_UPDATE); + $strategy->audit($col, [], AuditLogOtlpStrategy::EVENT_COLLECTION_UPDATE); } } catch (\Exception $e) { Log::error('Audit event listener failed', [ diff --git a/app/Audit/AuditLogOtlpStrategy.php b/app/Audit/AuditLogOtlpStrategy.php index dcba3c7a7..0142e8aca 100644 --- a/app/Audit/AuditLogOtlpStrategy.php +++ b/app/Audit/AuditLogOtlpStrategy.php @@ -26,6 +26,18 @@ class AuditLogOtlpStrategy implements IAuditStrategy public const EVENT_ENTITY_DELETION = 'event_entity_deletion'; public const EVENT_ENTITY_UPDATE = 'event_entity_update'; + public const ACTION_CREATE = 'create'; + public const ACTION_UPDATE = 'update'; + public const ACTION_DELETE = 'delete'; + public const ACTION_COLLECTION_UPDATE = 'collection_update'; + public const ACTION_UNKNOWN = 'unknown'; + + public const LOG_MESSAGE_CREATED = 'audit.entity.created'; + public const LOG_MESSAGE_UPDATED = 'audit.entity.updated'; + public const LOG_MESSAGE_DELETED = 'audit.entity.deleted'; + public const LOG_MESSAGE_COLLECTION_UPDATED = 'audit.collection.updated'; + public const LOG_MESSAGE_CHANGED = 'audit.entity.changed'; + private bool $enabled; private string $elasticIndex; @@ -43,7 +55,9 @@ public function audit($subject, array $change_set, string $event_type): void try { $entity = $this->resolveAuditableEntity($subject); - if (is_null($entity)) return; + if (is_null($entity)) { + return; + } $resource_server_ctx = App::make(\models\oauth2\IResourceServerContext::class); $user_id = $resource_server_ctx->getCurrentUserId(); @@ -129,12 +143,6 @@ private function buildAuditLogData($entity, $subject, array $change_set, string ]; switch ($event_type) { - case self::EVENT_ENTITY_CREATION: - $auditData['audit.created_data'] = $this->getEntityData($entity); - break; - case self::EVENT_ENTITY_DELETION: - $auditData['audit.deleted_data'] = $this->getEntityData($subject); - break; case self::EVENT_COLLECTION_UPDATE: if ($subject instanceof PersistentCollection) { $auditData['audit.collection_type'] = $this->getCollectionType($subject); @@ -147,41 +155,6 @@ private function buildAuditLogData($entity, $subject, array $change_set, string return $auditData; } - private function getEntityData($entity): array - { - $data = ['class' => get_class($entity)]; - - if (method_exists($entity, 'getId')) { - $id = $entity->getId(); - if ($id !== null) { - $data['id'] = $id; - } - } - - if (method_exists($entity, 'getTitle')) { - $title = $entity->getTitle(); - if ($title !== null) { - $data['title'] = $title; - } - } - - if (method_exists($entity, 'getName')) { - $name = $entity->getName(); - if ($name !== null) { - $data['name'] = $name; - } - } - - if (method_exists($entity, 'getSlug')) { - $slug = $entity->getSlug(); - if ($slug !== null) { - $data['slug'] = $slug; - } - } - - return $data; - } - private function getCollectionType(PersistentCollection $collection): string { if (empty($collection) && empty($collection->getSnapshot())) { @@ -204,22 +177,22 @@ private function getCollectionChanges(PersistentCollection $collection, array $c private function mapEventTypeToAction(string $event_type): string { return match($event_type) { - self::EVENT_ENTITY_CREATION => 'create', - self::EVENT_ENTITY_UPDATE => 'update', - self::EVENT_ENTITY_DELETION => 'delete', - self::EVENT_COLLECTION_UPDATE => 'collection_update', - default => 'unknown' + self::EVENT_ENTITY_CREATION => self::ACTION_CREATE, + self::EVENT_ENTITY_UPDATE => self::ACTION_UPDATE, + self::EVENT_ENTITY_DELETION => self::ACTION_DELETE, + self::EVENT_COLLECTION_UPDATE => self::ACTION_COLLECTION_UPDATE, + default => self::ACTION_UNKNOWN }; } private function getLogMessage(string $event_type): string { return match($event_type) { - self::EVENT_ENTITY_CREATION => 'audit.entity.created', - self::EVENT_ENTITY_UPDATE => 'audit.entity.updated', - self::EVENT_ENTITY_DELETION => 'audit.entity.deleted', - self::EVENT_COLLECTION_UPDATE => 'audit.collection.updated', - default => 'audit.entity.changed' + self::EVENT_ENTITY_CREATION => self::LOG_MESSAGE_CREATED, + self::EVENT_ENTITY_UPDATE => self::LOG_MESSAGE_UPDATED, + self::EVENT_ENTITY_DELETION => self::LOG_MESSAGE_DELETED, + self::EVENT_COLLECTION_UPDATE => self::LOG_MESSAGE_COLLECTION_UPDATED, + default => self::LOG_MESSAGE_CHANGED }; }