Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions examples/basic_observe.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,7 @@
)
print(f"call #{i + 1}: {resp.choices[0].message.content!r}")

# 4. Optional: print a coverage snapshot from the runtime instance.
# The same counters are sent over the WS heartbeat and via the
# HTTP-fallback path when the WS connection is down.
print("\nCoverage snapshot:")
rt = nullrun.get_runtime()
report = rt.coverage_report()
for k, v in report.items():
print(f" {k}: {v}")
# 4. 0.9.0: per-process coverage snapshot removed. Coverage is now
# derived server-side from llm_call span metadata (host + tracked +
# streaming_skipped flags). Query the dashboard or use
# `GET /api/v1/coverage/{org_id}` to inspect.
55 changes: 50 additions & 5 deletions src/nullrun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@ def my_agent(query):
from nullrun.runtime import track_event, track_llm, track_tool


def shutdown(timeout: float = 2.0) -> None:
"""Gracefully shut down the NullRun runtime.

Sends a clean WebSocket close frame, drains in-flight events, and
stops background threads (HTTP poller, WS push listener). After
this returns, any further ``nullrun.track(...)`` call or
``@protect``-decorated call is a no-op.

Audit 2026-06-29 (WS graceful close on exit): a long-running
script that exits via ``sys.exit()`` lets the kernel RST the TCP
socket, which the backend logs as WARN "Connection reset
without closing handshake". Calling ``nullrun.shutdown()``
before exit (or registering it via ``atexit``) eliminates the
noisy log. No-op if ``init()`` was never called.

Args:
timeout: seconds to wait for the WS close handshake to
complete before giving up. The underlying
``NullRunRuntime.shutdown()`` already caps WS join at
0.5s and the WS close at 2.0s — this parameter is
reserved for future expansion and is currently unused.

Example::

import atexit
import nullrun
atexit.register(nullrun.shutdown)
"""
# Lazy import so the SDK module-import path stays light (mirrors
# the pattern in `init` and `status`).
from nullrun.runtime import NullRunRuntime
runtime = NullRunRuntime._instance # type: ignore[attr-defined]
if runtime is None:
return
runtime.shutdown()


def status():
"""Return the current runtime state as a Layer-3
:class:`NullRunStatus` snapshot.
Expand Down Expand Up @@ -300,11 +337,10 @@ def my_agent():

auto_instrument(runtime)

# Start the coverage reporter so the backend gets a coverage_report
# event every 60s. Daemon thread; safe to leak across re-init.
# The coverage reporter is a no-op when no LLM traffic has been
# observed (see ``track_coverage``).
runtime.start_coverage_reporter()
# 0.9.0: coverage reporter removed. Coverage is now derived
# server-side from llm_call span metadata (host + tracked +
# streaming_skipped flags). No 60s daemon thread, no per-process
# counter dicts.

return runtime

Expand Down Expand Up @@ -448,6 +484,15 @@ def __dir__() -> list[str]:
"track_llm",
"track_tool",
"track_event",
# Audit 2026-06-29 (WS graceful close on exit): the user-facing
# top-level ``shutdown()`` sends a clean WS close frame and
# drains in-flight events. Without it, a long-running script
# that exits via ``sys.exit()`` lets the kernel RST the TCP
# socket → backend logs WARN "Connection reset without closing
# handshake". Calling ``nullrun.shutdown()`` before
# ``sys.exit(0)`` (or in an ``atexit`` handler) eliminates the
# noisy log. No-op if init() was never called.
"shutdown",
# Layer 2: global on_error hook. Eager because it is the
# single most important "give the user a chance" API — the
# user has to know it exists to call it.
Expand Down
Loading
Loading