Skip to navigation

Native Plugin Diagnostics

View as Markdown

The Relay host records native plugin outcomes in its diagnostic context. Enable diagnostics in the host before loading plugins. A native plugin uses that context. It does not need a separate diagnostic configuration or store.

Use CLI and Daemon Diagnostics for a gateway or daemon. Use Embedded Diagnostics for a language-hosted application. If your Rust application loads plugins directly, select its context with load_native_plugins_with_diagnostics.

Host Recording Coverage

The host records outcomes at the operation that owns them:

OperationRecorded Evidence
Policy, artifact, and schema checksFailures from the shared dynamic plugin checks.
Library loadingNative load success or failure.
Plugin validationNative validation success or failure.
Component registrationNative registration success or failure.
Runtime callbacksNative callback failures.

Native observations use PluginExecution::NativeAbi. Their phase identifies policy, validation, activation, or runtime work. Failure classes come from structured status values. Ok, StreamEnd, and Backpressured are not failures. A later success does not erase an earlier failure.

Common component activation also records its own summary. Do not add that summary to a native operation’s failure count as if they were separate failed activations. Refer to producer boundaries for ownership across the two layers.

Record Internal Observations

The native SDK exposes canonical diagnostics through runtime.diagnostics(). Obtain the runtime from the PluginContext supplied during registration. Retain its diagnostic component in callbacks that perform the internal operation.

Use the same lookup example as the Rust and Python worker guides. It declares one failure code and one success code, then records the observed outcome with a request ID. Registration runs once for that responsibility. Refer to Plugin Diagnostics for the shared recording pattern and the differences between host acceptance and queue admission.

Native lookup diagnostics
use std::num::NonZeroU64;
use nemo_relay_plugin::PluginRuntime;
use nemo_relay_plugin::diagnostics::{
ComponentDeclaration, DiagnosticCode, DiagnosticComponent, DiagnosticError, DiagnosticOutcome,
Diagnostics, FailureClass, Observation, RecordOutcome,
};
/// Register the lookup vocabulary once for this activation.
pub fn register_lookup_diagnostics(
runtime: &PluginRuntime,
) -> Result<(Diagnostics, DiagnosticComponent), DiagnosticError> {
let diagnostics = runtime.diagnostics();
let component = diagnostics.component_lazy(|| ComponentDeclaration {
name: "example.lookup",
codes: &[
DiagnosticCode::Extension {
name: "lookup_failed",
description: "The lookup backend was unavailable",
outcome: DiagnosticOutcome::Failure,
allowed_classes: &[FailureClass::Unavailable],
},
DiagnosticCode::Extension {
name: "lookup_completed",
description: "A lookup completed",
outcome: DiagnosticOutcome::Success,
allowed_classes: &[],
},
],
})?;
Ok((diagnostics, component))
}
/// Record a lookup whose backend is known to be unavailable.
pub fn record_lookup_failure(
component: &DiagnosticComponent,
request_id: &str,
) -> Result<RecordOutcome, DiagnosticError> {
component.try_record_lazy(|| {
let mut observation = Observation::failure(
1,
FailureClass::Unavailable,
"The lookup backend was unavailable",
NonZeroU64::MIN,
);
observation.correlation.request_id = Some(request_id.into());
observation
})
}
/// Record an independently observed successful lookup.
pub fn record_lookup_success(
component: &DiagnosticComponent,
request_id: &str,
) -> Result<RecordOutcome, DiagnosticError> {
component.try_record_lazy(|| {
let mut observation = Observation::success(2, "A lookup completed", NonZeroU64::MIN);
observation.correlation.request_id = Some(request_id.into());
observation
})
}

component_lazy registers the complete code list together. Disabled or unsupported facades skip its factory and return an inactive component. Inactive components skip observation factories too. A code position is one-based within that list. Extensions declare their outcome and allowed failure classes. Common plugin failure codes can also be selected with DiagnosticCode::Builtin.

Each successful record returns RecordOutcome::Recorded. Its receipt contains these fields:

  • sequence identifies the accepted source update.
  • code_total holds the updated code count when available.
  • evidence is Retained, Truncated, or Dropped.

