nemo_gym.telemetry.observer
nemo_gym.telemetry.observer
Event-loop, process, request, and rollout-driver metrics that show what is slowing a Gym process down.
A Gym server answers every request on one asyncio event loop. When synchronous work such as validation, serialization, or garbage collection holds that loop, every other request waits, even though no single request looks slow. These metrics make that pressure visible without inspecting private asyncio internals:
gym.event_loop.lag_ms(histogram, milliseconds): how late a sleeping coroutine wakes up. A coroutine asks to wake after a fixed interval, and the lag is how much later than that it actually runs. This is the scheduler-pressure signal.gym.event_loop.tasks(gauge, opt-in): how many asyncio tasks exist on the loop. This is resident work, not a queue depth: an idle task waiting on the network counts as much as a runnable one.gym.gc.pause_duration_ms(histogram, milliseconds), and thegym.gc.collections_total,gym.gc.collected_total, andgym.gc.uncollectable_totalcounters, all bynemo.gym.gc.generation.gym.gc.count(gauge, bynemo.gym.gc.generation) is the allocation count fromgc.get_count().process.cpu.time(counter, seconds, bycpu.mode),process.memory.usage,process.thread.count, andprocess.open_file_descriptor.count. These keep the OpenTelemetry process semantic-convention names, units, and attributes, so standard dashboards recognize them.gym.server.requests.in_flight,gym.server.requests.unhandled_exceptions_total, andgym.server.responses_totalbyhttp.response.status_code.gym.server.requests.cancelled_totalcarrieshttp.routeandnemo.gym.request.phase. The outermost server middleware keeps these totals locally, and the exporter thread reads them.gym.http.client.requests.in_flight: outbound calls throughserver_utils.requeststill waiting for their response headers.gym.driver.rollouts.offered_total,gym.driver.rollouts.resident, andgym.driver.rollouts.active, plus thegym.driver.admission_duration_ms,gym.driver.output_write_duration_ms,gym.driver.output_write_bytes, andgym.driver.materialize_duration_mshistograms, from the rollout driver.gym.driver.rollout.input_items,gym.driver.rollout.output_items,gym.driver.rollout.turns, andgym.driver.rollout.tool_calls: the size of each completed rollout, recorded once per rollout by the driver.
Each kind of work runs where it costs the least:
- The event loop runs one sampler coroutine per interval, which measures lag, and counts tasks when that is on.
- The exporter’s background thread reads process CPU, memory, threads, file descriptors, and GC totals when it exports, through observable instruments.
- The garbage collector’s callback only stores timestamps and totals. It never calls OpenTelemetry, because it runs inside a collection. Pause durations wait in a bounded buffer until the next sample records them.
Process-wide state, which is the GC callback and the observable instruments, is set up once per process.
The last event loop to stop removes it.
These metrics are on whenever telemetry metrics are, because each costs at most a few microseconds per request or rollout.
The one exception is the event-loop task count, which walks every task on the loop and is opt-in through telemetry.event_loop_task_count.
Every failure is logged and swallowed, because telemetry must never stop a server.
Module Contents
Classes
Functions
Data
DRIVER_WRITE_DURATION_INSTRUMENT
PROCESS_MEMORY_USAGE_INSTRUMENT
PROCESS_THREAD_COUNT_INSTRUMENT
REQUESTS_UNHANDLED_EXCEPTIONS_INSTRUMENT
ROLLOUT_INPUT_ITEMS_INSTRUMENT
ROLLOUT_OUTPUT_ITEMS_INSTRUMENT
API
Rollout totals that the driver updates with plain integer arithmetic, read by the exporter thread.
Rollouts admitted past the concurrency limit and running.
Rollouts handed to dispatch.
Rollout tasks that exist and have not finished, whether waiting for admission or running.
Wraps the driver’s concurrency limit to record each rollout’s wait and count the rollouts running inside it.
Request totals that the server middleware and server_utils.request update with plain integer arithmetic.
The exporter thread reads them through observable instruments, so a request costs no OpenTelemetry call.
The serving event loop updates the inbound totals.
outbound_in_flight can also change on another thread’s event loop, such as the one the synchronous sandbox API runs on.
Each update is a single += on an attribute with no call in between, which CPython with the GIL never interrupts with another thread, so no update is lost.
Cumulative cancellations by registered route template and response-start phase.
Responses by HTTP status code, updated only by the serving event loop. HTTP bounds the set of codes.
Count one disconnect; route must be a registered template or the unmatched sentinel.
Collects GC pause times and totals from gc.callbacks without calling OpenTelemetry.
It takes no lock.
A lock would deadlock when a collection starts on a thread that already holds it, for example while drain_pauses allocates.
Python runs one collection at a time, so the callback never races itself, and the buffer swap in drain_pauses is safe under the GIL.
The observer’s instruments for one meter, created once so a restarted observer never registers them twice.
The per-process state of the observer: the GC recorder, the request totals, and this process’s handle.
Report a driver total only in a process that has offered rollouts, so servers export no constant-zero series.
Wrap a reading of the running observer as an observable-instrument callback.
It reports nothing while no observer runs, and one unavailable measurement never stops the others from exporting.
Await one rollout, counting it as resident until it finishes.
Return the process’s observer state while an observer is running, for the rollout driver, else None.
Run observe_event_loop for the lifetime of a FastAPI app, keeping any lifespan the app already has.
Sample the running event loop, and observe this process, for as long as the context is open.
loop_name is a bounded label for the loop, such as server, head_server, or driver.
Does nothing unless telemetry metrics are exporting.
Return the sample interval and whether to count tasks when metrics are exporting in this process, else None.
Return the request totals while an observer is running, else None.
Server middleware calls this once per request, so it is a plain global read.