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.requestspan (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 thenative_observability_executioncollection and therecent_request_spanskey. - 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 throwsTempStoreExceptionwhen 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).