Skip to navigation

Embedded Diagnostics

View as Markdown

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.

plugins.toml
# 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 for those APIs. The recording and classifier guide describes direct Rust runtime producers.

The supported embedded reader operations are:

OperationPythonNode.jsRust embedded adapter
Obtain an existing service readerdiagnostics.reader()diagnostics.reader()embedded::reader()
Capture with a native queryreader.snapshot(query)reader.snapshot(query)reader.snapshot(&query)
Capture canonical JSONreader.snapshot_json(query)reader.snapshotJson(query)reader.snapshot_json(query_json)
Inspect budgets, occupancy, and logging coveragereader.info()reader.info()reader.info()
Release a reader or pageRelease Python referencesRelease JavaScript referencesDrop 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. 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.

startup.py
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}

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: 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.

paginate.py
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")

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.

handle_error.py
from nemo_relay import diagnostics
reader = diagnostics.reader()
try:
reader.snapshot({"unknown": True})
except diagnostics.DiagnosticsError as error:
if error.reason != "invalid_query":
raise

Complete runnable Python, Node.js, and Rust examples are maintained in the embedded diagnostics examples. 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.

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:

ResourceBound
Encoded queryUp to 4,096 UTF-8 bytes. Duplicate or unknown fields are rejected, including nested selectors.
Simultaneous snapshot/memory copies and conversionsTwo per service, shared by all reader clones
Payload per copyConfigured core max_snapshot_bytes
Default copy reservation262,144 bytes: two 131,072-byte pages
Waiting queueThere 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 provides entry and component backing attribution and existing resource-sample context.