Services¶
This page lists the services a third-party module could realistically inject: storages, builders, resolvers, and similar API-shaped classes. It skips event subscribers, controllers-as-services, and Guzzle middleware, even where they are declared public: true, because that flag on those classes exists only to satisfy Drupal's compiler passes (event subscribers and router-resolved controllers must be public; middleware resolved by service ID from HandlerStackConfigurator must be public too), not to offer an API surface.
Across the whole family's *.services.yml files, 48 service definitions are marked public: true. Of those, 19 are event subscribers, controllers, or middleware (wiring, not API). The remaining 29 are listed below. Everything not in these tables is private: it can still be autowired by class or interface from your own module's service definitions (compile-time DI does not care about visibility), but it cannot be fetched with \Drupal::service('id') or $container->get('id'), and no snake_case service ID is guaranteed to exist for it.
native_observability (core)¶
| Service ID | Class / interface | Public | Purpose |
|---|---|---|---|
native_observability.request_log_exclusion_rule_normalizer |
RequestLogExclusionRuleNormalizer |
Yes | Normalizes stored request-log exclusion rules for runtime matching. |
native_observability.request_log_exclusion_repository |
RequestLogExclusionRepository |
Yes | Stores and loads request-log exclusion rules. |
native_observability.diagnostics |
DiagnosticsService |
Yes | Collects safe runtime diagnostics (no secrets) for the status page. |
native_observability.trace_storage |
TraceStorageInterface (DatabaseTraceStorage) |
Yes | Reads and writes the native_observability_trace table. The interface a third-party module should type-hint to query or insert traces. |
native_observability.destination_redirect_guard |
DestinationRedirectGuard |
Yes | Guards against open-redirect attacks driven by a ?destination= query parameter on module forms. |
native_observability.ui_builder |
NativeObservabilityUiBuilder |
Yes | Builds safe, reusable UI render arrays for Native Observability admin pages. |
native_observability.cleanup_manager |
ObservabilityCleanupManager |
Yes | Coordinates observability data cleanup across every service tagged native_observability.cleanup_handler. |
native_observability.service_integrity_inspector |
ServiceIntegrityInspector |
Yes | Inspects the live container/kernel for the family's decorators, wrappers, middleware, and subscribers; backs drush no:sanity-check. |
native_observability.request_log_exclusion_matcher (RequestLogExclusionMatcher) is private, but its isRequestExcluded(?Request $request): bool method is static and reads the exclusion flag already set on the request by the (public) subscriber. Code that only needs to check whether the current request is excluded can call RequestLogExclusionMatcher::isRequestExcluded($request) directly without injecting the service, see Checking whether a request is excluded below. native_observability.request_id_generator (RequestIdGeneratorInterface) and native_observability.privacy_policy (PrivacyPolicy) are private with no public alias.
native_observability_cache_observer¶
No service in this module is public. CacheObserverStorage, CacheObserverEnvironmentInspector, CacheObserverEnvironmentSnapshotManager, and CacheEventInsertBuffer are all private, resolved only through autowiring inside the module (or, for CacheObserverStorage and CacheObserverEnvironmentSnapshotManager, through the @? optional-reference syntax that native_observability_dashboard uses in its own services.yml). A third-party module wanting the cache observer's data should read it from native_observability_export's Prometheus output, or type-hint CacheObserverStorage directly as a class (autowiring resolves it regardless of visibility, but there is no service ID to look it up by string).
native_observability_dashboard¶
| Service ID / class | Public | Purpose |
|---|---|---|
Drupal\native_observability_dashboard\Service\TraceMetricsReader |
Yes | Reads raw trace metrics from storage for the dashboard's KPI sections. |
native_observability_dashboard.time_range_resolver (DashboardTimeRangeResolver) |
Yes | Resolves the selected dashboard time range from the current request. |
native_observability_dashboard.overview_builder (DashboardOverviewBuilder) |
Yes | Builds the Overview tab payload. |
native_observability_dashboard.cache_builder (DashboardCacheBuilder) |
Yes | Builds the Cache tab payload. |
native_observability_dashboard.database_builder (DashboardDatabaseBuilder) |
Yes | Builds the Database tab payload. |
native_observability_dashboard.forensic_route_analysis_builder (ForensicRouteAnalysisBuilder) |
Yes | Builds the Forensic Route Analysis payload that correlates traces, spans, and outbound HTTP/SQL signals for a route. |
Every other service in this module (section builders, formatters, resolvers) is private and internal to how the dashboard assembles its own render arrays.
native_observability_database_observer¶
No service in this module is public. DatabaseObserverStorage, DatabaseQueryRuntimeCollector, DatabaseQueryClassifier, and DatabaseQueryInsertBuffer are all private. native_observability_dashboard's own services.yml documents this directly: it injects DatabaseObserverStorage as '@?Drupal\native_observability_database_observer\Service\DatabaseObserverStorage' by fully-qualified class name, not by a snake_case service ID, because no such public ID exists.
native_observability_execution¶
| Service ID | Class / interface | Public | Purpose |
|---|---|---|---|
native_observability_execution.timeline_builder |
ExecutionTimelineBuilder |
Yes | Builds the render array for an execution timeline shown on the trace details page. |
native_observability_execution.guzzle_trace_middleware |
GuzzleTraceMiddleware |
Yes (tagged http_client_middleware) |
Records outbound HTTP spans for every request issued through http_client. Public because HandlerStackConfigurator resolves it by service ID at runtime, not because it is meant for direct injection. |
ExecutionSpanStack and ExecutionSpanRecorderInterface (ExecutionSpanRecorder) are private.
native_observability_export¶
| Service ID | Class / interface | Public | Purpose |
|---|---|---|---|
native_observability_export.secret_storage |
SecretStorageInterface (StateSecretStorage) |
Yes | Stores the write-only bearer tokens used by the Prometheus and OTLP scrape/push endpoints, backed by Drupal State (not exported with config:export). |
native_observability_export.prometheus_metrics_builder |
PrometheusMetricsBuilder |
Yes | Builds Prometheus-compatible metrics output from the live source tables. |
native_observability_export.elastic_telemetry_builder |
ElasticTelemetryBuilder |
Yes | Builds Elastic-compatible NDJSON telemetry documents from stored traces. |
LivePrometheusMetricsReader, the service that actually queries the trace, spans, and cache event tables, is private; PrometheusMetricsBuilder depends on it internally.
native_observability_metrics¶
| Service ID | Class | Public | Purpose |
|---|---|---|---|
native_observability_metrics.route_metrics_aggregator |
RouteMetricsAggregator |
Yes | Aggregates route performance metrics (throughput, average duration, P95/P99) from observability traces. Backs drush native-observability-metric:aggregate. |
PercentileCalculator and DatabaseMetricsStorage are private.
native_observability_otel¶
| Service ID | Class | Public | Purpose |
|---|---|---|---|
native_observability_otel.exporter_manager |
ExporterManager |
Yes | The plugin manager for NativeObservabilityExporter plugins. Use createInstance($id) or getDefinitions() to work with exporters directly instead of through the registry. |
native_observability_otel.otel_payload_builder |
OpenTelemetryPayloadBuilder |
Yes | Builds OTLP-compatible JSON payloads for exported traces. |
ExporterRegistry, the thin helper OpenTelemetryTraceSubscriber itself uses (get(), getAvailableExporters()), is private with no public service ID. A third-party module that wants the same convenience should type-hint Drupal\native_observability_otel\Service\ExporterRegistry directly in its own service constructor; autowiring resolves it at compile time regardless of visibility.
native_observability_report¶
| Service ID | Class | Public | Purpose |
|---|---|---|---|
native_observability_report.narrative_loader |
ReportNarrativeLoader |
Yes | Loads and resolves report narrative YAML definitions. |
native_observability_report.environment_resolver |
ObservabilityEnvironmentResolver |
Yes | Resolves the current execution environment for reporting purposes. |
native_observability_report.scoring |
ObservabilityScoringService |
Yes | Computes normalized statuses for report sections. |
native_observability_report.document_builder |
ObservabilityReportDocumentBuilder |
Yes | Builds printable report documents from prepared preview payloads. |
native_observability_report.narrative_path_validator |
NarrativePathValidator |
Yes | Enforces the narrative file location policy for custom narrative overrides. |
This is the only module in the family where every declared service is public; its own services.yml states the reason directly: all five are reached by string ID from callers.
native_observability_spans¶
| Service ID | Class / interface | Public | Purpose |
|---|---|---|---|
native_observability_spans.span_storage |
SpanStorageInterface (DatabaseSpanStorage) |
Yes | Reads and writes the native_observability_spans table. The interface a third-party module should type-hint to query spans. |
native_observability_spans.span_insert_buffer |
SpanInsertBuffer |
Yes | The deferred-persistence buffer for the spans table, flushed on kernel.terminate alongside every other tagged buffer. |
SpanRecorderInterface (SpanRecorder) and ActiveSpanContextStack are private. GuzzleSpanMiddleware is public but, like its execution counterpart, only because HandlerStackConfigurator resolves it by service ID at runtime.
Writing a cleanup handler¶
A module that stores its own observability rows can hand retention to this module by tagging a
service with native_observability.cleanup_handler and implementing
ObservabilityCleanupHandlerInterface.
final class MyCleanupHandler implements ObservabilityCleanupHandlerInterface {
public function timestampColumn(): string {
return 'created_at';
}
// deleteOlderThan(), enforceMaxRows() and the rest of the interface.
}
timestampColumn() is required from 2.0.0 and returns a literal column name. An earlier version
of this module guessed the column by probing the schema for a list of candidate names, which left
a handler and its table silently out of step when neither name matched. The method turns that into
an error the first time cleanup runs.
Checking whether a request is excluded¶
RequestLogExclusionMatcher answers two different questions about the current request, and every
observer in this family, meaning TraceSubscriber, the cache and database observers, the spans
subscribers and the OTLP exporter subscriber, asks it rather than deciding on its own:
- Does a user-configured exclusion rule match?
RequestLogExclusionSubscriber::onKernelRequest()callsRequestLogExclusionMatcher::isExcluded()onkernel.request, against the rules managed at/admin/config/development/native-observability/exclusions. - Does the request target one of this module's own routes? Once the route name is resolved,
RequestLogExclusionSubscriber::onKernelRequestOwnRoute()callsRequestLogExclusionMatcher::isOwnRouteExcluded(), governed by thecapture.exclude_own_routessetting (see Configuration). It is on by default: opening the dashboard, scraping the Prometheus endpoint, or browsing the settings forms would otherwise inflate request counters, skew latency percentiles, and fill the trace list with self-referential rows that say nothing about the site being observed.
Both decisions land on the same request attribute, RequestLogExclusionMatcher::ATTR_EXCLUDED
(native_observability.request_excluded). A new observer does not need to know which of the two
produced the answer, or evaluate either question itself: call the static
RequestLogExclusionMatcher::isRequestExcluded($request) and honor what it returns.
The calibration exception¶
The module ships two calibration routes, native_observability.calibration_minimal and
native_observability.calibration_calibrated, that exist to be measured by the overhead
measurement protocol (see Measuring the overhead). They carry the same
native_observability route-name prefix as every other page this module ships, so the own-route
check above would ordinarily exclude them too, which would make a measurement run write nothing
and go silently blank. RequestLogExclusionMatcher::isOwnRouteExcluded() exempts them, but only
while CalibrationWindow::isOpen() reports a measurement in progress. Outside that window the
calibration routes are indistinguishable from any other own route and are excluded like the rest.
Why an observer should not re-derive this itself¶
Subscribers that need to check own-route exclusion again later in the request lifecycle, after
kernel.request already settled ATTR_EXCLUDED, for example at kernel.response, where
TraceSubscriber and the OTLP exporter subscriber both do it, inject RequestLogExclusionMatcher
and call isOwnRouteExcluded($request) directly, rather than reading capture.exclude_own_routes
themselves or comparing the route name prefix by hand. That call is the only place both the config
check and the calibration exemption above are applied together; a private copy of either piece will
match today's behaviour and then quietly stop matching it the day the shared decision changes, for
example if the calibration exemption above gains a new case. Nothing fails loudly when that
happens; the symptom is data that is missing, or unexpectedly present, with no obvious link back to
the copy that fell out of step.
Summary¶
| Module | Public API services | Public but wiring-only (subscribers/controllers/middleware) |
|---|---|---|
| native_observability | 8 | 3 |
| native_observability_cache_observer | 0 | 1 |
| native_observability_dashboard | 6 | 5 |
| native_observability_database_observer | 0 | 1 |
| native_observability_execution | 1 | 2 |
| native_observability_export | 3 | 0 |
| native_observability_metrics | 1 | 0 |
| native_observability_otel | 2 | 1 |
| native_observability_report | 6 | 0 |
| native_observability_spans | 2 | 6 |
| Total | 29 | 19 |
Two modules, cache_observer and database_observer, expose no injectable API at all: everything they store stays private, reachable only through the dashboard's optional @? references or through the exports built on top of it.