RecordOutcome::Disabled means no observation or loss count was produced. A dropped or truncated evidence payload does not undo accepted counts. Keep diagnostic errors separate from callback results. Do not retry an uncertain record automatically, since the host might already have counted it.

diagnostics.delivery_stats() reports SDK-local dropped, rejected, and uncertain submission totals. These are separate from host occurrence counts and retention loss. diagnostics.availability() reports collection availability. diagnostics.capabilities() reports the adapter limits and read grant without exposing diagnostic evidence.

Correlation and Lifetime

Observation::failure and Observation::success default to no correlation. Set request or session IDs through observation.correlation. The shared DiagnosticCorrelation type accepts borrowed and owned strings. Correlation never becomes a counter key and counts toward the input-text budget.

Native recording is synchronous. There is no delivery queue or flush call. Explicit component.close() fences all clones. Last drop also closes the registration. Activation teardown revokes handles and releases adapter backing.

Closing a registration does not reset context totals or imply recovery. Closing live components frees their slots, but cumulative extension and declaration text budgets remain consumed for the session. Register a stable vocabulary once instead of creating a new component for each request.

Concurrent Calls and Recursion

Cloned components can record from concurrent callback threads. Runtime and facade clones share four SDK registration, recording, or read slots per activation. Admission happens before producer factories and SDK scratch or buffer allocation. Exhaustion returns DiagnosticError::Busy without waiting or automatic retry. Local authoring refusals increment delivery_stats().dropped; read refusals do not change authoring counters.

Separately, the SDK admits one in-flight read query/output-buffer pair per activation. A read can overlap recording within the call limit; another read returns Busy before allocating buffers. Returned JSON and decoded pages are caller-owned and are outside this in-flight limit and the core memory budget. Host work has its own four process-wide data permits and serializes access to each activation. The SDK admission limit does not guarantee host admission.

Same-thread recursion into the same activation returns Busy. Nested calls into different activations are allowed, with up to 32 nested activations per thread tracked by each SDK copy. Guards release admission slots on return, error, or panic unwind. Component closure does not require a data-call permit.

Read Host Evidence

The canonical reader can inspect every component in the host’s selected context. Reads require the host’s explicit native_diagnostics_read_context grant. The default is false. Recording does not require this grant.

Set the grant in host policy using the manifest plugin ID. This example uses the checked native example’s ID:

Host policy
[plugins.policy.overrides."examples.rust_native_policy"]
native_diagnostics_read_context = true

Resolve the setting through the existing host configuration layers. Within policy, defaults apply first, followed by ordered matching rules and plugin-ID overrides. An explicit false can override an earlier grant. A plugin manifest or component configuration cannot grant access. Workers always receive no grant.

plugins inspect <id> and plugins validate <id-or-path> report the effective native_diagnostics_read_context permission in text and JSON. The value describes policy, not whether collection is enabled or an activation is live. Configuration values stay redacted. Apply policy changes through the existing owner-controlled reload or reactivation path.

This example reads discovery, a snapshot, and a separate memory page:

Authorized native reads
use nemo_relay_plugin::diagnostics::{
DiagnosticDiscovery, DiagnosticError, DiagnosticMemoryPage, DiagnosticQuery,
DiagnosticSnapshot, Diagnostics,
};
/// Read separate discovery, snapshot, and memory views after an explicit grant.
pub fn inspect_authorized_context(
diagnostics: &Diagnostics,
) -> Result<
(
DiagnosticDiscovery,
DiagnosticSnapshot,
DiagnosticMemoryPage,
),
DiagnosticError,
> {
let reader = diagnostics.reader()?;
let discovery = reader.discovery()?;
let query = DiagnosticQuery {
max_components: Some(1),
max_events: Some(1),
..Default::default()
};
let snapshot = reader.snapshot(&query)?;
let memory = reader.inspect_memory(&query)?;
// Keep these caller-owned values inside the authorized inspection boundary.
Ok((discovery, snapshot, memory))
}

The reader exposes these operations:

MethodResult
discovery()Canonical capabilities for the selected host context.
snapshot(&query)An independently owned DiagnosticSnapshot.
snapshot_json(&query)Canonical snapshot JSON with exact decimal-string counts.
inspect_memory(&query)An independently owned DiagnosticMemoryPage.
inspect_memory_json(&query)Canonical memory-page JSON.

