refactor: remove beta diagnostics collection and reporting (#2616)

This commit is contained in:
RockChinQ
2026-09-30 20:24:35 +08:00
committed by GitHub
parent 5f3eb34baf
commit 30585c8657
82 changed files with 72 additions and 4934 deletions
+1 -12
View File
@@ -28,7 +28,6 @@ from ..authz import (
)
from ..context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
from ....cloud.support_admin import SupportAdminSessionError
from ... import management_diagnostics as diagnostics
if typing.TYPE_CHECKING:
from ....core.app import Application
@@ -238,7 +237,6 @@ class RouterGroup(abc.ABC):
observed_at = time.monotonic()
try:
if request_context is not None:
diagnostics.workspace(request_context)
with bounded_executor.blocking_work_scope(request_context.workspace_uuid):
persistence_mgr = getattr(
self.ap,
@@ -321,14 +319,7 @@ class RouterGroup(abc.ABC):
)
return self.internal_error_response(request_id)
# Observe outside authentication, using the registered Core handler
# identity rather than the URL (which can contain user identifiers).
new_f = diagnostics.observe(
diagnostics.operation_id('http', f, rule=rule, methods=options.get('methods')),
source='http',
ap=self.ap,
http=True,
)(handler_error)
new_f = handler_error
# Quart/Flask requires a unique endpoint name even when the same URL
# intentionally has separate handlers for different HTTP methods.
# Include the method set so CRUD routes can declare distinct
@@ -735,8 +726,6 @@ class RouterGroup(abc.ABC):
def fail(self, code: int | str, msg: str) -> quart.Response:
"""Return an error response"""
diagnostics.outcome('failed')
return quart.jsonify(
{
'code': code,
@@ -7,7 +7,6 @@ import contextlib
import json
import quart
from .... import management_diagnostics as diagnostics
from .....agent.runner.errors import (
RunnerError,
@@ -43,9 +42,6 @@ def debug_stream_response(service, context, agent_uuid: str, payload: dict) -> q
)
await queue.put({'kind': 'completed', 'data': result})
except Exception as exc:
# The stream still uses HTTP 200 when execution returns an
# error frame. Mark the inherited request, not its contents.
diagnostics.outcome('failed')
if isinstance(exc, RunnerExecutionError):
code, message = exc.error_code or 'runner_execution_failed', exc.message
elif isinstance(exc, RunnerNotFoundError):
@@ -21,7 +21,6 @@ import httpx
import quart
from ... import group
from ..... import management_diagnostics as diagnostics
from ......utils import httpclient, paths
from ......platform.sources.websocket_manager import WebSocketScope, is_valid_session_id, ws_connection_manager
from .websocket_chat import create_scoped_duplex_tasks, wait_for_duplex_tasks
@@ -330,24 +329,20 @@ class EmbedRouterGroup(group.RouterGroup):
# -- Embed WebSocket endpoint ----------------------------------------
@self.quart_app.websocket(self.path + '/<bot_uuid>/ws/connect')
@diagnostics.observe('websocket.embed.session', source='websocket', ap=self.ap)
async def embed_websocket_connect(bot_uuid: str):
"""WebSocket connection for embed widget, keyed by bot_uuid."""
await quart.websocket.accept()
if not _is_valid_uuid(bot_uuid):
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Invalid bot_uuid format'}))
diagnostics.outcome('rejected')
return
runtime_bot, pipeline_uuid = await self._resolve_bot(bot_uuid)
if runtime_bot is None:
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Bot not found or not available'}))
diagnostics.outcome('rejected')
return
session_type = quart.websocket.args.get('session_type', 'person')
if session_type not in ['person', 'group']:
diagnostics.outcome('rejected')
await quart.websocket.send(
json.dumps({'type': 'error', 'message': 'session_type must be person or group'})
)
@@ -356,7 +351,6 @@ class EmbedRouterGroup(group.RouterGroup):
session_id = quart.websocket.args.get('session_id', '')
if not is_valid_session_id(session_id):
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Valid session_id is required'}))
diagnostics.outcome('rejected')
return
try:
@@ -364,16 +358,12 @@ class EmbedRouterGroup(group.RouterGroup):
await self._assert_execution_active(runtime_bot)
except Exception:
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Unauthorized'}))
diagnostics.outcome('rejected')
return
diagnostics.workspace(runtime_bot.execution_context)
try:
proxy_bot = await self.ap.platform_mgr.get_websocket_proxy_bot(runtime_bot.execution_context)
websocket_adapter = proxy_bot.adapter
if not websocket_adapter:
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'WebSocket adapter not found'}))
diagnostics.outcome('rejected')
return
connection = await ws_connection_manager.add_connection(
@@ -431,13 +421,11 @@ class EmbedRouterGroup(group.RouterGroup):
try:
await wait_for_duplex_tasks(receive_task, send_task)
except Exception as e:
diagnostics.outcome('failed')
logger.error(f'Embed WebSocket task error: {e}')
finally:
await ws_connection_manager.remove_connection(connection.connection_id)
except Exception as e:
diagnostics.outcome('failed')
logger.error(f'Embed WebSocket connection error: {e}', exc_info=True)
try:
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Internal server error'}))
@@ -451,36 +439,30 @@ class EmbedRouterGroup(group.RouterGroup):
while connection.is_active:
message = await quart.websocket.receive()
await ws_connection_manager.update_activity(connection.connection_id)
try:
data = await asyncio.to_thread(json.loads, message)
message_type = data.get('type', 'message')
with diagnostics.scope(self.ap, 'websocket.embed.message', source='websocket'):
diagnostics.workspace(getattr(owner_bot, 'execution_context', None))
try:
data = await asyncio.to_thread(json.loads, message)
message_type = data.get('type', 'message')
if message_type == 'ping':
await connection.send_queue.put(
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
)
elif message_type == 'message':
try:
current_bot = await self._resolve_connected_bot(owner_bot, pipeline_uuid)
except Exception:
diagnostics.outcome('rejected')
await connection.send_queue.put({'type': 'error', 'message': 'Bot is unavailable'})
break
await websocket_adapter.handle_websocket_message(connection, data, owner_bot=current_bot)
elif message_type == 'disconnect':
if message_type == 'ping':
await connection.send_queue.put(
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
)
elif message_type == 'message':
try:
current_bot = await self._resolve_connected_bot(owner_bot, pipeline_uuid)
except Exception:
await connection.send_queue.put({'type': 'error', 'message': 'Bot is unavailable'})
break
else:
diagnostics.outcome('skipped')
await websocket_adapter.handle_websocket_message(connection, data, owner_bot=current_bot)
elif message_type == 'disconnect':
break
else:
pass
except json.JSONDecodeError:
diagnostics.outcome('rejected')
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
except json.JSONDecodeError:
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
except Exception as e:
diagnostics.outcome('failed')
logger.error(f'Embed receive error: {e}', exc_info=True)
finally:
connection.is_active = False
@@ -496,13 +478,11 @@ class EmbedRouterGroup(group.RouterGroup):
message = await asyncio.wait_for(connection.send_queue.get(), timeout=1.0)
if message is None:
break
with diagnostics.scope(self.ap, 'websocket.embed.send', source='websocket'):
encoded = await asyncio.to_thread(json.dumps, message)
await quart.websocket.send(encoded)
encoded = await asyncio.to_thread(json.dumps, message)
await quart.websocket.send(encoded)
except asyncio.TimeoutError:
continue
except Exception as e:
diagnostics.outcome('failed')
logger.error(f'Embed send error: {e}', exc_info=True)
finally:
connection.is_active = False
@@ -14,7 +14,6 @@ import quart
from ....authz import Permission, permissions_for_role, require_permission
from ....context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
from ... import group
from ..... import management_diagnostics as diagnostics
from ......core.task_boundary import run_in_workspace_uow
from ......platform.sources.websocket_manager import WebSocketScope, ws_connection_manager
from ......utils import bounded_executor
@@ -211,7 +210,6 @@ class WebSocketChatRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@self.quart_app.websocket(self.path + '/connect')
@diagnostics.observe('websocket.dashboard.session', source='websocket', ap=self.ap)
async def websocket_connect(pipeline_uuid: str):
"""Open one authenticated dashboard debug connection."""
@@ -219,14 +217,10 @@ class WebSocketChatRouterGroup(group.RouterGroup):
try:
request_context, token = await self._authenticate_websocket()
except Exception:
diagnostics.outcome('rejected')
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Unauthorized'}))
return
diagnostics.workspace(request_context)
session_type = quart.websocket.args.get('session_type', 'person')
if session_type not in ['person', 'group']:
diagnostics.outcome('rejected')
await quart.websocket.send(
json.dumps({'type': 'error', 'message': 'session_type must be person or group'})
)
@@ -235,7 +229,6 @@ class WebSocketChatRouterGroup(group.RouterGroup):
try:
websocket_adapter = await self._get_scoped_adapter(request_context, pipeline_uuid)
if websocket_adapter is None:
diagnostics.outcome('rejected')
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Pipeline not found'}))
return
@@ -294,13 +287,11 @@ class WebSocketChatRouterGroup(group.RouterGroup):
try:
await wait_for_duplex_tasks(receive_task, send_task)
except Exception as exc:
diagnostics.outcome('failed')
logger.error(f'WebSocket task execution error: {exc}')
finally:
await ws_connection_manager.remove_connection(connection.connection_id)
except Exception:
diagnostics.outcome('failed')
logger.error('Dashboard WebSocket connection error', exc_info=True)
try:
await quart.websocket.send(json.dumps({'type': 'error', 'message': 'Internal server error'}))
@@ -417,35 +408,28 @@ class WebSocketChatRouterGroup(group.RouterGroup):
while connection.is_active:
message = await quart.websocket.receive()
await ws_connection_manager.update_activity(connection.connection_id)
with diagnostics.scope(self.ap, 'websocket.dashboard.message', source='websocket'):
diagnostics.workspace(request_context)
try:
data = await asyncio.to_thread(json.loads, message)
message_type = data.get('type', 'message')
if message_type == 'ping':
await connection.send_queue.put(
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
)
elif message_type == 'message':
try:
request_context = await self._revalidate_websocket_authorization(request_context, token)
except Exception:
diagnostics.outcome('rejected')
await connection.send_queue.put({'type': 'error', 'message': 'Unauthorized'})
break
await websocket_adapter.handle_websocket_message(connection, data)
elif message_type == 'disconnect':
try:
data = await asyncio.to_thread(json.loads, message)
message_type = data.get('type', 'message')
if message_type == 'ping':
await connection.send_queue.put(
{'type': 'pong', 'timestamp': datetime.datetime.now().isoformat()}
)
elif message_type == 'message':
try:
request_context = await self._revalidate_websocket_authorization(request_context, token)
except Exception:
await connection.send_queue.put({'type': 'error', 'message': 'Unauthorized'})
break
else:
diagnostics.outcome('skipped')
logger.warning(f'Unknown WebSocket message type: {message_type}')
except json.JSONDecodeError:
diagnostics.outcome('rejected')
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
await websocket_adapter.handle_websocket_message(connection, data)
elif message_type == 'disconnect':
break
else:
logger.warning(f'Unknown WebSocket message type: {message_type}')
except json.JSONDecodeError:
await connection.send_queue.put({'type': 'error', 'message': 'Invalid JSON format'})
except Exception:
diagnostics.outcome('failed')
logger.error('Dashboard WebSocket receive error', exc_info=True)
finally:
connection.is_active = False
@@ -461,13 +445,11 @@ class WebSocketChatRouterGroup(group.RouterGroup):
message = await asyncio.wait_for(connection.send_queue.get(), timeout=1.0)
if message is None:
break
with diagnostics.scope(self.ap, 'websocket.dashboard.send', source='websocket'):
encoded = await asyncio.to_thread(json.dumps, message)
await quart.websocket.send(encoded)
encoded = await asyncio.to_thread(json.dumps, message)
await quart.websocket.send(encoded)
except asyncio.TimeoutError:
continue
except Exception:
diagnostics.outcome('failed')
logger.error('Dashboard WebSocket send error', exc_info=True)
finally:
connection.is_active = False
@@ -200,11 +200,14 @@ class SystemRouterGroup(group.RouterGroup):
if invitation_delivery_service is None:
invitation_delivery_service = InvitationDeliveryService(self.ap)
# Detect beta flag from telemetry diagnostics manager
is_beta = False
diagnostics = getattr(self.ap, 'diagnostics', None)
if diagnostics is not None:
is_beta = getattr(diagnostics, 'beta', False)
# Release channel is independent of diagnostics or usage telemetry.
from packaging.version import Version, InvalidVersion
try:
release = Version(constants.semantic_version)
is_beta = bool(release.pre and release.pre[0] == 'b' and not release.local)
except InvalidVersion:
is_beta = False
# Detect Cloud deployment mode
is_cloud = getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud'
@@ -6,7 +6,6 @@ import fnmatch
import time
import uuid
import typing
from ... import management_diagnostics as diagnostics
import sqlalchemy
from langbot_plugin.api.entities.builtin.runner.delivery import DeliveryContext
@@ -135,7 +134,6 @@ class AgentService:
return None
@diagnostics.observe('http.agent.debug_agent', source='webui_debug')
async def debug_agent(
self,
context: RequestContext,
@@ -150,7 +148,6 @@ class AgentService:
delivers outputs to a real platform, and supports both message and
non-message event envelopes.
"""
diagnostics.workspace(context)
agent = await self.get_agent(context, agent_uuid)
if agent is None or agent.get('kind') not in {AGENT_KIND_AGENT, AGENT_KIND_EVENT_PROCESSOR}:
raise ValueError('Agent not found')
@@ -1,225 +0,0 @@
"""Content-free management boundaries. Diagnostics never participate in I/O.
Only source-code identities and status categories enter these helpers. Never
pass a URL, request, frame, token, tool argument, or serialized result to them.
"""
from __future__ import annotations
import contextlib
import contextvars
import functools
import re
import quart
from quart.wrappers.response import IterableBody, ResponseBody
from ..telemetry import diagnostics as d
from ..telemetry import diagnostic_privacy as privacy
_ACTIVE_BOUNDARY = contextvars.ContextVar('management_diagnostic_boundary', default=None)
def operation_id(source, fn, *, rule='', methods=()):
"""Called at registration with a Core function, never a client tool name."""
module = fn.__module__.split('.groups.', 1)[-1]
if source != 'http':
module = ''
name = fn.__name__
if source == 'http' and name == '_':
# Many Core routes use the anonymous function name `_`. Disambiguate
# using ONLY the source-declared template and methods at registration.
# This is never quart.request.path, url_rule, endpoint or request.method.
template = re.sub(r'[^A-Za-z0-9_.:-]+', '.', rule).strip('.') or 'root'
name = '.'.join((*sorted(methods or ('GET',)), template))
return '.'.join(part for part in (source, module, name) if part)[:128]
def outcome(value, reason_code='response_error'):
try:
d.set_outcome(value, reason_code=reason_code)
except Exception:
pass
def workspace(context):
"""Annotate only this boundary's owned span with a matching trusted context."""
try:
from .http.context import ExecutionContext, RequestContext
if not isinstance(context, (ExecutionContext, RequestContext)):
return
boundary = _ACTIVE_BOUNDARY.get()
span = d.current_span()
if boundary is None or span is None or span is not boundary.span:
return
if d._manager(boundary.ap) is not span.manager:
return
instance = getattr(
getattr(boundary.ap, 'workspace_service', None),
'instance_uuid',
getattr(span.manager, 'instance_id', None),
)
# Structural recorders need not expose instance metadata. Real managers
# do, and must never receive another instance's Workspace annotation.
if instance is not None and instance != context.instance_uuid:
return
d.annotate(workspace_uuid=context.workspace_uuid)
except Exception:
pass
class Boundary:
def __init__(self, ap, operation, source):
self.ap = ap
self.span = None
try:
manager = d._manager(ap)
if manager is not None:
fields = {'source': source, 'stage': 'execute'}
if source == 'webui_debug':
fields['attributes'] = {'synthetic': True}
self.span = d.Span(manager, 'api', operation, fields)
except Exception:
pass
@contextlib.contextmanager
def activate(self):
# None is an explicit inheritance barrier, not a no-op: disabled or
# broken B work must not borrow enabled A's span through ContextVars.
token = d._CURRENT.set(self.span)
boundary_token = _ACTIVE_BOUNDARY.set(self)
try:
yield
finally:
_ACTIVE_BOUNDARY.reset(boundary_token)
d._CURRENT.reset(token)
def finish(self, error=None):
if self.span is not None:
try:
self.span.finish(error)
except Exception:
pass
class _ObservedBody(ResponseBody):
"""Carry the HTTP parent through iteration, not generator suspension."""
def __init__(self, body, boundary):
self.body = body
self.boundary = boundary
self.iterator = None
self.exhausted = False
async def __aenter__(self):
try:
with self.boundary.activate():
entered = await self.body.__aenter__()
self.iterator = entered.__aiter__()
return self
except BaseException as exc:
self.boundary.finish(exc)
raise
async def __aexit__(self, exc_type, exc_value, tb):
try:
with self.boundary.activate():
result = await self.body.__aexit__(exc_type, exc_value, tb)
except BaseException as exc:
self.boundary.finish(exc)
raise
finally:
self.boundary.finish(exc_value if exc_value is not None else (None if self.exhausted else GeneratorExit()))
return result
async def __aiter__(self):
try:
if self.iterator is None:
with self.boundary.activate():
self.iterator = self.body.__aiter__()
while True:
try:
with self.boundary.activate():
value = await anext(self.iterator)
except StopAsyncIteration:
self.exhausted = True
self.boundary.finish()
return
yield value
except BaseException as exc:
self.boundary.finish(exc)
raise
def _response_status(value):
"""Inspect response metadata only; never read or deserialize a body."""
response = value[0] if isinstance(value, tuple) else value
status = response.status_code if isinstance(response, quart.Response) else 200
if isinstance(value, tuple) and len(value) > 1 and type(value[1]) is int:
status = value[1]
if status in (401, 403):
outcome('rejected')
elif status >= 400:
outcome('failed')
elif isinstance(response, dict) and 'code' in response and response['code'] != 0:
outcome('failed')
return response
@contextlib.contextmanager
def scope(ap, operation, *, source):
"""A fixed operation around an existing non-generator statement block."""
try:
privacy.code_value('operation', operation)
except Exception:
pass
boundary = Boundary(ap, operation, source)
try:
with boundary.activate():
yield
except BaseException as exc:
boundary.finish(exc)
raise
finally:
boundary.finish()
def observe(operation, *, source, ap=None, http=False):
# The operation is supplied by Core registration code, not request data.
try:
privacy.code_value('operation', operation)
except Exception:
pass
def decorator(fn):
@functools.wraps(fn)
async def wrapped(*args, **kwargs):
try:
owner = (ap() if callable(ap) else ap) if ap is not None else getattr(args[0], 'ap', None)
except Exception:
owner = None
boundary = Boundary(owner, operation, source)
streaming = False
try:
with boundary.activate():
value = await fn(*args, **kwargs)
if http:
try:
response = _response_status(value)
if isinstance(response, quart.Response) and isinstance(response.response, IterableBody):
response.response = _ObservedBody(response.response, boundary)
streaming = True
except Exception:
pass
return value
except BaseException as exc:
boundary.finish(exc)
raise
finally:
if not streaming:
boundary.finish()
return wrapped
return decorator
-5
View File
@@ -24,7 +24,6 @@ import uuid
from ..http.context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
from .context import bind_request_context, reset_request_context
from .server import LangBotMCPServer
from .. import management_diagnostics as diagnostics
if typing.TYPE_CHECKING:
from ...core import app as app_module
@@ -86,7 +85,6 @@ class MCPMount:
authenticate_api_key = self.ap.apikey_service.authenticate_api_key
is_mcp_path = self._is_mcp_path
@diagnostics.observe('mcp.request', source='mcp', ap=self.ap)
async def dispatch_mcp(scope, receive, send):
# Authenticate MCP HTTP requests with a LangBot API key.
api_key = _extract_api_key(scope.get('headers', []))
@@ -96,7 +94,6 @@ class MCPMount:
identity = await authenticate_api_key(api_key)
if identity is None:
diagnostics.outcome('rejected')
await send(
{
'type': 'http.response.start',
@@ -123,7 +120,6 @@ class MCPMount:
entitlement = await resolver.resolve(identity.workspace_uuid)
entitlement_revision = entitlement.entitlement_revision
except Exception:
diagnostics.outcome('rejected')
await send(
{
'type': 'http.response.start',
@@ -151,7 +147,6 @@ class MCPMount:
),
entitlement_revision=entitlement_revision,
)
diagnostics.workspace(request_context)
tenant_scope = getattr(self.ap.persistence_mgr, 'tenant_scope', None)
if not callable(tenant_scope):
raise RuntimeError('MCP request persistence scope is unavailable')
+1 -3
View File
@@ -24,7 +24,6 @@ from mcp.server.fastmcp import FastMCP
from ..http.authz import Permission, require_permission
from .context import get_request_context
from .. import management_diagnostics as diagnostics
if typing.TYPE_CHECKING:
from ...core import app as app_module
@@ -53,7 +52,6 @@ def _dump(value: typing.Any) -> str:
def _authorized(permission: Permission):
context = get_request_context()
require_permission(context, permission)
diagnostics.workspace(context)
return context
@@ -443,7 +441,7 @@ class LangBotMCPServer:
"""Register a tool boundary before its authorization and service call."""
def register(fn):
observed = diagnostics.observe(diagnostics.operation_id('mcp', fn), source='mcp', ap=self.ap)(fn)
observed = fn
return self.mcp.tool(**options)(observed)
return register