diff --git a/hermes_cli/observability/__init__.py b/hermes_cli/observability/__init__.py index 1c4e70300c..3f7e611bc8 100644 --- a/hermes_cli/observability/__init__.py +++ b/hermes_cli/observability/__init__.py @@ -12,7 +12,10 @@ def observe_lifecycle(hook_name: str, **kwargs: Any) -> None: """Dispatch a Hermes lifecycle event to built-in observability features.""" from . import relay_shared_metrics - _safe_observe(relay_shared_metrics.observe_lifecycle, hook_name, kwargs) + try: + relay_shared_metrics.observe_lifecycle(hook_name, **kwargs) + except Exception: + logger.warning("Built-in observability hook failed: %s", hook_name, exc_info=True) def handles_hook(hook_name: str) -> bool: @@ -20,12 +23,3 @@ def handles_hook(hook_name: str) -> bool: from . import relay_shared_metrics return relay_shared_metrics.handles_hook(hook_name) - - -def _safe_observe(callback: Any, hook_name: str, kwargs: dict[str, Any]) -> None: - try: - callback(hook_name, **kwargs) - except Exception: - logger.warning( - "Built-in observability hook failed: %s", hook_name, exc_info=True - ) diff --git a/hermes_cli/observability/shared_metrics_send_config.py b/hermes_cli/observability/shared_metrics_send_config.py index 0d44906b19..de8ac454ad 100644 --- a/hermes_cli/observability/shared_metrics_send_config.py +++ b/hermes_cli/observability/shared_metrics_send_config.py @@ -8,14 +8,10 @@ from urllib.parse import urlparse logger = logging.getLogger(__name__) -#: Production ingest endpoint. Overridable through config only. -#: -#: Deliberately NOT overridable by an environment variable: AGENTS.md reserves -#: HERMES_* env vars for secrets, and a behavioural override here would be a -#: consent hazard — a user who agreed to send metrics to Nous could have them -#: silently redirected to any host by an inherited variable, with nothing -#: visible in their config to show it. Tests and the staging E2E write this -#: key into a throwaway profile instead. +#: Production ingest endpoint. Overridable through config only — deliberately NOT by an +#: environment variable: AGENTS.md reserves HERMES_* for secrets, and an inherited variable +#: could silently redirect consented metrics to any host with nothing visible in config. +#: Tests and the staging E2E write this key into a throwaway profile instead. DEFAULT_ENDPOINT = "https://telemetry.nousresearch.com/v1/telemetry" _LOCAL_HOSTS = frozenset({"localhost", "127.0.0.1", "::1", "[::1]"}) @@ -38,28 +34,20 @@ class SendConfig: def _endpoint_is_safe(endpoint: str) -> bool: - """Reject plaintext destinations unless they are loopback. - - Telemetry must not leave a machine in clear text because of a typo in a config file. Loopback - stays allowed so tests can use a local HTTP server. - """ + """Reject plaintext destinations unless they are loopback (so tests can use local HTTP).""" try: parsed = urlparse(endpoint) except ValueError: return False if parsed.scheme == "https": return True - if parsed.scheme == "http": - return (parsed.hostname or "") in _LOCAL_HOSTS - return False + return parsed.scheme == "http" and (parsed.hostname or "") in _LOCAL_HOSTS def resolve_send_config(config: dict | None) -> SendConfig: - """Resolve transmission settings from config plus the environment. + """Resolve transmission settings from config (endpoint: config > production default). - Endpoint precedence: config > production default. - - ``send`` is returned as False whenever transmission cannot legitimately happen, so callers never + ``send`` is False whenever transmission cannot legitimately happen, so callers never have to re-check the combination. """ global _warned_send_without_collection @@ -74,8 +62,7 @@ def resolve_send_config(config: dict | None) -> SendConfig: send_requested = shared.get("send") is True if send_requested and not enabled: - # Loud, not silent: the user believes telemetry is being sent, and it - # never will be. Error level, once per process. + # Loud, not silent: the user believes telemetry is being sent, and it never will be. if not _warned_send_without_collection: _warned_send_without_collection = True logger.error( diff --git a/hermes_cli/observability/shared_metrics_subscriber.py b/hermes_cli/observability/shared_metrics_subscriber.py index 1104a8a0d3..48e96733b9 100644 --- a/hermes_cli/observability/shared_metrics_subscriber.py +++ b/hermes_cli/observability/shared_metrics_subscriber.py @@ -26,6 +26,20 @@ from .shared_metrics_contract import ( logger = logging.getLogger(__name__) +# Contract projections in match order; each yields (metric_name, dimensions) or None. +_COUNTERS = ( + client_active_counter, + lambda event: _named(MODEL_ROUTE_METRIC, model_call_dimensions(event)), + lambda event: _named(TOOL_CALL_METRIC, tool_call_dimensions(event)), + task_counter, + tool_approval_counter, + skill_counter, +) + + +def _named(metric_name: str, dimensions: dict | None) -> tuple[str, dict] | None: + return None if dimensions is None else (metric_name, dimensions) + class SharedMetricsSubscriber: """Persist validated Hermes counters from Relay lifecycle events.""" @@ -56,17 +70,7 @@ class SharedMetricsSubscriber: @staticmethod def _classify(event: Any) -> tuple[str, dict] | None: """Return ``(metric_name, dimensions)`` for the first matching contract, else None.""" - metric = client_active_counter(event) - if metric is not None: - return metric - for metric_name, project in ( - (MODEL_ROUTE_METRIC, model_call_dimensions), - (TOOL_CALL_METRIC, tool_call_dimensions), - ): - dimensions = project(event) - if dimensions is not None: - return metric_name, dimensions - return task_counter(event) or tool_approval_counter(event) or skill_counter(event) + return next((m for m in (project(event) for project in _COUNTERS) if m is not None), None) def __call__(self, event: Any) -> None: if self._runtime_id is not None: @@ -87,14 +91,8 @@ class SharedMetricsSubscriber: if metric_name == CLIENT_ACTIVE_METRIC: self.store.record_client_active(self._client_resource) else: - self.store.record_counter( - metric_name, - dimensions, - self._client_resource, - ) + self.store.record_counter(metric_name, dimensions, self._client_resource) except Exception: logger.warning( - "Unable to persist the Hermes shared metric: %s", - metric_name, - exc_info=True, + "Unable to persist the Hermes shared metric: %s", metric_name, exc_info=True )