function PerformanceTestTrait::openTelemetryTracing

Same name and namespace in other branches
  1. 11.x core/tests/Drupal/Tests/PerformanceTestTrait.php \Drupal\Tests\PerformanceTestTrait::openTelemetryTracing()

Sends metrics to OpenTelemetry.

Parameters

array $messages: The ChromeDriver performance log messages.

string $service_name: A human readable identifier so that traces can be grouped together.

See also

https://opentelemetry.io/docs/instrumentation/php/manual/

1 call to PerformanceTestTrait::openTelemetryTracing()
PerformanceTestTrait::processChromeDriverPerformanceLogs in core/tests/Drupal/Tests/PerformanceTestTrait.php
Gets the chromedriver performance log and extracts metrics from it.

File

core/tests/Drupal/Tests/PerformanceTestTrait.php, line 422

Class

PerformanceTestTrait
Provides various methods to aid in collecting performance data during tests.

Namespace

Drupal\Tests

Code

private function openTelemetryTracing(array $messages, string $service_name) : void {
  // Open telemetry timestamps are always in nanoseconds.
  // @todo Consider moving these to trait constants once we require PHP 8.2.
  $nanoseconds_per_second = 1000000000;
  $nanoseconds_per_millisecond = 1000000;
  $nanoseconds_per_microsecond = 1000;
  $collector = getenv('OTEL_COLLECTOR');
  if (!$collector) {
    return;
  }
  $first_request_timestamp = NULL;
  $first_response_timestamp = NULL;
  $request_wall_time = NULL;
  $response_wall_time = NULL;
  $url = NULL;
  foreach ($messages as $message) {
    // Since chrome timestamps are since OS start, we take the first network
    // request and response, determine the wall times of each, then calculate
    // offsets from those for everything else.
    if ($message['method'] === 'Tracing.dataCollected' && isset($message['params']['name']) && $message['params']['name'] === 'ResourceReceiveResponse') {
      $first_response_timestamp = (int) ($message['params']['ts'] * $nanoseconds_per_microsecond);
      // Get the actual timestamp of the response which is a millisecond unix
      // epoch timestamp. The log doesn't provide this for the request.
      $response_wall_time = (int) ($message['params']['args']['data']['responseTime'] * $nanoseconds_per_millisecond);
      // 'requestTime' is in the format 'seconds since OS boot with
      // microsecond precision'.
      $first_request_timestamp = (int) ($message['params']['args']['data']['timing']['requestTime'] * $nanoseconds_per_second);
      // By subtracting the request timestamp from the response wall time we
      // get the request wall time.
      $request_wall_time = $response_wall_time - ($first_response_timestamp - $first_request_timestamp);
      break;

    }
  }
  if ($first_response_timestamp === NULL) {
    // If the $first_response_timestamp is null, this means we got an
    // incomplete log from chromedriver, mark the test as skipped.
    $this->markTestSkipped('Incomplete log from chromedriver, giving up.');
  }
  // @todo Get commit hash from an environment variable and add this as an
  //   additional attribute.
  //   @see https://www.drupal.org/project/drupal/issues/3379761
  $resource = ResourceInfoFactory::defaultResource();
  $resource = $resource->merge(ResourceInfo::create(Attributes::create([
    ResourceAttributes::SERVICE_NAMESPACE => 'Drupal',
    ResourceAttributes::SERVICE_NAME => $service_name,
    ResourceAttributes::SERVICE_INSTANCE_ID => 1,
    ResourceAttributes::SERVICE_VERSION => \Drupal::VERSION,
    ResourceAttributes::DEPLOYMENT_ENVIRONMENT => 'local',
  ])));
  $otel_collector_headers = getenv('OTEL_COLLECTOR_HEADERS') ?: [];
  if ($otel_collector_headers) {
    $otel_collector_headers = json_decode($otel_collector_headers, TRUE);
  }
  $transport = (new OtlpHttpTransportFactory())->create($collector, 'application/x-protobuf', $otel_collector_headers);
  $exporter = new SpanExporter($transport);
  $tracerProvider = new TracerProvider(new SimpleSpanProcessor($exporter), NULL, $resource);
  $tracer = $tracerProvider->getTracer('Drupal');
  $span = $tracer->spanBuilder('main')
    ->setStartTimestamp($request_wall_time)
    ->setAttribute('http.method', 'GET')
    ->setAttribute('http.url', $url)
    ->setSpanKind(SpanKind::KIND_SERVER)
    ->startSpan();
  $last_timestamp = $response_wall_time;
  try {
    $scope = $span->activate();
    $first_byte_span = $tracer->spanBuilder('firstByte')
      ->setStartTimestamp($request_wall_time)
      ->setAttribute('http.url', $url)
      ->startSpan();
    $first_byte_span->end($response_wall_time);
    $collection = \Drupal::keyValue('performance_test');
    $performance_test_data = $collection->get('performance_test_data');
    $query_events = $performance_test_data['database_events'] ?? [];
    foreach ($query_events as $key => $event) {
      if (static::isDatabaseCache($event)) {
        continue;
      }
      // Use the first part of the database query for the span name.
      $query_span = $tracer->spanBuilder(substr($event->queryString, 0, 64))
        ->setStartTimestamp((int) ($event->startTime * $nanoseconds_per_second))
        ->setAttribute('query.string', $event->queryString)
        ->setAttribute('query.args', var_export($event->args, TRUE))
        ->setAttribute('query.caller', var_export($event->caller, TRUE))
        ->startSpan();
      $query_span->end((int) ($event->time * $nanoseconds_per_second));
    }
    $cache_operations = $performance_test_data['cache_operations'] ?? [];
    foreach ($cache_operations as $operation) {
      $cache_span = $tracer->spanBuilder('cache ' . $operation['operation'] . ' ' . $operation['bin'])
        ->setStartTimestamp((int) ($operation['start'] * $nanoseconds_per_second))
        ->setAttribute('cache.operation', $operation['operation'])
        ->setAttribute('cache.cids', $operation['cids'])
        ->setAttribute('cache.bin', $operation['bin'])
        ->startSpan();
      $cache_span->end((int) ($operation['stop'] * $nanoseconds_per_second));
    }
    $cache_tag_operations = $performance_test_data['cache_tag_operations'] ?? [];
    foreach ($cache_tag_operations as $operation) {
      $cache_tag_span = $tracer->spanBuilder('cache_tag ' . $operation['operation']->name . ' ' . $operation['tags'])
        ->setStartTimestamp((int) ($operation['start'] * $nanoseconds_per_second))
        ->setAttribute('cache_tag.operation', $operation['operation']->name)
        ->setAttribute('cache_tag.tags', $operation['tags'])
        ->startSpan();
      $cache_tag_span->end((int) ($operation['stop'] * $nanoseconds_per_second));
    }
    $lcp_timestamp = NULL;
    $fcp_timestamp = NULL;
    $lcp_size = 0;
    foreach ($messages as $message) {
      if ($message['method'] === 'Tracing.dataCollected' && $message['params']['name'] === 'firstContentfulPaint') {
        if (!isset($fcp_timestamp)) {
          // Tracing timestamps are microseconds since OS boot.
          $fcp_timestamp = $message['params']['ts'] * $nanoseconds_per_microsecond;
          $fcp_span = $tracer->spanBuilder('firstContentfulPaint')
            ->setStartTimestamp($request_wall_time)
            ->setAttribute('http.url', $url)
            ->startSpan();
          $last_timestamp = $first_contentful_paint_wall_time = (int) ($request_wall_time + ($fcp_timestamp - $first_request_timestamp));
          $fcp_span->end($first_contentful_paint_wall_time);
        }
      }
      // There can be multiple largestContentfulPaint candidates, remember
      // the largest one.
      if ($message['method'] === 'Tracing.dataCollected' && $message['params']['name'] === 'largestContentfulPaint::Candidate' && $message['params']['args']['data']['size'] > $lcp_size) {
        $lcp_timestamp = $message['params']['ts'] * $nanoseconds_per_microsecond;
        $lcp_size = $message['params']['args']['data']['size'];
      }
    }
    if (isset($lcp_timestamp)) {
      $lcp_span = $tracer->spanBuilder('largestContentfulPaint')
        ->setStartTimestamp($request_wall_time)
        ->setAttribute('http.url', $url)
        ->startSpan();
      $last_timestamp = $largest_contentful_paint_wall_time = (int) ($request_wall_time + ($lcp_timestamp - $first_request_timestamp));
      $lcp_span->setAttribute('lcp.size', $lcp_size);
      $lcp_span->end($largest_contentful_paint_wall_time);
    }
  } finally {
    // The scope must be detached before the span is ended, because it's
    // created from the span.
    if (isset($scope)) {
      $scope->detach();
    }
    $span->end($last_timestamp);
    $tracerProvider->shutdown();
  }
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.