Skip to content

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 *.services.yml files of the twelve tiered modules, 80 service definitions are marked public: true. Of those, 20 are event subscribers, controllers, or middleware (wiring, not API). The remaining 60 are listed below. The native_observability_mcp submodule is not counted: its public services are event subscribers, OAuth hardening and admin page helpers, and the MCP tools are tool plugins, see Native Observability via MCP. 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.exposure_policy ExposurePolicy Yes Answers whether a consumer (mcp, export, otel) may receive aggregates or detail under the exposure grade. Code that sends observability data out of the site should ask it first, see Sending data out of the site.
Drupal\native_observability\Query\RoutePerformanceQuery, CacheEventQuery, OverheadQuery, SlowStatementQuery, TraceListQuery read-only query classes Yes The queries behind the MCP tools. Each answers by exposure grade, applies the privacy policy and returns an envelope with the period, the unit and the number of groups withheld. They do not depend on the tool or mcp_server modules.
Drupal\native_observability\Query\UntrustedText, LegendBuilder helpers Yes Cuts visitor-written text to exposure.untrusted_text_max_length and builds the legend of a query result.

The other 20 public core services are internal plumbing, public only so that other parts of the family can fetch them by ID: native_observability.database_capabilities, native_observability.capture_gate, native_observability.capture_cost_ledger, native_observability.calibration_route_access_check, the cache.native_observability_calibration cache bin, and the overhead measurement services (native_observability.overhead_workload_catalog, _row_accountant, _capture_toggle, _duration_share_classifier, _measurement_store, _workload_runner, _measurement_runner, _calibration_preflight, _calibration_window, _ledger_pipeline_order, _submodule_dependents_resolver, _result_printer, _admin_result_presenter, _measurement_guard and _measurement_operations, all prefixed native_observability.). Third-party code has no reason to depend on them.

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 API service in this module is public (the only public definition is an event subscriber). CacheObserverStorage (service ID native_observability_cache_observer.storage), 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; its service ID is private, so it cannot be fetched 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.
Drupal\native_observability_dashboard\Service\CsvExportFilenameBuilder Yes Builds the file names of the dashboard CSV exports.
Drupal\native_observability_dashboard\Service\MetricValueFormatter Yes Formats metric values for display.
Drupal\native_observability_dashboard\Service\DatabaseObservabilitySectionBuilder Yes Builds the database observability section payload, including the recent records a server-rendered consumer such as the report prints.

Every other service in this module 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 Prometheus scrape bearer token, 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.

| native_observability_metrics.route_metrics_reader | RouteMetricsReader | Yes | Returns the stored aggregates by route name only, and only the groups at or above minimum_request_count. |

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';
  }

  // id(), isAvailable(), cleanup(array $settings), cleanupAll().
}

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() calls RequestLogExclusionMatcher::isExcluded() on kernel.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() calls RequestLogExclusionMatcher::isOwnRouteExcluded(), governed by the capture.exclude_own_routes setting (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 behavior 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.

Sending data out of the site

A module that sends observability data to another system, for example a custom exporter plugin, should follow the exposure grade like the shipped consumers do.

use Drupal\native_observability\Service\ExposurePolicy;

if (!$exposurePolicy->allows(ExposurePolicy::CONSUMER_EXPORT, ExposurePolicy::LEVEL_DETAIL)) {
  return FALSE;
}

allows() takes one of the consumers mcp, export and otel and a level, aggregate or detail. It answers FALSE at grade 0, TRUE for aggregate only at grade 1, and TRUE for both at grade 2 once the declaration is recorded. In legacy mode it answers TRUE for export and otel and never for mcp. Send route machine names, never a path or a URL, and run any free text through PrivacyPolicy::redactText() first.

Removed class

Drupal\native_observability\Batch\OverheadMeasurementBatch is gone. Its logic moved to the service native_observability.overhead_measurement_operations (OverheadMeasurementOperations), which the procedural batch callbacks in native_observability.module call. Code that called the old static methods has to use the service.

Summary

Module Public API services Public but wiring-only (subscribers/controllers/middleware)
native_observability 36 3
native_observability_cache_observer 0 1
native_observability_dashboard 9 6
native_observability_database_observer 0 1
native_observability_execution 1 2
native_observability_export 3 0
native_observability_metrics 2 0
native_observability_otel 2 1
native_observability_report 5 0
native_observability_spans 2 6
Total 60 20

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.