diff --git a/agent/monitoring/gateway_health_export.py b/agent/monitoring/gateway_health_export.py index 9295072da98..04fff71a381 100644 --- a/agent/monitoring/gateway_health_export.py +++ b/agent/monitoring/gateway_health_export.py @@ -72,6 +72,20 @@ def _safe_resource_attributes(raw: Any) -> Dict[str, str]: return attrs +def _runtime_resource_attributes( + config: Dict[str, Any], *, telemetry_scope: str +) -> Dict[str, str]: + """Build the safe OTLP resource shared by metrics and diagnostic logs.""" + gh = _gateway_health_config(config) + attrs = _safe_resource_attributes(gh.get("resource_attributes")) + from agent.monitoring.gateway_health import _safe_instance_id + + attrs["service.name"] = "hermes-gateway" + attrs["service.instance.id"] = _safe_instance_id(_install_id(config)) + attrs["telemetry.scope"] = telemetry_scope + return attrs + + def _diagnostic_log_attributes(event: Dict[str, Any]) -> Dict[str, Any]: attrs: Dict[str, Any] = {} for key in _DIAGNOSTIC_ATTRIBUTE_KEYS: @@ -299,9 +313,9 @@ def _start_metric_provider(config: Dict[str, Any], sdk: Dict[str, Any]) -> Any: exporter = sdk["OTLPMetricExporter"](endpoint=endpoint, headers=headers or None) interval_ms = max(5, int(gh.get("export_interval_seconds", 60))) * 1000 reader = sdk["PeriodicExportingMetricReader"](exporter, export_interval_millis=interval_ms) - resource_attrs = _safe_resource_attributes(gh.get("resource_attributes")) - resource_attrs["service.name"] = "hermes-gateway" - resource_attrs["telemetry.scope"] = "gateway_health" + resource_attrs = _runtime_resource_attributes( + config, telemetry_scope="gateway_health" + ) provider = sdk["MeterProvider"]( metric_readers=[reader], resource=sdk["Resource"].create(resource_attrs), @@ -354,12 +368,11 @@ class GatewayDiagnosticLogStreamer: def __init__(self, config: Dict[str, Any], sdk: Dict[str, Any]): otlp = _otlp_config(config) - gh = _gateway_health_config(config) headers = _resolve_headers(otlp.get("headers_env")) endpoint = _logs_endpoint(str(otlp.get("endpoint"))) - resource_attrs = _safe_resource_attributes(gh.get("resource_attributes")) - resource_attrs["service.name"] = "hermes-gateway" - resource_attrs["telemetry.scope"] = "gateway_diagnostics" + resource_attrs = _runtime_resource_attributes( + config, telemetry_scope="gateway_diagnostics" + ) self._provider = sdk["LoggerProvider"](resource=sdk["Resource"].create(resource_attrs)) self._processor = sdk["BatchLogRecordProcessor"]( sdk["OTLPLogExporter"](endpoint=endpoint, headers=headers or None) diff --git a/agent/monitoring/otlp_exporter.py b/agent/monitoring/otlp_exporter.py index 27c600dcf8b..c9268b4ee5a 100644 --- a/agent/monitoring/otlp_exporter.py +++ b/agent/monitoring/otlp_exporter.py @@ -115,12 +115,20 @@ def build_exporter(config: Dict[str, Any]): return sdk["OTLPSpanExporter"](endpoint=endpoint, headers=headers or None) +def _resource_attributes(config: Dict[str, Any]) -> Dict[str, str]: + from agent.monitoring.gateway_health import _safe_instance_id + from agent.monitoring.policy import ensure_install_id + + return { + "service.name": "hermes-gateway", + "service.instance.id": _safe_instance_id(ensure_install_id(config)), + "telemetry.scope": "gateway_monitoring", + } + + def _make_provider(config: Dict[str, Any]): sdk = _require_sdk() - resource = sdk["Resource"].create({ - "service.name": "hermes-gateway", - "telemetry.scope": "gateway_monitoring", - }) + resource = sdk["Resource"].create(_resource_attributes(config)) provider = sdk["TracerProvider"](resource=resource) processor = sdk["BatchSpanProcessor"](build_exporter(config)) provider.add_span_processor(processor) diff --git a/tests/monitoring/test_gateway_health_export.py b/tests/monitoring/test_gateway_health_export.py index 715de681bf8..363b86cdfc4 100644 --- a/tests/monitoring/test_gateway_health_export.py +++ b/tests/monitoring/test_gateway_health_export.py @@ -299,6 +299,28 @@ def test_instance_id_hash_is_stable_and_distinguishes_instances(): assert "install-1" not in first +def test_runtime_resource_attributes_include_stable_hashed_instance(): + from agent.monitoring.gateway_health_export import _runtime_resource_attributes + + config = { + "monitoring": { + "install_id": "private-install-id", + "gateway_health_export": { + "resource_attributes": {"deployment.environment.name": "staging"} + }, + } + } + + attrs = _runtime_resource_attributes(config, telemetry_scope="gateway_health") + + assert attrs["service.name"] == "hermes-gateway" + assert attrs["service.instance.id"].startswith("sha256:") + assert len(attrs["service.instance.id"]) == len("sha256:") + 24 + assert "private-install-id" not in str(attrs) + assert attrs["deployment.environment.name"] == "staging" + assert attrs["telemetry.scope"] == "gateway_health" + + def test_diagnostic_log_attributes_are_allowlisted_redacted_and_profile_free(): from agent.monitoring.gateway_health_export import _diagnostic_log_attributes diff --git a/tests/monitoring/test_otlp_exporter.py b/tests/monitoring/test_otlp_exporter.py index 56edd566524..91ba7ede553 100644 --- a/tests/monitoring/test_otlp_exporter.py +++ b/tests/monitoring/test_otlp_exporter.py @@ -76,6 +76,18 @@ def test_is_enabled_requires_endpoint_and_flag(): assert not OE.is_enabled({}) +def test_trace_resource_includes_stable_hashed_instance(): + attrs = OE._resource_attributes( + {"monitoring": {"install_id": "private-install-id"}} + ) + + assert attrs["service.name"] == "hermes-gateway" + assert attrs["service.instance.id"].startswith("sha256:") + assert len(attrs["service.instance.id"]) == len("sha256:") + 24 + assert "private-install-id" not in str(attrs) + assert attrs["telemetry.scope"] == "gateway_monitoring" + + def test_export_otlp_feature_specs_match_pyproject(): from tools.lazy_deps import LAZY_DEPS import re