diff --git a/src/rius/client.py b/src/rius/client.py index e794866..d8489d7 100644 --- a/src/rius/client.py +++ b/src/rius/client.py @@ -166,7 +166,7 @@ def init( are only enabled when ``instruments`` is passed explicitly. span_exporter: Override the default OTLP exporter (useful for testing). heartbeat: Enable the agent-lifetime heartbeat thread - (``RIUS_HEARTBEAT``; default off this release). Pings + (``RIUS_HEARTBEAT``; on by default, set False to opt out). Pings ``/v1/heartbeat`` from init until process exit so the platform can tell a live-but-idle agent from a vanished one. heartbeat_interval: Seconds between pings (default 15, clamped to diff --git a/src/rius/config.py b/src/rius/config.py index a8437fe..6203b99 100644 --- a/src/rius/config.py +++ b/src/rius/config.py @@ -98,7 +98,7 @@ class GlassflowConfig: disabled: bool = False sample_rate: float = 1.0 capture_content: bool = True - heartbeat: bool = False + heartbeat: bool = True heartbeat_interval: float = DEFAULT_HEARTBEAT_INTERVAL agent_name: str = DEFAULT_SERVICE_NAME partial_spans: bool = False @@ -193,7 +193,8 @@ def resolve_config( capture_content: When ``False``, content attributes are stripped at export (``RIUS_CAPTURE_CONTENT``). heartbeat: Enable the agent-lifetime heartbeat thread - (``RIUS_HEARTBEAT``). Off by default this release. + (``RIUS_HEARTBEAT``). On by default; set ``False`` or + ``RIUS_HEARTBEAT=false`` to opt out. heartbeat_interval: Seconds between pings (``RIUS_HEARTBEAT_INTERVAL``), clamped to ``[5, 300]``; the backend derives staleness from this, so the bounds are part @@ -235,7 +236,7 @@ def resolve_config( ) resolved_heartbeat = ( - _env_bool(ENV_HEARTBEAT, deprecated_used, default=False) if heartbeat is None else heartbeat + _env_bool(ENV_HEARTBEAT, deprecated_used, default=True) if heartbeat is None else heartbeat ) resolved_heartbeat_interval = _clamp_heartbeat_interval( _env_float(ENV_HEARTBEAT_INTERVAL, deprecated_used, default=DEFAULT_HEARTBEAT_INTERVAL) diff --git a/tests/conftest.py b/tests/conftest.py index a2d8f1f..0e6636c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,17 @@ def exported_spans() -> InMemorySpanExporter: return _EXPORTER +@pytest.fixture(autouse=True) +def _no_ambient_heartbeat(monkeypatch: pytest.MonkeyPatch) -> None: + """Force heartbeat off for every test. + + Heartbeat is on by default, and its default transport does real HTTP; + a unit suite must never touch the network. Tests exercising heartbeat + behavior delete this variable and inject a stub transport. + """ + monkeypatch.setenv("RIUS_HEARTBEAT", "false") + + @pytest.fixture(autouse=True) def _reset_rius_lifecycle() -> "Iterator[None]": """Clear module-level init()/instrumentation state between tests.""" diff --git a/tests/test_heartbeat.py b/tests/test_heartbeat.py index 596b489..2f65e84 100644 --- a/tests/test_heartbeat.py +++ b/tests/test_heartbeat.py @@ -2,7 +2,7 @@ Implements the heartbeat spec: payload v1, emission semantics (init-to-exit lifetime, immediate first ping, stopped ping on graceful shutdown, silent -failure), and the config surface (off by default, interval clamped [5, 300], +failure), and the config surface (on by default, interval clamped [5, 300], agent_name defaults to service_name). """ @@ -28,12 +28,21 @@ # --------------------------------------------------------------------------- -def test_heartbeat_disabled_by_default() -> None: +def test_heartbeat_enabled_by_default(monkeypatch: pytest.MonkeyPatch) -> None: + # The conftest guard forces heartbeat off suite-wide (network safety); + # clear it here to observe the real default. + monkeypatch.delenv("RIUS_HEARTBEAT", raising=False) + monkeypatch.delenv("GLASSFLOW_HEARTBEAT", raising=False) + assert resolve_config().heartbeat is True + + +def test_heartbeat_opt_out_via_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("RIUS_HEARTBEAT", "false") assert resolve_config().heartbeat is False def test_heartbeat_enabled_via_env(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("GLASSFLOW_HEARTBEAT", "true") + monkeypatch.setenv("RIUS_HEARTBEAT", "true") assert resolve_config().heartbeat is True @@ -250,8 +259,27 @@ def test_open_traces_flow_into_payloads() -> None: client.shutdown() -def test_heartbeat_off_by_default_no_thread() -> None: - client = init(set_global=False, service_name="svc", span_exporter=InMemorySpanExporter()) +def test_heartbeat_on_by_default_starts_thread(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("RIUS_HEARTBEAT", raising=False) + monkeypatch.delenv("GLASSFLOW_HEARTBEAT", raising=False) + sent: list[dict[str, Any]] = [] + client = init( + set_global=False, + service_name="svc", + span_exporter=InMemorySpanExporter(), + heartbeat_transport=sent.append, + ) + assert client._heartbeat is not None # noqa: SLF001 + client.shutdown() + + +def test_heartbeat_opt_out_starts_no_thread() -> None: + client = init( + set_global=False, + service_name="svc", + heartbeat=False, + span_exporter=InMemorySpanExporter(), + ) assert client._heartbeat is None # noqa: SLF001 client.shutdown() @@ -365,9 +393,9 @@ def log_message(self, *args: Any) -> None: try: env = { **os.environ, - "GLASSFLOW_ENDPOINT": f"http://127.0.0.1:{server.server_port}", - "GLASSFLOW_HEARTBEAT": "1", - "GLASSFLOW_SERVICE_NAME": "atexit-agent", + "RIUS_ENDPOINT": f"http://127.0.0.1:{server.server_port}", + "RIUS_HEARTBEAT": "1", # overrides the conftest guard in os.environ + "RIUS_SERVICE_NAME": "atexit-agent", } # init() then exit normally WITHOUT calling shutdown(): the atexit # hook alone must produce the stopped ping.