diff --git a/_register.php b/_register.php index 27ae7f6..82c9455 100644 --- a/_register.php +++ b/_register.php @@ -3,6 +3,7 @@ declare(strict_types=1); use OpenTelemetry\Contrib\Instrumentation\Drupal\DrupalInstrumentation; +use OpenTelemetry\Contrib\Instrumentation\Drupal\EntityInstrumentation; use OpenTelemetry\Contrib\Instrumentation\Drupal\ViewsInstrumentation; use Skpr\SkprConfig; @@ -20,3 +21,7 @@ if ($skpr->get('otel.drupal_views.enabled')) { ViewsInstrumentation::register(); } + +if ($skpr->get('otel.drupal_entity.enabled')) { + EntityInstrumentation::register(); +} diff --git a/docker-compose.yml b/docker-compose.yml index ca7b2e1..0439dcf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,7 @@ services: - ./:/data - ./docker/compose/volume/etc/skpr/data:/etc/skpr/data environment: + PHP_IDE_CONFIG: serverName=localhost <<: *otel-common ################## diff --git a/docker/compose/volume/etc/skpr/data/config.json b/docker/compose/volume/etc/skpr/data/config.json index 5e179a9..f55d121 100644 --- a/docker/compose/volume/etc/skpr/data/config.json +++ b/docker/compose/volume/etc/skpr/data/config.json @@ -1,4 +1,5 @@ { "otel.drupal.enabled": "true", - "otel.drupal_views.enabled": "true" -} \ No newline at end of file + "otel.drupal_views.enabled": "true", + "otel.drupal_entity.enabled": "true" +} diff --git a/src/EntityInstrumentation.php b/src/EntityInstrumentation.php new file mode 100644 index 0000000..43c4a77 --- /dev/null +++ b/src/EntityInstrumentation.php @@ -0,0 +1,151 @@ +getEntityTypeId(), $entity->isNew() ? 'new' : $entity->id()); + + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = $instrumentation + ->tracer() + ->spanBuilder($span) + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute('entity.type', $entity->getEntityTypeId()) + ->setAttribute('entity.is_new', $entity->isNew()) + ->setAttribute('entity.id', $entity->id()) + ->setAttribute('entity.label', $entity->label()) + ->setAttribute('entity.bundle', $entity->bundle()) + ->setAttribute(TraceAttributes::CODE_FUNCTION, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINENO, $lineno); + + $parent = Context::getCurrent(); + $span = $builder + ->setParent($parent) + ->startSpan(); + + $context = $span->storeInContext($parent); + Context::storage()->attach($context); + + return $params; + }, + post: static function ( + SqlContentEntityStorage $storage, + array $params, + ?int $return, + ?\Throwable $exception + ): void { + $scope = Context::storage()->scope(); + if (null === $scope) { + return; + } + + $scope->detach(); + + $span = Span::fromContext($scope->context()); + + $span->end(); + } + ); + + hook( + SqlContentEntityStorage::class, + 'delete', + pre: static function ( + EntityStorageInterface $storage, + array $params, + string $class, + string $function, + ?string $filename, + ?int $lineno, + ) use ($instrumentation): array { + /** @var \Drupal\Core\Entity\EntityInterface[] $entities */ + $entities = $params[0]; + if (count($entities) === 0) { + return $params; + } + + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = $instrumentation + ->tracer() + ->spanBuilder('Entity delete') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute(TraceAttributes::CODE_FUNCTION, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINENO, $lineno); + + $entitiesGrouped = \array_reduce($entities, function (array $carry, EntityInterface $entity) { + $carry[$entity->getEntityTypeId()][] = $entity->id(); + return $carry; + }, []); + $entitiesTag = []; + foreach ($entitiesGrouped as $entityTypeId => $entityIds) { + $entitiesTag[] = \sprintf('%s: %s', $entityTypeId, \implode(', ', $entityIds)); + } + $builder->setAttribute('entities.deleted', \implode('; ', $entitiesTag)); + + $parent = Context::getCurrent(); + $span = $builder + ->setParent($parent) + ->startSpan(); + + $context = $span->storeInContext($parent); + Context::storage()->attach($context); + + return $params; + }, + post: static function ( + SqlContentEntityStorage $storage, + array $params, + ?\Throwable $exception + ): void { + $scope = Context::storage()->scope(); + if (null === $scope) { + return; + } + + $scope->detach(); + + $span = Span::fromContext($scope->context()); + + $span->end(); + } + ); + } + +}