|
41 | 41 | HEALTH_TOOLS, |
42 | 42 | ) |
43 | 43 | from kubeflow_mcp.core.logging import with_correlation_id |
44 | | -from kubeflow_mcp.core.policy import apply_policy_filters, get_allowed_tools, is_read_only |
| 44 | +from kubeflow_mcp.core.policy import ( |
| 45 | + apply_policy_filters, |
| 46 | + get_allowed_tools, |
| 47 | + get_effective_persona, |
| 48 | + is_read_only, |
| 49 | +) |
45 | 50 | from kubeflow_mcp.core.resilience import RateLimiter, get_breaker |
46 | 51 | from kubeflow_mcp.core.resources import register_resources |
47 | 52 | from kubeflow_mcp.core.security import mask_sensitive_data |
| 53 | +from kubeflow_mcp.core.telemetry import get_tracer |
48 | 54 |
|
49 | 55 | logger = logging.getLogger(__name__) |
50 | 56 |
|
@@ -88,66 +94,84 @@ def _audit_wrap(tool_func): |
88 | 94 | @functools.wraps(tool_func) |
89 | 95 | def wrapper(**kwargs): |
90 | 96 | tool_name = tool_func.__name__ |
91 | | - |
92 | | - if _rate_limiter is not None and not _rate_limiter.acquire(): |
93 | | - logger.warning("rate_limited", extra={"tool": tool_name}) |
94 | | - return { |
95 | | - "error": "Rate limit exceeded. Retry after a brief pause.", |
96 | | - "error_code": ErrorCode.RATE_LIMITED, |
97 | | - } |
98 | | - |
99 | | - breaker = get_breaker(tool_name) |
100 | | - if not breaker.can_execute(): |
101 | | - logger.warning("circuit_open", extra={"tool": tool_name}) |
102 | | - return { |
103 | | - "error": f"Circuit breaker open for '{tool_name}' — K8s API may be degraded. Retries automatically after recovery timeout.", |
104 | | - "error_code": ErrorCode.CIRCUIT_OPEN, |
105 | | - } |
106 | | - |
107 | 97 | cid = with_correlation_id() |
108 | | - masked = mask_sensitive_data(kwargs) if kwargs else {} |
| 98 | + tracer = get_tracer("kubeflow_mcp.tools") |
| 99 | + persona = get_effective_persona() |
109 | 100 | start = time.monotonic() |
110 | | - try: |
111 | | - result = tool_func(**kwargs) |
112 | | - duration_ms = int((time.monotonic() - start) * 1000) |
113 | | - is_success = ( |
114 | | - "error_code" not in result and "error" not in result |
115 | | - if isinstance(result, dict) |
116 | | - else True |
117 | | - ) |
118 | | - if is_success: |
119 | | - breaker.record_success() |
120 | | - elif is_infrastructure_error(result): |
121 | | - breaker.record_failure() |
122 | 101 |
|
123 | | - logger.info( |
124 | | - "tool_call", |
125 | | - extra={ |
126 | | - "audit": True, |
127 | | - "correlation_id": cid, |
128 | | - "tool": tool_name, |
129 | | - "parameters": masked, |
130 | | - "success": is_success, |
131 | | - "duration_ms": duration_ms, |
132 | | - }, |
133 | | - ) |
134 | | - return _inject_meta(result, tool_name) |
135 | | - except Exception: |
136 | | - duration_ms = int((time.monotonic() - start) * 1000) |
137 | | - breaker.record_failure() |
138 | | - logger.error( |
139 | | - "tool_call_failed", |
140 | | - extra={ |
141 | | - "audit": True, |
142 | | - "correlation_id": cid, |
143 | | - "tool": tool_name, |
144 | | - "parameters": masked, |
145 | | - "success": False, |
146 | | - "duration_ms": duration_ms, |
147 | | - }, |
148 | | - exc_info=True, |
149 | | - ) |
150 | | - raise |
| 102 | + with tracer.start_as_current_span("tool_call") as span: |
| 103 | + span.set_attribute("tool.name", tool_name) |
| 104 | + span.set_attribute("kubeflow.persona", persona) |
| 105 | + span.set_attribute("correlation_id", cid) |
| 106 | + |
| 107 | + if _rate_limiter is not None and not _rate_limiter.acquire(): |
| 108 | + duration_ms = int((time.monotonic() - start) * 1000) |
| 109 | + span.set_attribute("tool.success", False) |
| 110 | + span.set_attribute("tool.duration_ms", duration_ms) |
| 111 | + logger.warning("rate_limited", extra={"tool": tool_name}) |
| 112 | + return { |
| 113 | + "error": "Rate limit exceeded. Retry after a brief pause.", |
| 114 | + "error_code": ErrorCode.RATE_LIMITED, |
| 115 | + } |
| 116 | + |
| 117 | + breaker = get_breaker(tool_name) |
| 118 | + if not breaker.can_execute(): |
| 119 | + duration_ms = int((time.monotonic() - start) * 1000) |
| 120 | + span.set_attribute("tool.success", False) |
| 121 | + span.set_attribute("tool.duration_ms", duration_ms) |
| 122 | + logger.warning("circuit_open", extra={"tool": tool_name}) |
| 123 | + return { |
| 124 | + "error": f"Circuit breaker open for '{tool_name}' — K8s API may be degraded. Retries automatically after recovery timeout.", |
| 125 | + "error_code": ErrorCode.CIRCUIT_OPEN, |
| 126 | + } |
| 127 | + |
| 128 | + masked = mask_sensitive_data(kwargs) if kwargs else {} |
| 129 | + try: |
| 130 | + result = tool_func(**kwargs) |
| 131 | + duration_ms = int((time.monotonic() - start) * 1000) |
| 132 | + is_success = ( |
| 133 | + "error_code" not in result and "error" not in result |
| 134 | + if isinstance(result, dict) |
| 135 | + else True |
| 136 | + ) |
| 137 | + span.set_attribute("tool.success", is_success) |
| 138 | + span.set_attribute("tool.duration_ms", duration_ms) |
| 139 | + if is_success: |
| 140 | + breaker.record_success() |
| 141 | + elif is_infrastructure_error(result): |
| 142 | + breaker.record_failure() |
| 143 | + |
| 144 | + logger.info( |
| 145 | + "tool_call", |
| 146 | + extra={ |
| 147 | + "audit": True, |
| 148 | + "correlation_id": cid, |
| 149 | + "tool": tool_name, |
| 150 | + "parameters": masked, |
| 151 | + "success": is_success, |
| 152 | + "duration_ms": duration_ms, |
| 153 | + }, |
| 154 | + ) |
| 155 | + return _inject_meta(result, tool_name) |
| 156 | + except Exception as exc: |
| 157 | + duration_ms = int((time.monotonic() - start) * 1000) |
| 158 | + breaker.record_failure() |
| 159 | + span.set_attribute("tool.success", False) |
| 160 | + span.set_attribute("tool.duration_ms", duration_ms) |
| 161 | + span.record_exception(exc) |
| 162 | + logger.error( |
| 163 | + "tool_call_failed", |
| 164 | + extra={ |
| 165 | + "audit": True, |
| 166 | + "correlation_id": cid, |
| 167 | + "tool": tool_name, |
| 168 | + "parameters": masked, |
| 169 | + "success": False, |
| 170 | + "duration_ms": duration_ms, |
| 171 | + }, |
| 172 | + exc_info=True, |
| 173 | + ) |
| 174 | + raise |
151 | 175 |
|
152 | 176 | return wrapper |
153 | 177 |
|
|
0 commit comments