> This page is for NeMo Relay, version dev.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.4) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.11.0: https://docs.nvidia.com/nemo/relay/v0.11.0/llms.txt
> - v0.10.4: https://docs.nvidia.com/nemo/relay/v0.10.4/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# Embedded Diagnostics

> Configure process-owned diagnostics and inspect bounded snapshots from Rust, Python, and Node.js.

## Embedded Language Runtimes

Rust, Python, and Node.js initialize diagnostics before
validating or activating plugins. Use the existing `plugin.initialize` entry
point and configure diagnostics in `plugins.toml`. You can pass an explicit file
through the existing file argument. No separate setup call is needed. Embedded
diagnostics does not need CLI configuration, a listener, an exporter, or a framework
adapter.

```toml title={"plugins.toml"} showLineNumbers={false}
# plugins.toml: uses the existing user/explicit and system file layers.
[diagnostics]
enabled = true

[diagnostics.limits]
max_events = 256
max_snapshot_bytes = 131072
```

Configuration files merge field by field in the existing resolver's order.
If your code omits `diagnostics`, the service uses the merged file settings.
If your code supplies `diagnostics`, that profile replaces all file settings for
the service. Omitted fields in that profile use core defaults.

Service settings do not enter plugin component configuration, execution
fingerprints, or legacy plugin reports. Invalid settings fail startup without
falling back to another profile. `validate` checks the profile without starting
the service.

The first initialization selects the process-owned service, enabled or disabled.
Repeated initialization with equivalent settings reuses it. Changed settings return
`diagnostics: configuration_conflict`. The selected context and existing activation
remain intact. Change service settings by restarting the process. Plugin failure,
close, replacement, and reload do not erase the service or its lifetime counters.
The service does not keep the process alive and does not persist evidence after exit.

Importing Python or registering a Node environment does not select a diagnostic
default. Existing logging behavior stays the same. When diagnostics starts, a
Relay-owned default logger attaches once. It uses its existing sinks, queues, and
rate limits, and does not replay earlier logs. Application-owned loggers stay in
place. Reader metadata reports logging coverage separately.

Node runtime environments share the service. Unloading one does not close another
environment's logger. After `fork`, inherited diagnostic handles return
`fork_requires_exec` before taking service locks. Start a fresh executable process
to use diagnostics in the child.

### Optional Readers

A reader grants access to detail that can contain unsanitized data. Query filters
do not restrict that authority. Snapshots also contain totals for the whole context.
If your application serves reads to other clients, it must authorize each client.
Snapshots do not flow into subscribers, exporters, legacy reports, exception
attachments, or operational logs. OpenTelemetry (OTel) integration is not yet
supported. Among embedded application bindings, only the Rust runtime API creates
custom components and recorders. Native Rust and Rust/Python worker SDKs also
support plugin-internal recording through their separate diagnostic facades.
Refer to [Plugin Diagnostics](/nemo/relay/dev/build-plugins/diagnostics) for those APIs. The
[recording and classifier guide](/nemo/relay/dev/reference/diagnostics) describes direct Rust
runtime producers.

The supported embedded reader operations are:

| Operation                                        | Python                        | Node.js                       | Rust embedded adapter              |
| ------------------------------------------------ | ----------------------------- | ----------------------------- | ---------------------------------- |
| Obtain an existing service reader                | `diagnostics.reader()`        | `diagnostics.reader()`        | `embedded::reader()`               |
| Capture with a native query                      | `reader.snapshot(query)`      | `reader.snapshot(query)`      | `reader.snapshot(&query)`          |
| Capture canonical JSON                           | `reader.snapshot_json(query)` | `reader.snapshotJson(query)`  | `reader.snapshot_json(query_json)` |
| Inspect budgets, occupancy, and logging coverage | `reader.info()`               | `reader.info()`               | `reader.info()`                    |
| Release a reader or page                         | Release Python references     | Release JavaScript references | Drop the reader or page            |

