Operational Logging
Operational logging records diagnostics about the Relay process, including startup, configuration, plugins, gateway behavior, and runtime failures. It is separate from agent observability through ATOF, ATIF, OpenTelemetry, or OpenInference.
Relay writes operational logs to stderr by default. Set stderr_enabled to
false to disable that sink; optional file sinks continue receiving records.
Each record includes a root Relay ID for correlation.
MCP failure records include error_kind. The mcp_session_failed record and
the matching command_failed record for command = "mcp" also include
failure_reason. This field is a short, fixed code that names the MCP step that
failed. It never includes the source error text. Use the shared root Relay ID to
match the two records. Read stderr for the full error message.
Gateway startup failures use the same pattern. gateway_acquisition_failed and
gateway_recovery_failed include failure_reason as well as the existing
failure_kind. For example, gateway_recovered_then_unhealthy means the
gateway became unhealthy after Relay had already tried to recover it once.
Daemon worker activation failures include failure_reason on both
worker_launch_failed and the matching worker_activation_failed record. The
code shows whether Relay could not find or start the worker, send its activation
permission, or wait for it to become ready. It never includes activation data or the
source error text.
CLI Hook and Upstream Records
At debug level, the CLI writes lifecycle records for hooks and for gateway calls
to an upstream LLM. This includes local hooks, persistent hooks, daemon hooks, and
managed-worker hooks. A hook forwarder and the Relay handler use the same random
operation_id. Each gateway call gets a new ID. If Relay selects a session, the
record also includes session_id. This is a stable, derived tag, not the native
session ID.
Normal lifecycle records are hook_started, hook_completed,
upstream_started, upstream_headers_received, upstream_first_event,
upstream_completed, and upstream_cancelled. They carry only operation_id,
optional session_id, boundary, outcome, and elapsed_millis.
Warnings show slow or failed external work. They do not change request behavior:
upstream_headers_delayedafter 10 seconds without upstream response headers.upstream_first_event_delayedafter 10 seconds without a decoded stream event.upstream_stream_stalledevery 60 seconds while no decoded stream event arrives. Network heartbeats do not reset these timers.upstream_non_successwhen the upstream HTTP status is not a success.upstream_stream_read_failedwhen Relay can recover from a stream read failure.hook_failedfor a hook that is allowed to fail open.
These timing records only report what happened. They do not cancel a request or a
stream. Local gateway calls still use their 30-second connect limit and 300-second
idle-read limit. error records cover rejected payload and hook-response limits, hooks that
must fail closed, upstream transport failures, and Relay adapter, session, or
runtime failures. These records use safe fields such as error_kind, status_code,
threshold_millis, and elapsed_millis. A limit_exceeded record also includes
limit_name and the configured limit_bytes.
These records never include tool names or call IDs, hook event names, arguments, results, prompts, model names, provider names, route names, upstream URLs, headers, bodies, body sizes, or credentials. Relay removes its internal correlation header when it receives the request and never sends it upstream.
Defaults
Without configuration, Relay uses:
erroras the minimum log level- Human-readable stderr output
- No file sinks
Choose a Configuration Source
Use the source that matches how Relay is launched:
For CLI processes, Relay selects one source in this order:
--log-*options or--log-config-pathNEMO_RELAY_LOG*environment variables[logging]in the resolved Relayconfig.toml- Built-in defaults
Sources are selected rather than merged. Python, Node.js, and Go install a
process-lifetime LoggingRuntime when the binding loads, using environment
configuration or built-in defaults. Rust applications explicitly choose which
LoggingRuntime initialization method to use and do not apply the CLI
precedence rules.
CLI Options
Configure the minimum level and stderr format directly:
Use an absolute TOML path when file sinks or other logging settings are needed:
Do not combine --log-config-path with --log-level or
--log-stderr-format.
Environment Variables
Set these variables for a Python, Node.js, or Go process, the CLI, or a Rust
application that initializes logging with
LoggingRuntime::configure_from_environment():
Supported values are:
NEMO_RELAY_LOG:error,warn,info,debug, ortraceNEMO_RELAY_LOG_STDERR:true(default) orfalseNEMO_RELAY_LOG_STDERR_FORMAT:humanorjsonl
Alternatively, select an absolute TOML file:
NEMO_RELAY_LOG_CONFIG_PATH cannot be combined with the other logging
environment variables.
When none of these variables are set, Python, Node.js, and Go install Relay’s
built-in default logger unless the host already owns Rust’s process-global
log facade. They also preserve an existing Relay logger rather than replacing
it. Records emitted before any logger is installed are discarded, and a host
cannot install its own logger after Relay has claimed the facade. Set one of
these variables when Relay must configure its own logging.
This binding behavior differs from
LoggingRuntime::configure_from_environment(), which always attempts to
install Relay’s built-in defaults when no variables are set.
Python and Node.js drain pending file-sink records during normal runtime
teardown. Go applications that configure file sinks must call
nemo_relay.ShutdownLogging before main returns; defer it near the start of
main so it runs after other Relay cleanup.
TOML Configuration
Logging settings use a [logging] table:
File sink paths are resolved relative to the process working directory. File
sinks use asynchronous queues, and queue_capacity cannot exceed 8,192 entries
per sink. Size-based rotation is optional; when enabled,
max_file_size_bytes and retained_files must be configured together.
retained_files counts backup files in addition to the active file and cannot
exceed 9. A record larger than max_file_size_bytes remains intact rather
than being split. File sinks remain append-only when rotation settings are
omitted.
When Relay layers config.toml files, the resolved destination path is the
file sink identity. Distinct paths are emitted in highest-to-lowest precedence
order: system, then explicit-or-user. For matching paths, higher-layer
fields recursively overlay lower-layer fields, producing one effective sink.
Path aliases such as relay.log and ./relay.log resolve to one sink, using
the higher-layer spelling and settings.
Rust Library API
Initialize operational logging once during application startup by choosing one of these public APIs:
LoggingRuntime::configure(config)for a constructedLoggingConfigLoggingRuntime::configure_from_environment()for environment configurationLoggingRuntime::configure_from_file_path(path)for an absolute TOML path
For example:
Keep the returned runtime alive until application shutdown so pending file records can be flushed.