Merge pull request #64536 from victor-kyriazakos/feat/gateway-health-diagnostics

feat(monitoring): gateway health & diagnostics OTLP export
This commit is contained in:
Teknium 2026-07-29 08:58:33 -07:00 • committed by GitHub
commit 7de33cc57e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 4316 additions and 26 deletions

View file

@ -2230,7 +2230,7 @@ DEFAULT_CONFIG = {
"privacy": {
"redact_pii": False, # When True, hash user IDs and strip phone numbers from LLM context
},
# Text-to-speech configuration
# Each provider supports an optional `max_text_length:` override for the
# per-request input-character cap. Omit it to use the provider's documented
@ -3211,6 +3211,44 @@ DEFAULT_CONFIG = {
"force_ipv4": False,
},
# Gateway monitoring — Service Health Monitoring plus redacted Operational
# Diagnostics for the gateway daemon, exported over OTLP to an
# operator-configured endpoint (OTEL Collector, DataDog, ...). Content-free
# by construction: no prompts, messages, tool args/results, session
# history, usage analytics, audit logs, or trajectories. Off by default;
# nothing is collected or sent until an operator enables it and sets an
# endpoint.
"monitoring": {
# Stable install identifier attached to exported health signals so an
# operator can tell instances apart in their collector. Empty string
# means "mint a fresh UUID on first use"; clear it to rotate. Carries
# no account identity.
"install_id": "",
# Gateway health & diagnostics export.
"gateway_health_export": {
"enabled": False,
"metrics_enabled": True,
"diagnostic_events_enabled": True,
"warning_error_events_enabled": True,
"export_interval_seconds": 60,
"logs_export_interval_seconds": 5,
"resource_attributes": {
"service.name": "hermes-gateway",
"deployment.environment.name": "production",
},
},
# OTLP destination. headers_env maps header names to ENVIRONMENT
# VARIABLE NAMES (never secret values); values are read from the
# environment at export time.
"export": {
"otlp": {
"enabled": False,
"endpoint": "",
"headers_env": {},
},
},
},
# Gateway settings — control how messaging platforms (Telegram, Discord,
# Slack, etc.) deliver agent-produced files as native attachments.
"gateway": {

View file

@ -465,6 +465,7 @@ from hermes_cli.subcommands.memory import build_memory_parser
from hermes_cli.subcommands.acp import build_acp_parser
from hermes_cli.subcommands.tools import build_tools_parser
from hermes_cli.subcommands.insights import build_insights_parser
from hermes_cli.subcommands.monitoring import build_monitoring_parser
from hermes_cli.subcommands.skills import build_skills_parser
from hermes_cli.subcommands.pairing import build_pairing_parser
from hermes_cli.subcommands.plugins import build_plugins_parser
@ -15429,7 +15430,7 @@ _BUILTIN_SUBCOMMANDS = frozenset(
"dump", "egress", "fallback", "gateway", "hooks", "import", "import-agent", "insights",
"gui", "desktop", "kanban", "login", "logout", "logs", "lsp", "mcp", "memory", "migrate", "moa",
"journey", "memory-graph", "learning",
"model", "pairing", "pets", "plugins", "portal", "profile",
"model", "monitoring", "pairing", "pets", "plugins", "portal", "profile",
"project", "proxy",
"prompt-size",
"send", "sessions", "setup",
@ -15911,6 +15912,51 @@ def cmd_insights(args):
print(f"Error generating insights: {e}")
def cmd_monitoring(args):
"""Gateway monitoring status: health & diagnostics export posture."""
from hermes_cli.config import load_config
action = getattr(args, "monitoring_action", None) or "status"
config = load_config()
mon_raw = config.get("monitoring")
mon: dict = mon_raw if isinstance(mon_raw, dict) else {}
if action == "status":
from agent.monitoring import otlp_exporter
gh_raw = mon.get("gateway_health_export")
gh: dict = gh_raw if isinstance(gh_raw, dict) else {}
export_raw = mon.get("export")
export_cfg: dict = export_raw if isinstance(export_raw, dict) else {}
otlp_raw = export_cfg.get("otlp")
otlp: dict = otlp_raw if isinstance(otlp_raw, dict) else {}
print("Gateway monitoring")
print(f" Health export: {'enabled' if gh.get('enabled') else 'disabled'} "
f"(monitoring.gateway_health_export.enabled)")
if gh.get("enabled"):
print(f" Metrics: {'on' if gh.get('metrics_enabled', True) else 'off'} "
f"(interval {gh.get('export_interval_seconds', 60)}s)")
print(f" Diagnostic events: {'on' if gh.get('diagnostic_events_enabled', True) else 'off'}")
print(f" Warning/error logs: {'on' if gh.get('warning_error_events_enabled', True) else 'off'} "
f"(interval {gh.get('logs_export_interval_seconds', 5)}s)")
print(" Content safety: always on "
"(rendered messages are never exported; not configurable)")
endpoint = otlp.get("endpoint") or ""
if otlp.get("enabled") and endpoint:
print(f" OTLP endpoint: {endpoint}")
else:
print(" OTLP endpoint: not configured (monitoring.export.otlp)")
print(f" OTel SDK: {'installed' if otlp_exporter.is_available() else 'not installed'} "
f"(optional extra: hermes-agent[otlp])")
print("\n Scope: gateway service health + redacted diagnostics only.")
print(" No prompts, messages, tool args/results, usage analytics, or traces.")
return
print(f"Unknown monitoring action: {action}", file=sys.stderr)
sys.exit(2)
def cmd_skills(args):
# Route 'config' action to skills_config module
if getattr(args, "skills_action", None) == "config":
@ -18216,6 +18262,7 @@ def main():
# insights command (parser built in hermes_cli/subcommands/insights.py)
# =========================================================================
build_insights_parser(subparsers, cmd_insights=cmd_insights)
build_monitoring_parser(subparsers, cmd_monitoring=cmd_monitoring)
# =========================================================================
# claw command (parser built in hermes_cli/subcommands/claw.py)

View file

@ -0,0 +1,36 @@
"""``hermes monitoring`` subcommand parser.
Gateway monitoring control and inspection. ``status`` shows whether the
gateway health & diagnostics export is enabled, where it points, and the
redaction posture.
The handler is injected to avoid importing ``main`` (mirrors the insights
subcommand).
"""
from __future__ import annotations
from typing import Callable
def build_monitoring_parser(subparsers, *, cmd_monitoring: Callable) -> None:
"""Attach the ``monitoring`` subcommand (with actions) to ``subparsers``."""
p = subparsers.add_parser(
"monitoring",
help="Inspect gateway monitoring (health & diagnostics export)",
description=(
"Gateway monitoring: service health metrics plus redacted "
"diagnostics, exported over OTLP to an operator-configured "
"endpoint. Content-free by construction — no prompts, messages, "
"tool args/results, or usage analytics. Configure under "
"monitoring.* in config.yaml."
),
)
sub = p.add_subparsers(dest="monitoring_action")
sub.add_parser(
"status",
help="Show monitoring settings, export state, and redaction posture",
)
p.set_defaults(func=cmd_monitoring)