Skip to content

Execution timeline

The execution status page and outbound HTTP tracing middleware are provided by the native_observability_execution submodule. It is part of the DASHBOARD install tier; enable it with drush no:preset:dashboard, or directly with drush en native_observability_execution -y.

Routes

Route Path Permission
native_observability_execution.status /admin/reports/native-observability/execution access native observability execution

Execution status page

Open /admin/reports/native-observability/execution. ExecutionStatusController::status() renders an ExecutionFiltersForm (filterable by path, method, category, status, sort and order) above a timeline table built by ExecutionTimelineBuilder. The page disables caching (#cache => ['max-age' => 0]) so it always reflects the latest captured spans.

The timeline table shows, per span: method, path, status, duration (ms), and category.

How spans reach the timeline

ExecutionRequestSubscriber measures the whole request lifecycle and stores a rolling history of spans so the status page can show results across requests, not just the one currently open.

sequenceDiagram
    participant Req as Request
    participant Sub as ExecutionRequestSubscriber
    participant Stack as ExecutionSpanStack
    participant Store as PrivateTempStore (per user)

    Req->>Sub: kernel.request
    Sub->>Sub: record start time (microtime)
    Req->>Sub: kernel.response
    Sub->>Sub: compute duration, build root http.request span
    Sub->>Stack: collect all spans recorded during the request
    Sub->>Store: append batch, trim to last 1000, save
    Sub->>Stack: reset()

Behaviour to be aware of:

  • Excluded requests (matched by a request log exclusion rule) are skipped entirely: the subscriber resets its state and never times or stores them.
  • The root http.request span (method, path, status, duration, timestamp) is appended last, so it is always present even if no other spans were recorded during the request.
  • History is kept per user in PrivateTempStore, under the native_observability_execution collection and the recent_request_spans key.
  • The history is a rolling window capped at 1000 spans. On every response, the subscriber reads the stored history, appends the new batch, and trims it back to the last 1000 entries with array_slice($history, -1000) before saving, so the payload and lock-hold time stay bounded for active users.
  • PrivateTempStore::set() takes a lock and throws TempStoreException when it cannot acquire one, which happens under concurrent in-flight requests from the same user. The subscriber catches and discards this exception: recording a trace must never turn into a 500 response, so the only consequence of lock contention is losing that one batch of spans from the Execution UI.

ExecutionTimelineBuilder reads spans from the in-memory ExecutionSpanStack for the current request first, and falls back to the persisted PrivateTempStore history when the current request recorded nothing, so navigating straight to the status page still shows recent activity.

Outbound HTTP tracing middleware

GuzzleTraceMiddleware is registered as a Guzzle middleware (tagged http_client_middleware), not as a decorator of the http_client service, so http_client remains a real GuzzleHttp\Client for consumers that type-hint the concrete class. It wraps every outbound request issued through Drupal's HTTP client and records, per call:

  • method
  • host
  • the full request URL (scheme, host, optional non-standard port, path, query string)
  • status code on success, or an error_type (the exception class) on failure
  • duration in milliseconds

Spans are recorded under the external.http root category. Callers can pass a native_observability request option to customize the span:

$http_client->request('GET', $url, [
  'native_observability' => [
    'subtype' => 'drupal_org',
    'name' => 'Drupal.org API',
  ],
]);
// -> span category "external.http.drupal_org"

The subtype is normalized to lowercase [a-z0-9_.] and appended to the root category; an invalid or empty subtype falls back to the bare external.http category. This is a separate middleware from the one registered by native_observability_spans: enabling both modules produces two independent spans per outbound call, one per category root (http.client and external.http).