Data map¶
This page lists every field Native Observability writes to its database tables: where the value comes from, whether it can be personal data, and how an MCP tool may expose it. It exists to decide what MCP tools may return. Every claim cites a file:line in the module code; where the code does not settle a question the entry says unverified and why.
Paths are relative to the module root. Line numbers are approximate: they drift as the code changes.
Exposure grades¶
Decided on 31/10/2026.
| Grade | What an MCP tool may return |
|---|---|
| 0 | Nothing. |
| 1 | Aggregates grouped by route name (for example entity.node.canonical), never by path, and only for groups that reach a minimum request count. |
| 2 | Row-level detail filtered through PrivacyPolicy, never the IP address. It needs two confirmations: a site-level declaration and the observability:raw scope on the client. A token gets the scope only if the client asks for it at sign-in once the declaration is recorded: tokens issued before keep their scopes, and a refresh never adds it. |
In the tables below, Origin is internal when the module or Drupal generates the value, and external when the visitor or a third party writes or influences it (path, query string, user agent, referer, headers, route parameters, SQL values, remote URLs). The Grade 2 column uses these treatments: expose, truncate + mark untrusted, redact, omit.
What the outward exports read from these tables¶
The Prometheus scrape and the OpenTelemetry exporter read none of the external fields above except as follows. Neither one sends path, payload.path, payload.query, headers, referers, payload.exception.message or the outbound span uri, url, target and error_message.
| Consumer | Fields used |
|---|---|
| Prometheus | Trace: route_name, method, status_code, duration_ms, is_ajax. Spans: category, duration_ms and the host part of payload.host. Cache events: status, cache_layer. |
| OpenTelemetry | Request: method, route_name, status_code, duration_ms, request id, exception class. uid when capture.store_uid is on. No outbound HTTP span and no SQL statement is exported. |
The six tables below are the ones in scope. Two other places hold request data and are listed under Other stores.
native_observability_trace¶
One row per main request. Schema: native_observability.install:56-150. Writer: src/EventSubscriber/TraceSubscriber.php:324-337 through src/Service/DatabaseTraceStorage.php:84-116.
| Field | Type | Origin | Personal data | Grade 1 treatment | Grade 2 treatment | Source |
|---|---|---|---|---|---|---|
id |
serial | internal | no | omit | expose | native_observability.install:59 |
request_id |
varchar(64) | internal (generated ULID) | no | omit | expose | native_observability.install:65; generated at src/EventSubscriber/TraceSubscriber.php:123 |
method |
varchar(16) | external (request line; the HTTP client picks it) | no | allowed as a group key only if the route group stays above the threshold; otherwise omit | expose | native_observability.install:71; src/EventSubscriber/TraceSubscriber.php:326 |
path |
varchar(255) | external (the visitor chooses it, including for unmatched routes) | possible: segments can carry user ids, slugs, search terms, email addresses typed in URLs. Stored redacted: route parameters named like secrets, the core one-time login shapes and token-like segments become [REDACTED] |
omit | never returned: route is the location |
native_observability.install:77; src/EventSubscriber/TraceSubscriber.php:327 |
route_name |
varchar(255), nullable | internal (defined by code) when matched; NULL when no route matched | no | expose as the group key | expose | native_observability.install:83; src/EventSubscriber/TraceSubscriber.php:328 |
status_code |
smallint | internal | no | aggregate (counts per class) | expose | native_observability.install:89; src/EventSubscriber/TraceSubscriber.php:329 |
is_ajax |
tinyint | external (derived from an X-Requested-With header, a _wrapper_format query value or a _drupal_ajax body value) |
no | omit | expose | native_observability.install:96; src/EventSubscriber/TraceSubscriber.php:354-358 |
duration_ms |
int | internal | no | aggregate (avg, percentiles) | expose | native_observability.install:104; src/EventSubscriber/TraceSubscriber.php:252 |
started_at |
int (s) | internal | no | bucket to the window | expose | native_observability.install:110; src/EventSubscriber/TraceSubscriber.php:332 |
ended_at |
int (s), nullable | internal; always written as NULL by the trace subscriber | no | omit | expose | native_observability.install:116; src/EventSubscriber/TraceSubscriber.php:333 |
uid |
int, nullable | internal | yes: a stable user identifier, in an indirect form. Written unless capture.store_uid is off (default on) |
omit | redact (omit, or replace with anonymous/authenticated) |
native_observability.install:122; src/EventSubscriber/TraceSubscriber.php:287-291; default config/install/native_observability.settings.yml:17 |
client_ip |
varchar(64), nullable | external (network peer, or X-Forwarded-For when Drupal trusts a proxy; unverified: the trusted-proxy setup is site configuration, outside this module) |
yes: IP address. Default mode anonymized (last IPv4 octet zeroed, IPv6 truncated to /64). hashed is a salted SHA-256 of the full IP; full stores the raw IP |
omit | omit (grade 2 never returns the IP, whatever the mode) | native_observability.install:128; src/EventSubscriber/TraceSubscriber.php:476-491; default config/install/native_observability.settings.yml:20 |
payload |
blob (JSON) | mixed, see Trace payload keys | possible | omit | expose per key, following the key table | native_observability.install:134; src/EventSubscriber/TraceSubscriber.php:257-336 |
Retention: age-based deletion at trace_retention_hours (default 72) plus a row cap retention.max_rows (default 50000), both run from cron in batches of cleanup_batch_limit. See src/Service/TraceCleanupHandler.php:56-91,104-133, src/Service/ObservabilityCleanupManager.php:40-48,164-175, native_observability.module:16-17, defaults config/install/native_observability.settings.yml:94-103. Cleanup is skipped when cleanup_enabled is off (src/Service/ObservabilityCleanupManager.php:43). Manual full purge: src/Service/TraceCleanupHandler.php:138-155.
Trace payload keys¶
The JSON in payload is built at src/EventSubscriber/TraceSubscriber.php:257-316. Keys stored:
| Key | Origin | Personal data | Grade 2 treatment | Source |
|---|---|---|---|---|
request_id |
internal | no | expose | :258 |
parent_request_id |
external: read from the request header X-Native-Observability-Parent-Request-Id and stored only when it is a ULID or a UUID of at most 64 characters |
no, but the client chose the value | truncate + mark untrusted | :131-134, :259 |
method |
external | no | expose | :260 |
path |
external | possible (see above) | never returned | :261 |
query_keys |
external | no: names only, sorted, at most 20; odd or token-like names become [KEY] |
return as is | PrivacyPolicy::queryKeyNames() |
is_ajax |
external-derived | no | expose | :262 |
route_name |
internal | no | expose | :265-267 (switch capture.route_name) |
status_code |
internal | no | expose | :269-271 |
duration_ms |
internal | no | expose | :273-275 |
database |
internal numbers (request summary of the database observer) | no | expose | :277-280; keys built at modules/native_observability_database_observer/src/Service/DatabaseQueryRuntimeCollector.php:71-74, plus stored_slow_queries, slow_threshold_ms at modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:204-207 |
exception (class, message, code) |
class internal; message external or mixed: the text of any thrown exception, which can embed request values, SQL, file paths or user input. Stored with URLs and referers redacted, database arguments and quoted literals removed, cut to capture.exception_message_max_length (default 1024) |
possible | class: expose; message: never returned |
:165-169, :282-285 |
user_agent |
external (request header). Default mode family stores only {browser, os} from a fixed vocabulary; mode full stores the raw header |
family: no. full: possible (device model, build identifiers) |
family: expose. full: truncate + mark untrusted |
:298-301, :502-516, :781; default config/install/native_observability.settings.yml:24 |
headers |
external. Off by default. allowlist stores the listed headers, full stores all headers except a hard deny-list |
possible: Referer, X-Forwarded-For, Accept-Language, custom headers |
omit unless the key is on a module-side allowlist; truncate + mark untrusted | :303-306, :527-562; deny-list :58-70; default config/install/native_observability.settings.yml:55 |
query |
external. Off by default. keys_only stores parameter names, allowlist and full store values (sensitive names replaced by [REDACTED], other values cut at 2048 bytes) |
possible: search terms, email addresses, tracking ids | omit; at most keys, truncated, marked untrusted | :308-311, :573-610, :701-707, :720-730; default config/install/native_observability.settings.yml:60 |
body |
external. Off by default (config calls it dangerous). Same modes and redaction as query, applied to parsed form fields |
possible: form submissions | omit | :313-316, :621-662; default config/install/native_observability.settings.yml:65 |
native_observability_spans¶
Timeline entries linked to a request. Schema: native_observability.install:152-236 (the table belongs to the base module although the writers live in native_observability_spans; see modules/native_observability_spans/native_observability_spans.install:8). Writers: modules/native_observability_spans/src/Service/SpanRecorder.php:100-117 (start) and :160-170 (end, which overwrites the payload), modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:137-148, modules/native_observability_spans/src/EventSubscriber/DatabaseSummarySpanSubscriber.php:132-143.
| Field | Type | Origin | Personal data | Grade 1 treatment | Grade 2 treatment | Source |
|---|---|---|---|---|---|---|
id |
serial | internal | no | omit | expose | native_observability.install:155 |
trace_id |
int | internal; written as 0 by all three writers | no | omit | expose | native_observability.install:161; SpanRecorder.php:102, RequestSummarySpanSubscriber.php:138, DatabaseSummarySpanSubscriber.php:133 |
span_id |
varchar(64), unique | internal (generated) | no | omit | expose | native_observability.install:167 |
request_id |
varchar(64) | internal | no | omit | expose | native_observability.install:173 |
parent_span_id |
varchar(64), nullable | internal | no | omit | expose | native_observability.install:179 |
name |
varchar(255) | mixed. Internal for http.request_summary and database.request_summary. For outbound HTTP spans: METHOD host, where the host comes from a URL that site code or a visitor-supplied value can influence (external), or a caller-supplied native_observability.name option. For custom spans: whatever the calling code passes |
possible: a host can embed an identifier (for example a tenant subdomain). Not verified for any real case | omit | truncate + mark untrusted | native_observability.install:185; modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:189-201 |
category |
varchar(64) | internal. Subtype is restricted to [a-z0-9_.] |
no | allowed as a group key | expose | native_observability.install:191; GuzzleSpanMiddleware.php:146-154 |
started_at_ms |
bigint | internal | no | bucket | expose | native_observability.install:197 |
ended_at_ms |
bigint, nullable | internal | no | omit | expose | native_observability.install:204 |
duration_ms |
int | internal | no | aggregate | expose | native_observability.install:211 |
payload |
blob (JSON) | mixed, see below | possible | omit | expose per key, following the key table | native_observability.install:217 |
Retention: spans_retention_hours (default 24) and retention.max_rows (default 50000), with started_at_ms converted to milliseconds. src/Service/SpansCleanupHandler.php:56-95,108-137,145-162; defaults config/install/native_observability.settings.yml:104.
Span payload keys¶
Three families, by writer.
Outbound HTTP span (GuzzleSpanMiddleware), keys at modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:75-83 and :120-127:
| Key | Origin | Personal data | Grade 2 treatment |
|---|---|---|---|
type, method, host |
method internal, host external |
no | expose / truncate + mark untrusted |
uri, url, target (same value three times) |
external: the full outbound URL, passed through PrivacyPolicy::redactUri() (userinfo and values of sensitive query parameters replaced). Path segments and non-sensitive query values are kept |
possible: ids and search terms inside the URL | truncate + mark untrusted |
options |
internal: an allow-list of Guzzle request options by name and scalar type (timeouts and a few boolean flags). Everything else is dropped, path and array values are kept as booleans only | no | omit |
status_code |
external (remote response) | no | expose |
outcome |
internal | no | expose |
error_message |
external: the exception message of the HTTP client. The response excerpt that Guzzle appends is cut, the original URI is replaced by its redacted form, and the URLs left in the text are redacted | possible | truncate + mark untrusted |
Request summary span, modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:128-134,170-177: synthetic, summary_type, request.{method, path, route_name, status_code, is_ajax} (path through redactSensitivePath(), external), and any fragment returned by an enrichment provider. The one provider in the family adds cache.{page_cache_status, dynamic_page_cache_status, cacheability_status, tags_count, contexts_count, max_age} (modules/native_observability_cache_observer/src/Service/CacheSpanEnrichmentProvider.php:52-81), all internal. Other modules can register providers (modules/native_observability_spans/src/Service/RequestSummaryEnrichmentProviderInterface.php); their keys are unverified for third-party code.
Database summary span, modules/native_observability_spans/src/EventSubscriber/DatabaseSummarySpanSubscriber.php:125-129: synthetic, summary_type, and database (request summary, internal numbers: context_type, query_count, total_query_time_ms, slow_query_count, ...; DatabaseQueryRuntimeCollector.php:71-74).
Custom spans: any caller of SpanRecorderInterface::startSpan() can pass an arbitrary array that is stored as JSON (modules/native_observability_spans/src/Service/SpanRecorder.php:58,111). The module cannot say what other code puts there: unverified by construction.
native_observability_request_exclusion_rule¶
Admin-authored rules that keep requests out of capture. Schema: native_observability.install:238-304. Writer: src/Service/RequestLogExclusionRepository.php:129-170, fed by src/Form/RequestLogExclusionRuleForm.php.
| Field | Type | Origin | Personal data | Grade 1 treatment | Grade 2 treatment | Source |
|---|---|---|---|---|---|---|
id |
serial | internal | no | omit | expose | native_observability.install:241 |
label |
varchar(255) | internal (admin text; can be prefilled from a query-string value on the add form) | possible: free text by an administrator | omit | truncate + mark untrusted | native_observability.install:246; src/Form/RequestLogExclusionRuleForm.php:65,86-90 |
enabled |
tinyint | internal | no | omit | expose | native_observability.install:251 |
method |
varchar(16) | internal | no | omit | expose | native_observability.install:258 |
path_match_type |
varchar(16) | internal (exact, prefix, regex) |
no | omit | expose | native_observability.install:264 |
path_pattern |
varchar(255) | internal, but typically copied from a real visitor path | possible | omit | truncate + mark untrusted | native_observability.install:270 |
query_conditions |
blob (JSON) | internal, typically copied from real query strings (key|equals|value) |
possible | omit | truncate + mark untrusted | native_observability.install:275; src/Form/RequestLogExclusionRuleForm.php:64,127-131; format src/Service/RequestLogExclusionRuleNormalizer.php:62 |
notes |
text | internal (admin free text) | possible | omit | truncate + mark untrusted | native_observability.install:280 |
created_at, updated_at |
int | internal | no | omit | expose | native_observability.install:285,290 |
Retention: none found. No cleanup handler covers this table (the handlers registered at native_observability.services.yml:112-125 and by the submodules cover trace, spans, metrics, cache events and database queries). Rules are configuration-like data and are removed only by RequestLogExclusionRepository::delete() (:173) and on module uninstall (native_observability.install:316).
native_observability_route_metric¶
Aggregates per route and window, computed from the trace table. Schema: modules/native_observability_metrics/native_observability_metrics.install:44-130. Writer: modules/native_observability_metrics/src/Service/DatabaseMetricsStorage.php:44-71; grouping at modules/native_observability_metrics/src/Service/RouteMetricsAggregator.php:50-90.
| Field | Type | Origin | Personal data | Grade 1 treatment | Grade 2 treatment | Source |
|---|---|---|---|---|---|---|
id |
serial | internal | no | omit | expose | native_observability_metrics.install:48 |
route_key |
varchar(255) | internal: the route name when there is one, otherwise the fixed key unmatched |
no: the visitor path is never used as a key | aggregate by this key | aggregate by this key | native_observability_metrics.install:53; RouteMetricsAggregator.php |
route_name |
varchar(255), nullable | internal; holds the key unmatched when no route matched |
no | expose as the group key | expose | native_observability_metrics.install:59; DatabaseMetricsStorage.php:56 |
window_start, window_end |
int | internal | no | expose | expose | native_observability_metrics.install:65,71 |
request_count |
int | internal | no | expose only when at or above the threshold | expose | native_observability_metrics.install:77 |
throughput_per_minute |
decimal | internal | no | expose | expose | native_observability_metrics.install:83 |
avg_duration_ms, p95_duration_ms, p99_duration_ms |
int | internal | no | expose | expose | native_observability_metrics.install:90,95,101 |
created_at |
int | internal | no | expose | expose | native_observability_metrics.install:108 |
The aggregator skips system.css_asset and system.js_asset (RouteMetricsAggregator.php:60). A row with request_count of 1 describes a single request, so a minimum count is what keeps a grade 1 answer from being row-level data. The reader used by the MCP tools (RouteMetricsReader) withholds groups under minimum_request_count. The dashboard and the Prometheus reader do not apply a minimum (unverified for the dashboard, not audited here).
Retention: metrics_retention_days (default 7); the row cap is deliberately not applied. src/Service/MetricsCleanupHandler.php:59-94 (comment at :72-75); default config/install/native_observability.settings.yml:105. The window is replaced on recompute (DatabaseMetricsStorage.php:44-49).
native_observability_cache_event¶
Cache observations and tag invalidations. Schema: modules/native_observability_cache_observer/native_observability_cache_observer.install:13-110. Writers: modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:70-110,143-155, modules/native_observability_cache_observer/src/StackMiddleware/CachePageObserverMiddleware.php:104-117, modules/native_observability_cache_observer/src/Service/ObservedCacheTagsInvalidator.php:132-153.
| Field | Type | Origin | Personal data | Grade 1 treatment | Grade 2 treatment | Source |
|---|---|---|---|---|---|---|
id |
serial | internal | no | omit | expose | native_observability_cache_observer.install:17 |
request_id |
varchar(64), nullable | internal | no | omit | expose | :23 |
event_type |
varchar(64) | internal | no | allowed as a group key | expose | :29 |
cache_layer |
varchar(64) | internal | no | allowed as a group key | expose | :35 |
status |
varchar(64) | internal in practice (HIT, MISS, UNCACHEABLE, INVALIDATED). For the header-based events it is the upper-cased value of a response header that Drupal or another module sets |
no | aggregate | expose | :41; CacheResponseObserverSubscriber.php:134-143, CachePageObserverMiddleware.php:97-104 |
method |
varchar(16) | external | no | omit | expose | :47 |
path |
varchar(255) | external | possible (same as the trace path) | omit | truncate + mark untrusted | :53; CacheResponseObserverSubscriber.php:75, CachePageObserverMiddleware.php:110, ObservedCacheTagsInvalidator.php:138 |
route_name |
varchar(255) | internal | no | expose as the group key | expose | :59 |
tag_count, context_count, max_age |
int | internal | no | aggregate | expose | :65,72,79 |
created_at |
int | internal | no | bucket | expose | :84 |
payload |
blob (JSON) | internal values. Keys: tags, contexts, max_age (cacheability events); header_value (header events); tags (invalidation events) |
possible: cache tags are entity identifiers such as node:123, user:5, config:...; one request is unlikely to expose a person, but a tag names the entity |
omit | expose contexts and max_age; truncate the tag list |
:90; CacheResponseObserverSubscriber.php:101-105,150-152; ObservedCacheTagsInvalidator.php:150-152 |
The tag list is cut only by the per-request cap max_stored_invalidations_per_request (default 50) on invalidation rows (ObservedCacheTagsInvalidator.php:124-127), not on cacheability rows.
Retention: retention_hours (default 72) and the shared row cap. modules/native_observability_cache_observer/src/Service/CacheObserverCleanupHandler.php:61-85; default modules/native_observability_cache_observer/config/install/native_observability_cache_observer.settings.yml:6.
native_observability_database_query¶
Slow database queries. Schema: modules/native_observability_database_observer/native_observability_database_observer.install:13-128. Writer: modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:134-208 through modules/native_observability_database_observer/src/Service/DatabaseQueryInsertBuffer.php:25-45.
| Field | Type | Origin | Personal data | Grade 1 treatment | Grade 2 treatment | Source |
|---|---|---|---|---|---|---|
id |
serial | internal | no | omit | expose | native_observability_database_observer.install:17 |
request_id |
varchar(64), nullable | internal | no | omit | expose | :22 |
trace_id |
int, nullable | internal; always written as NULL | no | omit | expose | :28; DatabaseQueryObserverSubscriber.php:163 |
route_name |
varchar(255), nullable | internal | no | expose as the group key | expose | :34; :164 |
method |
varchar(16) | external | no | omit | expose | :40; :165 |
path |
varchar(255) | external | possible (same as the trace path) | omit | truncate + mark untrusted | :46; :141,166 |
relation_type |
varchar(16) | internal | no | allowed as a group key | expose | :52 |
connection_key, connection_target |
varchar | internal | no | omit | expose | :59,65 |
duration_ms |
decimal | internal | no | aggregate | expose | :71 |
is_slow |
tinyint | internal | no | omit | expose | :79 |
query_hash |
varchar(64) | internal: SHA-256 of the SQL text, unsalted | no. A hash of a statement that contains literals is guessable for short inputs; unverified whether real statements do | allowed as a group key | expose | :87; modules/native_observability_database_observer/src/Service/DatabaseQueryClassifier.php:38-40 |
query_text |
blob, nullable | mixed: the SQL string with placeholders, as Drupal logs it. Written unless store_slow_query_text is off (default off) |
possible. See SQL text | omit | truncate + mark untrusted, or omit | :93; DatabaseQueryObserverSubscriber.php:144,160,173; default modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:3 |
caller |
varchar(255), nullable | internal (Class::method from the call stack) |
no | omit | expose | :99; DatabaseQueryObserverSubscriber.php:177,235-252 |
created_at |
int | internal | no | bucket | expose | :105 |
payload |
blob (JSON) | internal. Keys: context_type, time_seconds, request_summary (the request summary numbers plus slow_threshold_ms) |
no | omit | expose | :111; DatabaseQueryObserverSubscriber.php:193-199 |
Only queries at or above slow_query_threshold_ms (default 100) are stored, at most max_stored_queries_per_request per request (default 20), keeping the slowest. DatabaseQueryObserverSubscriber.php:143-145,156-158,183; defaults modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:2-4.
Retention: retention_hours (default 72) and the shared row cap. modules/native_observability_database_observer/src/Service/DatabaseObserverCleanupHandler.php:58-86; default modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:5.
SQL text and query arguments¶
- Query arguments are not stored. The observer reads the
queryentry of each log record and ignoresargs(DatabaseQueryObserverSubscriber.php:150-179; thecallerarray also carriesargsandformatCaller()uses onlyclassandfunction,:244-251). Core's log record keeps the SQL string and the bound values apart (web/core/lib/Drupal/Core/Database/Log.php:114-120in the Drupal 11 tree used on the test bench). - The SQL text is not stored by default (
store_slow_query_text: false). When a site turns it on, the text goes throughSqlLiteralRedactorbefore the insert: string and numeric literals become?andINlists collapse toIN (?). The statement hash is taken from the redacted text. Identifiers built from input stay in the text. - The SQL text is not passed through
PrivacyPolicy. The redactor is the only step betweenDatabase::getLog()and the insert (DatabaseQueryObserverSubscriber.php). - The form calls the flag "Store SQL text for slow queries" and describes it only as a storage-size choice (
modules/native_observability_database_observer/src/Form/DatabaseObserverSettingsForm.php:85-90).
Other stores¶
These are not in scope as tables but hold request data.
- Private tempstore, collection
native_observability_execution: the execution timeline of authenticated users, up to 1000 spans, written at response time (modules/native_observability_execution/src/EventSubscriber/ExecutionRequestSubscriber.php:38-49,148-168). It holds the redacted path and, for outbound HTTP, the URL passed throughPrivacyPolicy::redactUri()(modules/native_observability_execution/src/Service/GuzzleTraceMiddleware.php). Where tempstore keeps it and for how long is core behavior, unverified here. - State:
native_observability.client_ip_hash_salt, or$settings['native_observability_client_ip_hash_salt']; never in config. - Config: the sensitive-key list (
config/install/native_observability.settings.yml:21,31-48).
Externally-controlled fields¶
Every field below is written or influenced by a visitor or an external party. Each one is a prompt-injection test case: an MCP tool that returns it hands attacker-chosen text to the model.
| # | Table | Field | Who controls it | Source |
|---|---|---|---|---|
| 1 | native_observability_trace |
path |
the visitor (any URL, including unmatched ones) | src/EventSubscriber/TraceSubscriber.php:327 |
| 2 | native_observability_trace |
payload.path |
the visitor | src/EventSubscriber/TraceSubscriber.php:261 |
| 3 | native_observability_trace |
payload.parent_request_id |
the visitor (request header, stored only when it is a ULID or a UUID) | src/EventSubscriber/TraceSubscriber.php:131-134,259 |
| 4 | native_observability_trace |
payload.exception.message |
whoever triggers the exception; may embed request values | src/EventSubscriber/TraceSubscriber.php:165-169 |
| 5 | native_observability_trace |
payload.user_agent (mode full; mode family is a fixed vocabulary) |
the visitor (request header) | src/EventSubscriber/TraceSubscriber.php:502-516 |
| 6 | native_observability_trace |
payload.headers.* (when headers_mode is not off) |
the visitor (request headers, including Referer) |
src/EventSubscriber/TraceSubscriber.php:527-562 |
| 7 | native_observability_trace |
payload.query.* (when query_mode is not off; keys in keys_only) |
the visitor | src/EventSubscriber/TraceSubscriber.php:573-610 |
| 8 | native_observability_trace |
payload.body.* (when body_mode is not off) |
the visitor | src/EventSubscriber/TraceSubscriber.php:621-662 |
| 9 | native_observability_trace |
client_ip (mode full; also a fixed-shape value in other modes) |
the network peer or a forwarding proxy | src/EventSubscriber/TraceSubscriber.php:476-491 |
| 10 | native_observability_spans |
name (outbound HTTP spans: METHOD host; custom spans: any string) |
the owner of the remote URL; the caller for custom names | modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:189-201 |
| 11 | native_observability_spans |
payload.uri, payload.url, payload.target |
the remote URL (path and query values survive redaction) | modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:75-81 |
| 12 | native_observability_spans |
payload.error_message |
the remote server and the network stack | modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:115-126 |
| 13 | native_observability_spans |
payload.status_code |
the remote server (integer) | modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:123 |
| 14 | native_observability_spans |
payload.request.path (request summary span) |
the visitor | modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:173 |
| 15 | native_observability_spans |
payload of custom spans |
any code calling startSpan() / endSpan() |
modules/native_observability_spans/src/Service/SpanRecorder.php:58,111,160-170 |
| 16 | native_observability_cache_event |
path |
the visitor | modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:75 |
| 17 | native_observability_cache_event |
payload.tags (cache tags carry entity ids; some modules build tags from input) |
code that sets tags; unverified whether visitor input can shape a tag | modules/native_observability_cache_observer/src/Service/ObservedCacheTagsInvalidator.php:150-152 |
| 18 | native_observability_cache_event |
status / payload.header_value (response header value) |
the code that sets X-Drupal-Cache / X-Drupal-Dynamic-Cache; unverified whether a third party can set it |
modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:130-143 |
| 19 | native_observability_database_query |
path |
the visitor | modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:141,166 |
| 20 | native_observability_database_query |
query_text (literal values written into SQL, when a statement has any) |
the visitor, through code that puts input directly into SQL; unverified for any real statement | modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:160,173 |
| 21 | native_observability_database_query |
method (and method in trace and cache tables) |
the client (request method; arbitrary token accepted by Symfony) | modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:140,165; src/EventSubscriber/TraceSubscriber.php:326 |
| 22 | native_observability_request_exclusion_rule |
label, path_pattern, query_conditions, notes |
an administrator; values are typically copied from visitor URLs | src/Service/RequestLogExclusionRepository.php:139-145 |
PrivacyPolicy coverage¶
Class: src/Service/PrivacyPolicy.php, registered as native_observability.privacy_policy (native_observability.services.yml:73-75).
What it covers¶
| Capability | Method | Behavior | Source |
|---|---|---|---|
| Sensitive path segments | redactSensitivePath() |
Replaces the one-time credentials in three core URL shapes (/user/reset/{uid}/{ts}/{hash}[/login], /user/{uid}/cancel/confirm/{ts}/{hash}) with [REDACTED], with or without language prefix and trailing slash. Route parameters named like secrets and token-like segments are redacted too. The uid stays |
:232-247 |
| Sensitive parameter names | isSensitiveKey(), isSensitiveName(), sensitiveKeys() |
Case-insensitive match on the whole name or one segment (key matches access_key, not keyword). Default list of 29 names; replaced by capture.sensitive_keys; switch capture.redact_sensitive_keys |
:31-49,76-91,142-162 |
| URIs | redactUri(), redactSensitiveUri() |
Values of sensitive query parameters and the password in the userinfo become [REDACTED]; the path is not inspected |
:99-102,284-331 |
| Client IP | applyToIp(), anonymize(), hash() |
Modes off, anonymized, hashed, full from capture.client_ip_mode |
:59-66,171-214 |
Where it is applied¶
| Call | Writer | Source |
|---|---|---|
redactSensitivePath() |
trace row and payload | src/EventSubscriber/TraceSubscriber.php:261,327 |
redactSensitivePath() |
request summary span | modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:173 |
redactSensitivePath() |
cache events (three writers) | modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:75; modules/native_observability_cache_observer/src/StackMiddleware/CachePageObserverMiddleware.php:110; modules/native_observability_cache_observer/src/Service/ObservedCacheTagsInvalidator.php:138 |
redactSensitivePath() |
database query rows | modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:141 |
redactSensitivePath() |
execution timeline (tempstore) | modules/native_observability_execution/src/EventSubscriber/ExecutionRequestSubscriber.php:127 |
isSensitiveName() |
query and body values in the trace payload |
src/EventSubscriber/TraceSubscriber.php:701-707 |
redactUri() |
outbound HTTP span URI, error message, host fallback | modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:73,117,193 |
sensitiveKeys() default list |
settings form | src/Form/NativeObservabilitySettingsForm.php:339 |
The IP mode is applied through PrivacyPolicy::applyToIp(), called by the trace subscriber. IPv4 loses the last octet and IPv6 keeps the first 64 bits.
The trace subscriber also keeps its own deny-list of headers that are never stored (authorization, cookie, set-cookie, x-csrf-token, x-drupal-csrf-token, x-forwarded-authorization, and the calibration token header): src/EventSubscriber/TraceSubscriber.php:58-70. It is separate from PrivacyPolicy.
What is not covered¶
- Untrusted-text marking.
PrivacyPolicydoes not mark a stored string as visitor-controlled. The query classes do it on read, throughsrc/Query/UntrustedText.php. - Path segments that carry identifiers or text. Only credential shapes, sensitive route parameters and token-like segments are matched.
/user/5,/search/jane.doe@example.comor/node/123/editare stored as sent (:233-237). - Query values in the path-like fields.
pathisgetPathInfo(), which has no query string, so the query is not in thepathcolumns. The query is stored only inpayload.querywhenquery_modeis notoff, where only sensitive names are redacted and the other values are kept (cut at 2048 bytes); names that are not in the list are not (src/EventSubscriber/TraceSubscriber.php:594,604,701-707). - Exception messages. Redacted by
PrivacyPolicy::redactExceptionMessage()and cut to a maximum length. Free text in them that is not a URL is stored as it is. The tools never return the message. - SQL text. Literals are redacted, identifiers are not (
SqlLiteralRedactor.php). - Header values other than the deny-list: the sensitive-key list applies to query and body names, not to header names (
src/EventSubscriber/TraceSubscriber.php:527-562). uid. Stored in its column unlesscapture.store_uidis off.PrivacyPolicy::storesUid()reports the switch to every sink.- Reading side.
PrivacyPolicyis applied when writing. Nothing in the policy filters rows on read, so an MCP tool must apply its own grade filter. Whether the dashboard, report and export readers add filtering is unverified (not audited for this page); the report's privacy-aware export controls are mentioned indocs/index.md.
Findings that may require core changes¶
Facts only. Each item names a value stored, or a gap, that matters for the exposure grades.
- Unmatched requests share one key. Requests without a route are stored under the fixed
route_keyunmatched, so the metrics table holds no visitor path.modules/native_observability_metrics/src/Service/RouteMetricsAggregator.php. parent_request_idcomes from a request header. It is stored only when it is a ULID or a UUID of at most 64 characters (src/EventSubscriber/TraceSubscriber.php). The client still chose it, so tools return it as untrusted text.- Exception messages are stored redacted and are never returned. They are cut to
capture.exception_message_max_length.src/EventSubscriber/TraceSubscriber.php:165-169,282-285. Setting:capture.exception, default on (config/install/native_observability.settings.yml:14). - SQL text is optional and redacted.
store_slow_query_textdefaults tofalse(modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:3). Arguments are never stored. Literals in the SQL string are replaced bySqlLiteralRedactor. - IP hashing is salted. The salt is generated on first use, kept in State (
PrivacyPolicy::SALT_STATE_KEY) and never written to config;$settings['native_observability_client_ip_hash_salt']overrides it. - Small counts are written.
request_countof 1 is stored (RouteMetricsAggregator.php). The reader used by the tools withholds groups underminimum_request_count(RouteMetricsReader.php). - No retention for the exclusion rule table. See above; the free-text fields
labelandnoteshave no cleanup path other than deleting the rule. - Any code can write arbitrary JSON into
native_observability_spans.payload.modules/native_observability_spans/src/Service/SpanRecorder.php:58,111,160-170. The family cannot vouch for what third-party callers store.