Python returns caller-owned dictionaries with canonical `snake_case` keys and
Python `int` diagnostic integers. Node.js returns caller-owned `camelCase` objects
and `bigint` diagnostic integers. For entry and component accounting and resource
context, use the
[memory reader operations](/nemo/relay/dev/reference/diagnostics/memory#detailed-memory-inspection).
These representations also apply to memory pages and aggregate memory gauges.
Rust returns an adapter-owned canonical page.
`page.as_str()` borrows it, and `serde_json::from_str::<DiagnosticSnapshot>()`
decodes a separate caller-owned value. Drop each Rust page after use to return its
copy permit. Dropping a reader does not stop the service in any binding.

### Startup, Snapshots, and Metadata

These examples use a complete programmatic diagnostics profile. Omit it to use
the existing file resolver. `validate` checks the selected host profile without
starting the service. `initialize` selects the process service. The exact plugin
validation APIs accept component configuration only and reject a top-level
`diagnostics` field. Use ordinary host `validate` for service settings.

#### Python

```python title={"startup.py"} showLineNumbers={false}
import json

from nemo_relay import diagnostics, plugin


async def inspect_runtime():
    config = plugin.PluginConfig(diagnostics={"enabled": True})
    plugin.validate(config)
    activation = await plugin.initialize(config)
    try:
        reader = diagnostics.reader()  # Does not start or enable collection.
        page = reader.snapshot({"component_key": "logging"})
        failures = page["lifetime"]["totals"]["failures_total"]["value"]
        assert type(failures) is int
        wire = json.loads(reader.snapshot_json())
        assert isinstance(wire["sequence"], str)
        info = reader.info()
        reserved = info["binding_reserved_bytes"]
        logging_coverage = info["logging_coverage"]
        # Inspect these values only inside the authorized application boundary.
    finally:
        await activation.close()
    final_page = reader.snapshot()  # The service outlives this activation.
    return {"reserved_bytes": reserved, "logging_coverage": logging_coverage, "snapshot": final_page}
```

#### Node.js

```javascript title={"startup.js"} showLineNumbers={false}
const plugin = require('nemo-relay-node/plugin');
const diagnostics = require('nemo-relay-node/diagnostics');

async function inspectRuntime() {
  const config = { diagnostics: { enabled: true } };
  plugin.validate(config);
  const activation = await plugin.initialize(config);
  let reader;
  try {
    reader = diagnostics.reader();
    const page = reader.snapshot({ componentKey: 'logging' });
    const failures = page.lifetime.totals.failuresTotal.value; // bigint
    const wire = JSON.parse(reader.snapshotJson());
    const sequence = wire.sequence; // Decimal string; portable across languages.
    const info = reader.info();
    const reserved = info.bindingReservedBytes;
    const loggingCoverage = info.loggingCoverage;
    // Inspect these values only inside the authorized application boundary.
  } finally {
    await activation.close();
  }
  const finalPage = reader.snapshot(); // The service outlives this activation.
}
```

#### Rust

```rust title={"startup.rs"} showLineNumbers={false}
use nemo_relay::{
    diagnostics::{DiagnosticQuery, DiagnosticSnapshot, DiagnosticsSettings, embedded},
    plugin::dynamic::{PluginHostConfig, initialize, validate},
};

let config = PluginHostConfig {
    diagnostics: Some(DiagnosticsSettings {
        enabled: true,
        ..Default::default()
    }),
    ..Default::default()
};
validate(config.clone(), None)?;
let mut activation = initialize(config, None).await?;
let reader = embedded::reader()?;
let page = reader.snapshot(&DiagnosticQuery::default())?;
let snapshot: DiagnosticSnapshot = serde_json::from_str(page.as_str())?;
let failures: u64 = snapshot.lifetime.totals.failures_total.value.get();
drop(page); // Return this page's adapter permit before another capture.
let info = reader.info()?;
let reserved = info.binding_reserved_bytes;
let logging_coverage = info.logging_coverage;
activation.close()?;
let final_page = reader.snapshot_json("{}")?; // Same process-owned service.
```

`JSON.stringify` cannot serialize Node's native `bigint` values. Use
`snapshotJson()` for portable JSON or `inspectMemoryJson()` for a memory page.
Canonical JSON uses `snake_case` keys and decimal strings for 64-bit integers in
every language.

Native query helpers use these numeric types:

* Python accepts `int` for 64-bit fields such as `after_sequence`.
* Node requires `bigint` for 64-bit fields such as `afterSequence`. A JavaScript
  `number` is rejected in those fields.
* Rust uses `DiagnosticNumber` for 64-bit query fields.

Row caps such as `max_events` and `maxEvents` remain small integers. Unknown code,
failure-class, execution, and phase names stay strings. Conversion preserves
saturation markers and missing optional values.

### Bounded Pagination

Follow the shared [pagination contract](/nemo/relay/dev/reference/diagnostics/api#continue-or-restart-a-read):
keep selectors and limits unchanged, pass the returned continuation, and restart
with a fresh caller budget if the source revision changes. Native continuations
can be passed directly back to their binding. Do not convert Node `bigint` cursor
fields to `number`. Context lifetime totals repeat across pages and must not be
summed.

The examples below limit the caller to 32 pages and consume each page immediately.
The runnable examples report page-budget exhaustion explicitly. In a live service,
also handle `busy` and `invalid_cursor` using the error contract below.

#### Python

```python title={"paginate.py"} showLineNumbers={false}
from nemo_relay import diagnostics

reader = diagnostics.reader()
query = {"max_components": 1, "max_events": 1}
for _ in range(32):
    page = reader.snapshot(query)
    # Inspect page["components"] and page["events"] within the authorized boundary.
    if page["continuation"] is None:
        break
    query["continuation"] = page["continuation"]
else:
    raise RuntimeError("The caller's page budget was exhausted")
```

#### Node.js

```javascript title={"paginate.js"} showLineNumbers={false}
const query = { maxComponents: 1, maxEvents: 1 };
let complete = false;
for (let remaining = 32; remaining > 0; --remaining) {
  const page = reader.snapshot(query);
  // Inspect page.components and page.events within the authorized boundary.
  if (page.continuation === null) {
    complete = true;
    break;
  }
  query.continuation = page.continuation;
}
if (!complete) throw new Error("The caller's page budget was exhausted");
```

#### Rust

```rust title={"paginate.rs"} showLineNumbers={false}
let mut query = DiagnosticQuery {
    max_components: Some(1),
    max_events: Some(1),
    ..Default::default()
};
let mut complete = false;
for _ in 0..32 {
    let page = reader.snapshot(&query)?;
    let snapshot: DiagnosticSnapshot = serde_json::from_str(page.as_str())?;
    // Inspect snapshot.components and snapshot.events within the authorized boundary.
    query.continuation = snapshot.continuation;
    if query.continuation.is_none() {
        complete = true;
        break;
    }
} // Each page's adapter permit is returned on drop.
if !complete {
    return Err("The caller's page budget was exhausted".into());
}
```

### Handle Reader Errors

Branch on stable reasons rather than exception text. A reader error contains no
query text or retained detail. This invalid-query example demonstrates the error
surface without logging any snapshot.

#### Python

```python title={"handle_error.py"} showLineNumbers={false}
from nemo_relay import diagnostics

reader = diagnostics.reader()
try:
    reader.snapshot({"unknown": True})
except diagnostics.DiagnosticsError as error:
    if error.reason != "invalid_query":
        raise
```

#### Node.js

```javascript title={"handle_error.js"} showLineNumbers={false}
try {
  reader.snapshot({ unknown: true });
} catch (error) {
  if (!(error instanceof diagnostics.DiagnosticsError) || error.reason !== 'invalid_query') {
    throw error;
  }
}
```

#### Rust

```rust title={"handle_error.rs"} showLineNumbers={false}
match reader.snapshot_json(r#"{"unknown":true}"#) {
    Err(error) if error.reason() == "invalid_query" => {}
    Err(error) => return Err(error.into()),
    Ok(_) => return Err("An unknown query field was accepted".into()),
}
```

Complete runnable Python, Node.js, and Rust examples are maintained in the
[embedded diagnostics examples](https://github.com/NVIDIA/NeMo-Relay/tree/c88f30af19a92b492122e1301c3acf38f690837b/examples/diagnostics).
The Python and Node examples run through their binding test suites. Build and run
the Rust example with `cargo run -p nemo-relay --example embedded_diagnostics`.
Direct Rust owners can also compose explicit `Diagnostics` contexts and core
readers as shown in [Runtime Diagnostics](/nemo/relay/dev/reference/diagnostics#rust-context-startup).

### Availability, Budgets, and Copy Ownership

Calling `reader()` before configuration returns `not_initialized`. It does not
create a disabled default. A configured disabled reader exposes metadata, but
snapshot reads return `disabled`.

Other errors have stable reasons:

* `busy` indicates that all read slots are in use.
* `query_disabled` indicates that no snapshot workspaces are configured.
* `invalid_query` and `invalid_cursor` identify invalid read requests.
* `snapshot_capacity` indicates that the requested page cannot fit.
* `unsupported_schema` identifies an unsupported snapshot version.
* `allocation_failed` reports an allocation failure.
* `fork_requires_exec` indicates that an inherited handle cannot be used after
  `fork`.

Python and Node.js helper errors expose `.reason`. No failure contains query text
or retained detail.

`info()` reports core limits,
core memory occupancy, logging coverage, and the independent adapter budget:

| Resource                                            | Bound                                                                                          |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Encoded query                                       | Up to 4,096 UTF-8 bytes. Duplicate or unknown fields are rejected, including nested selectors. |
| Simultaneous snapshot/memory copies and conversions | Two per service, shared by all reader clones                                                   |
| Payload per copy                                    | Configured core `max_snapshot_bytes`                                                           |
| Default copy reservation                            | 262,144 bytes: two 131,072-byte pages                                                          |
| Waiting queue                                       | There is no waiting queue. A read returns `busy` when all slots are in use.                    |

Disabled services and services with `max_queries = 0` allocate no copy payload
buffers. Enabled services reserve the copy payloads during startup. Those bytes
are additional to core `max_owned_bytes`.
After copying a canonical page, the adapter releases the core query workspace.
It keeps its own copy permit until language conversion succeeds or fails.

The service's payload budgets exclude the following memory:

* JSON strings kept by callers.
* Decoded objects.
* Python and JavaScript object overhead.
* Allocator metadata.

These budgets do not limit total process memory or resident set size (RSS).

Reads are synchronous and bounded. They do not create background tasks or wait
queues. A reader does not reset counters, infer readiness, or supply an empty
snapshot when evidence is unavailable. The separate [memory view](/nemo/relay/dev/reference/diagnostics/memory#detailed-memory-inspection)
provides entry and component backing attribution and existing resource-sample context.