Native Plugin Diagnostics
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:
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.
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:
sequenceidentifies the accepted source update.code_totalholds the updated code count when available.evidenceisRetained,Truncated, orDropped.
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:
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:
The reader exposes these operations:
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:
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:
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.
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.