diff --git a/README.md b/README.md index 6a36e7b94..b117b149c 100644 --- a/README.md +++ b/README.md @@ -188,3 +188,7 @@ Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contr + +## Optional telemetry + +Telemetry is enabled by default. To opt out, set `space.disable_telemetry: true` in your instance’s `data/config.yaml`, preserve other settings, and restart the instance. This disables usage, heartbeat and execution telemetry. In Cloud, the instance operator controls this setting for every workspace in the instance. Delivery is best-effort in background tasks; connection failures, timeouts and server errors do not block normal operations. Failure details are logged only at DEBUG level. diff --git a/README_CN.md b/README_CN.md index 33b92645e..b2c4b1604 100644 --- a/README_CN.md +++ b/README_CN.md @@ -212,3 +212,7 @@ LangBot **从设计上就对 Agent 友好** —— 你的编码 Agent(Claude C If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development. --> + +## 可选遥测 + +遥测默认开启。可以在实例 `data/config.yaml` 中将 `space.disable_telemetry` 改为 `true`,保留其他配置,然后重启实例。此开关关闭使用统计、实例心跳和功能执行遥测;Cloud 由实例运维方配置,对该实例所有工作区生效。遥测在后台尽力发送,网络故障、超时或服务端错误不会阻断正常操作;失败详情仅记录在 DEBUG 日志中。 diff --git a/src/langbot/pkg/telemetry/telemetry.py b/src/langbot/pkg/telemetry/telemetry.py index e4aaf6fd8..c36b35e3c 100644 --- a/src/langbot/pkg/telemetry/telemetry.py +++ b/src/langbot/pkg/telemetry/telemetry.py @@ -170,9 +170,7 @@ class TelemetryManager: if resp.status_code >= 400: body = await httpclient.response_text(resp, max_chars=200) - self.ap.logger.warning( - f'Telemetry post to {url} returned status {resp.status_code} - {body}' - ) + self.ap.logger.debug(f'Telemetry post to {url} returned status {resp.status_code} - {body}') else: # Detect application-level errors inside HTTP 200 responses app_err = False @@ -181,7 +179,7 @@ class TelemetryManager: app_code = j.get('code') if isinstance(j, dict) else None if app_code is not None and int(app_code) >= 400: app_err = True - self.ap.logger.warning( + self.ap.logger.debug( f'Telemetry post to {url} returned application error code {j.get("code")} - {j.get("msg")}' ) except Exception: @@ -189,7 +187,7 @@ class TelemetryManager: if app_err: body = await httpclient.response_text(resp, max_chars=200) - self.ap.logger.warning( + self.ap.logger.debug( f'Telemetry post to {url} returned app-level error - response: {body}' ) else: @@ -200,19 +198,19 @@ class TelemetryManager: if not app_err: return True except asyncio.TimeoutError: - self.ap.logger.warning(f'Telemetry post to {url} timed out') + self.ap.logger.debug(f'Telemetry post to {url} timed out') except Exception as e: - self.ap.logger.warning(f'Failed to post telemetry to {url}: {e}', exc_info=True) + self.ap.logger.debug(f'Failed to post telemetry to {url}: {e}', exc_info=True) except Exception as e: try: - self.ap.logger.warning( + self.ap.logger.debug( f'Failed to create HTTP client for telemetry or sanitize payload: {e}', exc_info=True ) except Exception: pass except Exception as e: - # Never raise from telemetry; surface as warning for visibility + # Never raise from telemetry; diagnostics are debug-only. try: - self.ap.logger.warning(f'Unexpected telemetry error: {e}', exc_info=True) + self.ap.logger.debug(f'Unexpected telemetry error: {e}', exc_info=True) except Exception: pass diff --git a/src/langbot/templates/config.yaml b/src/langbot/templates/config.yaml index 88939477a..edd64149f 100644 --- a/src/langbot/templates/config.yaml +++ b/src/langbot/templates/config.yaml @@ -426,5 +426,6 @@ space: # OAuth authorization page URL (user will be redirected here) oauth_authorize_url: 'https://space.langbot.app/auth/authorize' disable_models_service: false - # Disable usage telemetry. + # Optional telemetry is enabled by default. Set true to disable usage, + # heartbeat and execution reporting; restart after changing this setting. disable_telemetry: false diff --git a/tests/unit_tests/telemetry/test_telemetry.py b/tests/unit_tests/telemetry/test_telemetry.py index 291264f61..004009a68 100644 --- a/tests/unit_tests/telemetry/test_telemetry.py +++ b/tests/unit_tests/telemetry/test_telemetry.py @@ -431,6 +431,7 @@ class TestHTTPScenarios: with patch.object(httpx, 'AsyncClient', return_value=mock_client): await manager.send({'query_id': 'test'}) + mock_app.logger.warning.assert_not_called() mock_app.logger.debug.assert_called() # Verify debug message contains URL and status debug_call_args = mock_app.logger.debug.call_args[0][0] @@ -459,8 +460,9 @@ class TestHTTPScenarios: with patch.object(httpx, 'AsyncClient', return_value=mock_client): await manager.send({'query_id': 'test'}) - mock_app.logger.warning.assert_called() - warning_call_args = mock_app.logger.warning.call_args[0][0] + mock_app.logger.warning.assert_not_called() + mock_app.logger.debug.assert_called() + warning_call_args = mock_app.logger.debug.call_args[0][0] assert 'status 500' in warning_call_args @pytest.mark.asyncio @@ -488,9 +490,9 @@ class TestHTTPScenarios: await manager.send({'query_id': 'test'}) # Source code calls warning twice for application errors - assert mock_app.logger.warning.call_count >= 1 + assert mock_app.logger.debug.call_count >= 1 # Check that one of the calls contains application error info - all_warnings = [call[0][0] for call in mock_app.logger.warning.call_args_list] + all_warnings = [call[0][0] for call in mock_app.logger.debug.call_args_list] assert any('400' in w for w in all_warnings), f'No warning contained error code 400: {all_warnings}' @pytest.mark.asyncio @@ -516,8 +518,9 @@ class TestHTTPScenarios: with patch.object(httpx, 'AsyncClient', return_value=mock_client): await manager.send({'query_id': 'test'}) - mock_app.logger.warning.assert_called() - warning_call_args = mock_app.logger.warning.call_args[0][0] + mock_app.logger.warning.assert_not_called() + mock_app.logger.debug.assert_called() + warning_call_args = mock_app.logger.debug.call_args[0][0] assert 'timed out' in warning_call_args @pytest.mark.asyncio @@ -542,7 +545,8 @@ class TestHTTPScenarios: # Should not raise exception await manager.send({'query_id': 'test'}) - mock_app.logger.warning.assert_called() + mock_app.logger.warning.assert_not_called() + mock_app.logger.debug.assert_called() @pytest.mark.asyncio async def test_send_never_raises_exception(self): @@ -551,7 +555,7 @@ class TestHTTPScenarios: mock_app = Mock() # Even logger may fail mock_app.logger = Mock() - mock_app.logger.warning = Mock(side_effect=Exception('Logger failed')) + mock_app.logger.debug = Mock(side_effect=Exception('Logger failed')) manager = telemetry.TelemetryManager(mock_app) manager.telemetry_config = {'url': 'https://example.com'}