Every read checks live activation and read authority. Without a grant, reader() returns permission_denied. After teardown, handles return closed. An authorized read can still fail if collection or queries are disabled, a cursor is invalid, or the page cannot fit. Use DiagnosticError::reason() for stable error labels.

Follow the Diagnostics API for query fields, precision, pagination, and coverage. Snapshot and memory pages have separate cursors and are captured independently. Decoded pages and SDK buffers belong to the caller and use memory outside the core budget. A short output buffer never yields a usable partial page. Reads do not start a service or resource collector.

A read grant exposes other components’ restricted detail too. Filters select results but do not narrow permission. Keep returned data within the authorized inspection boundary. Snapshot reads never automatically feed logs or exporters.

Handle SDK Errors

Use stable reason labels to handle adapter errors:

ReasonMeaning and Response
unsupportedThe host has no compatible diagnostics extension. Continue without optional diagnostics.
unavailableAdapter session storage could not be admitted. Keep the operation’s original result.
closedThe activation or registration is closed. Stop using its handles.
permission_deniedHost policy did not grant whole-context reads. Recording can still be available.
busyA call/read slot is unavailable or the call recurses into the same activation. Native recording does not retry automatically.
capacityA declaration or request exceeds an adapter limit. Reduce its size or vocabulary.
invalidAn input value or handle is invalid. Check code positions, outcomes, and failure classes.
conflictA local component name is already registered. Reuse its existing handle.
buffer_too_smallA caller-owned output buffer cannot fit the complete response. Ignore the output.
internalAdapter acceptance is uncertain. Do not automatically replay the observation.

DiagnosticError::Read carries a canonical read reason, such as disabled, query_disabled, invalid_cursor, or snapshot_capacity. Refer to the shared read errors for their meaning. Include a fallback when matching the non-exhaustive Rust error enum.

Admission Limits

Native adapter limits apply in addition to core diagnostic storage limits:

ResourceLimit
Authoring sessionsUp to 32 per host process.
Live componentsUp to eight per session.
CodesUp to 32 per component.
Extension declarationsUp to 16 over the session’s lifetime.
Declaration textUp to 16 KiB over the session’s lifetime.
Aggregate input textUp to 16 KiB per call, including detail and correlation.
Concurrent host data callsUp to four per process. Cleanup does not need a data permit.
Queries and responsesThe selected context’s query and page limits.

Extension names allow 32 UTF-8 bytes. Descriptions allow 256 UTF-8 bytes. Core detail and correlation limits still apply and can truncate accepted evidence. Capacity errors during registration do not disable existing recorders. Full event history can evict older evidence while counters keep updating.

Capabilities report adapter layout bounds and the current activation’s session bytes separately from core storage. These values do not measure total process RSS or allocator overhead. Caller-owned read results have their own memory cost.

SDK and ABI Boundaries

The optional NativeDiagnosticsApiV1 table follows the frozen execution ABI-8 prefix. The SDK checks the table’s size and independent contract version before using it. Execution ABI 8, ABI-7 negotiation, and manifest native API 1 remain unchanged. This adds an optional tail to the native host table.

ContractCurrent Version
Execution ABI8
Manifest compat.native_api"1"
Diagnostics extension contract1

Incompatible diagnostics callback or payload layout changes require versioning that extension contract. Changes to private SDK synchronization do not change these ABI layouts or callback signatures.

Core recorders and Rust-owned objects stay inside the host. Registration and recording cross the ABI as fixed layouts with borrowed UTF-8 spans. The host retains no plugin input pointers. A compatible host without the extension exposes an unsupported facade and keeps existing execution behavior.

PluginRuntime::runtime_diagnostics() remains the older host-level report from ABI v4. It is ordered by code and does not identify the plugin that recorded an entry. It does not return canonical snapshots or fall back to them. Refer to Runtime Events and Scopes for that older API, and the native ABI reference for table ownership and compatibility details.

A plugin that requires canonical diagnostics must declare a Relay compatibility range that includes this extension. Do not treat compat.native_api = "1" as proof that every compatible host exposes it. Native code remains trusted and shares the host process.