mcp-mesh 0.7.12__py3-none-any.whl → 0.7.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/__init__.py +1 -22
- _mcp_mesh/engine/async_mcp_client.py +88 -25
- _mcp_mesh/engine/decorator_registry.py +10 -9
- _mcp_mesh/engine/dependency_injector.py +64 -53
- _mcp_mesh/engine/mesh_llm_agent.py +119 -5
- _mcp_mesh/engine/mesh_llm_agent_injector.py +30 -0
- _mcp_mesh/engine/session_aware_client.py +3 -3
- _mcp_mesh/engine/unified_mcp_proxy.py +82 -90
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -89
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +3 -3
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +30 -28
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +16 -18
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +5 -5
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +3 -3
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -6
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +1 -1
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +15 -11
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +3 -3
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +37 -268
- _mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +142 -0
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +57 -93
- _mcp_mesh/pipeline/shared/registry_connection.py +1 -1
- _mcp_mesh/shared/health_check_manager.py +313 -0
- _mcp_mesh/shared/logging_config.py +190 -7
- _mcp_mesh/shared/registry_client_wrapper.py +8 -8
- _mcp_mesh/shared/sse_parser.py +19 -17
- _mcp_mesh/tracing/execution_tracer.py +26 -1
- _mcp_mesh/tracing/fastapi_tracing_middleware.py +3 -4
- _mcp_mesh/tracing/trace_context_helper.py +25 -6
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/METADATA +1 -1
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/RECORD +38 -39
- mesh/__init__.py +3 -1
- mesh/decorators.py +81 -43
- mesh/helpers.py +72 -4
- mesh/types.py +48 -4
- _mcp_mesh/engine/full_mcp_proxy.py +0 -641
- _mcp_mesh/engine/mcp_client_proxy.py +0 -457
- _mcp_mesh/shared/health_check_cache.py +0 -246
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,12 +2,34 @@
|
|
|
2
2
|
Centralized logging configuration for MCP Mesh runtime.
|
|
3
3
|
|
|
4
4
|
This module configures logging based on the MCP_MESH_LOG_LEVEL environment variable.
|
|
5
|
+
|
|
6
|
+
Log Levels:
|
|
7
|
+
CRITICAL (50) - Fatal errors
|
|
8
|
+
ERROR (40) - Errors
|
|
9
|
+
WARNING (30) - Warnings
|
|
10
|
+
INFO (20) - Normal operation (heartbeat counts, connections)
|
|
11
|
+
DEBUG (10) - Debugging info (tool calls, actual issues)
|
|
12
|
+
TRACE (5) - Verbose internals (heartbeat steps, SSE parsing)
|
|
5
13
|
"""
|
|
6
14
|
|
|
7
15
|
import logging
|
|
8
16
|
import os
|
|
9
17
|
import sys
|
|
10
18
|
|
|
19
|
+
# Define TRACE level (below DEBUG)
|
|
20
|
+
TRACE = 5
|
|
21
|
+
logging.addLevelName(TRACE, "TRACE")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _trace(self, message, *args, **kwargs):
|
|
25
|
+
"""Log a message with TRACE level."""
|
|
26
|
+
if self.isEnabledFor(TRACE):
|
|
27
|
+
self._log(TRACE, message, args, **kwargs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Add trace method to Logger class
|
|
31
|
+
logging.Logger.trace = _trace
|
|
32
|
+
|
|
11
33
|
|
|
12
34
|
class SafeStreamHandler(logging.StreamHandler):
|
|
13
35
|
"""A stream handler that gracefully handles closed streams."""
|
|
@@ -41,6 +63,7 @@ def configure_logging():
|
|
|
41
63
|
|
|
42
64
|
# Map string to logging level
|
|
43
65
|
log_levels = {
|
|
66
|
+
"TRACE": TRACE,
|
|
44
67
|
"DEBUG": logging.DEBUG,
|
|
45
68
|
"INFO": logging.INFO,
|
|
46
69
|
"WARNING": logging.WARNING,
|
|
@@ -51,13 +74,16 @@ def configure_logging():
|
|
|
51
74
|
|
|
52
75
|
log_level = log_levels.get(log_level_str, logging.INFO)
|
|
53
76
|
|
|
54
|
-
# Check if debug mode is enabled
|
|
77
|
+
# Check if debug mode is enabled (sets DEBUG level)
|
|
55
78
|
debug_mode = os.environ.get("MCP_MESH_DEBUG_MODE", "").lower() in (
|
|
56
79
|
"true",
|
|
57
80
|
"1",
|
|
58
81
|
"yes",
|
|
59
82
|
)
|
|
60
83
|
|
|
84
|
+
# Check if trace mode is enabled via log level
|
|
85
|
+
trace_mode = log_level_str == "TRACE"
|
|
86
|
+
|
|
61
87
|
# Clear any existing handlers to avoid conflicts
|
|
62
88
|
root_logger = logging.getLogger()
|
|
63
89
|
for handler in root_logger.handlers[:]:
|
|
@@ -65,18 +91,32 @@ def configure_logging():
|
|
|
65
91
|
|
|
66
92
|
# Configure with safe stream handler for background threads
|
|
67
93
|
handler = SafeStreamHandler(sys.stdout)
|
|
68
|
-
handler.setLevel(
|
|
94
|
+
handler.setLevel(TRACE) # Handler allows all levels including TRACE
|
|
69
95
|
handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s"))
|
|
70
96
|
|
|
71
97
|
root_logger.addHandler(handler)
|
|
72
98
|
|
|
73
99
|
# Root logger always INFO - all third-party libs stay quiet
|
|
74
100
|
# This is the allowlist approach: instead of blocklisting noisy loggers one by one,
|
|
75
|
-
# we keep root at INFO and only elevate mcp-mesh loggers
|
|
101
|
+
# we keep root at INFO and only elevate mcp-mesh loggers
|
|
76
102
|
root_logger.setLevel(logging.INFO)
|
|
77
103
|
|
|
78
|
-
#
|
|
79
|
-
|
|
104
|
+
# Suppress noisy third-party loggers (FastMCP/MCP library logs)
|
|
105
|
+
# These produce verbose INFO logs like "Terminating session: None" and
|
|
106
|
+
# "Processing request of type CallToolRequest" that clutter debug output
|
|
107
|
+
logging.getLogger("mcp").setLevel(logging.WARNING)
|
|
108
|
+
logging.getLogger("mcp.server").setLevel(logging.WARNING)
|
|
109
|
+
logging.getLogger("mcp.client").setLevel(logging.WARNING)
|
|
110
|
+
logging.getLogger("fastmcp").setLevel(logging.WARNING)
|
|
111
|
+
|
|
112
|
+
# Set MCP Mesh logger levels based on configuration
|
|
113
|
+
if trace_mode:
|
|
114
|
+
# TRACE mode: show everything including verbose heartbeat internals
|
|
115
|
+
logging.getLogger("mesh").setLevel(TRACE)
|
|
116
|
+
logging.getLogger("mcp_mesh").setLevel(TRACE)
|
|
117
|
+
logging.getLogger("_mcp_mesh").setLevel(TRACE)
|
|
118
|
+
elif debug_mode:
|
|
119
|
+
# DEBUG mode: show debug info but not verbose trace logs
|
|
80
120
|
logging.getLogger("mesh").setLevel(logging.DEBUG)
|
|
81
121
|
logging.getLogger("mcp_mesh").setLevel(logging.DEBUG)
|
|
82
122
|
logging.getLogger("_mcp_mesh").setLevel(logging.DEBUG)
|
|
@@ -86,9 +126,152 @@ def configure_logging():
|
|
|
86
126
|
logging.getLogger("mcp_mesh").setLevel(log_level)
|
|
87
127
|
logging.getLogger("_mcp_mesh").setLevel(log_level)
|
|
88
128
|
|
|
89
|
-
# Return the configured level for reference
|
|
90
|
-
|
|
129
|
+
# Return the configured level for reference
|
|
130
|
+
if trace_mode:
|
|
131
|
+
return TRACE
|
|
132
|
+
elif debug_mode:
|
|
133
|
+
return logging.DEBUG
|
|
134
|
+
else:
|
|
135
|
+
return log_level
|
|
91
136
|
|
|
92
137
|
|
|
93
138
|
# Configure logging on module import
|
|
94
139
|
_configured_level = configure_logging()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ============================================================================
|
|
143
|
+
# Log Value Formatting Helpers
|
|
144
|
+
# ============================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_trace_prefix() -> str:
|
|
148
|
+
"""Get trace ID prefix for log lines if tracing is active.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
String like "[trace=abc12345] " if trace context exists, empty string otherwise.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
from ..tracing.context import TraceContext
|
|
155
|
+
|
|
156
|
+
trace_info = TraceContext.get_current()
|
|
157
|
+
if trace_info and trace_info.trace_id:
|
|
158
|
+
# Use first 8 chars for readability
|
|
159
|
+
short_id = trace_info.trace_id[:8]
|
|
160
|
+
return f"[{short_id}] "
|
|
161
|
+
except Exception:
|
|
162
|
+
# Tracing not available or not configured
|
|
163
|
+
pass
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def format_log_value(value, max_len: int = 1000) -> str:
|
|
168
|
+
"""Format a value for logging with truncation.
|
|
169
|
+
|
|
170
|
+
Provides a readable representation of values with size info and truncation
|
|
171
|
+
for large payloads. Suitable for DEBUG level logging.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
value: Any value to format
|
|
175
|
+
max_len: Maximum length before truncation (default 1000)
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Formatted string representation
|
|
179
|
+
"""
|
|
180
|
+
if value is None:
|
|
181
|
+
return "None"
|
|
182
|
+
|
|
183
|
+
type_name = type(value).__name__
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
if isinstance(value, dict):
|
|
187
|
+
content = str(value)
|
|
188
|
+
if len(content) > max_len:
|
|
189
|
+
return f"{type_name}({len(value)} keys): {content[:max_len]}..."
|
|
190
|
+
return content
|
|
191
|
+
|
|
192
|
+
elif isinstance(value, (list, tuple)):
|
|
193
|
+
content = str(value)
|
|
194
|
+
if len(content) > max_len:
|
|
195
|
+
return f"{type_name}({len(value)} items): {content[:max_len]}..."
|
|
196
|
+
return content
|
|
197
|
+
|
|
198
|
+
elif isinstance(value, str):
|
|
199
|
+
if len(value) > max_len:
|
|
200
|
+
return f'"{value[:max_len]}..." ({len(value)} chars)'
|
|
201
|
+
return f'"{value}"'
|
|
202
|
+
|
|
203
|
+
elif isinstance(value, bytes):
|
|
204
|
+
return f"bytes({len(value)} bytes)"
|
|
205
|
+
|
|
206
|
+
elif hasattr(value, "__dict__"):
|
|
207
|
+
# Object with attributes - show class name and key attributes
|
|
208
|
+
content = str(value)
|
|
209
|
+
if len(content) > max_len:
|
|
210
|
+
return f"{type_name}: {content[:max_len]}..."
|
|
211
|
+
return f"{type_name}: {content}"
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
content = str(value)
|
|
215
|
+
if len(content) > max_len:
|
|
216
|
+
return f"{type_name}: {content[:max_len]}..."
|
|
217
|
+
return content
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return f"{type_name}: <error formatting: {e}>"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def format_args_summary(args: tuple, kwargs: dict) -> str:
|
|
224
|
+
"""Format function arguments as a summary (keys only).
|
|
225
|
+
|
|
226
|
+
Suitable for concise DEBUG logging showing what was passed.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
args: Positional arguments tuple
|
|
230
|
+
kwargs: Keyword arguments dict
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Summary string like "args=(2), kwargs=['name', 'value']"
|
|
234
|
+
"""
|
|
235
|
+
parts = []
|
|
236
|
+
|
|
237
|
+
if args:
|
|
238
|
+
parts.append(f"args=({len(args)})")
|
|
239
|
+
|
|
240
|
+
if kwargs:
|
|
241
|
+
keys = list(kwargs.keys())
|
|
242
|
+
parts.append(f"kwargs={keys}")
|
|
243
|
+
|
|
244
|
+
return ", ".join(parts) if parts else "no args"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def format_result_summary(result) -> str:
|
|
248
|
+
"""Format a result value as a summary (type and size).
|
|
249
|
+
|
|
250
|
+
Suitable for concise DEBUG logging showing what was returned.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
result: The return value
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Summary string like "dict(3 keys)" or "str(150 chars)"
|
|
257
|
+
"""
|
|
258
|
+
if result is None:
|
|
259
|
+
return "None"
|
|
260
|
+
|
|
261
|
+
type_name = type(result).__name__
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
if isinstance(result, dict):
|
|
265
|
+
return f"dict({len(result)} keys)"
|
|
266
|
+
elif isinstance(result, (list, tuple)):
|
|
267
|
+
return f"{type_name}({len(result)} items)"
|
|
268
|
+
elif isinstance(result, str):
|
|
269
|
+
return f"str({len(result)} chars)"
|
|
270
|
+
elif isinstance(result, bytes):
|
|
271
|
+
return f"bytes({len(result)} bytes)"
|
|
272
|
+
elif isinstance(result, (int, float, bool)):
|
|
273
|
+
return str(result)
|
|
274
|
+
else:
|
|
275
|
+
return type_name
|
|
276
|
+
except Exception:
|
|
277
|
+
return type_name
|
|
@@ -72,7 +72,7 @@ class RegistryClientWrapper:
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
registration_json = json.dumps(registration_dict, indent=2, default=str)
|
|
75
|
-
self.logger.
|
|
75
|
+
self.logger.trace(
|
|
76
76
|
f"🔍 Full heartbeat registration payload:\n{registration_json}"
|
|
77
77
|
)
|
|
78
78
|
|
|
@@ -148,7 +148,7 @@ class RegistryClientWrapper:
|
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
parsed_dep["kwargs"] = kwargs_data
|
|
151
|
-
self.logger.
|
|
151
|
+
self.logger.trace(
|
|
152
152
|
f"🔧 Parsed kwargs for {dep_resolution.get('capability')}: {kwargs_data}"
|
|
153
153
|
)
|
|
154
154
|
except (json.JSONDecodeError, TypeError) as e:
|
|
@@ -178,7 +178,7 @@ class RegistryClientWrapper:
|
|
|
178
178
|
FastHeartbeatStatus indicating required action
|
|
179
179
|
"""
|
|
180
180
|
try:
|
|
181
|
-
self.logger.
|
|
181
|
+
self.logger.trace(
|
|
182
182
|
f"🚀 Performing fast heartbeat check for agent '{agent_id}'"
|
|
183
183
|
)
|
|
184
184
|
|
|
@@ -189,14 +189,14 @@ class RegistryClientWrapper:
|
|
|
189
189
|
|
|
190
190
|
# Extract the actual HTTP status code from the response
|
|
191
191
|
status_code = http_response.status_code
|
|
192
|
-
self.logger.
|
|
192
|
+
self.logger.trace(
|
|
193
193
|
f"Fast heartbeat HEAD request for agent '{agent_id}' returned HTTP {status_code}"
|
|
194
194
|
)
|
|
195
195
|
|
|
196
196
|
# Convert HTTP status to semantic status
|
|
197
197
|
status = FastHeartbeatStatusUtil.from_http_code(status_code)
|
|
198
198
|
|
|
199
|
-
self.logger.
|
|
199
|
+
self.logger.trace(
|
|
200
200
|
f"✅ Fast heartbeat check completed for agent '{agent_id}': {status.value}"
|
|
201
201
|
)
|
|
202
202
|
return status
|
|
@@ -214,7 +214,7 @@ class RegistryClientWrapper:
|
|
|
214
214
|
|
|
215
215
|
# Handle 410 Gone specifically (agent unknown)
|
|
216
216
|
if "(410)" in error_str or "Gone" in error_str:
|
|
217
|
-
self.logger.
|
|
217
|
+
self.logger.trace(
|
|
218
218
|
f"🔍 Fast heartbeat: Agent '{agent_id}' unknown (410 Gone) - re-registration needed"
|
|
219
219
|
)
|
|
220
220
|
return FastHeartbeatStatus.AGENT_UNKNOWN
|
|
@@ -423,7 +423,7 @@ class RegistryClientWrapper:
|
|
|
423
423
|
"filter_mode": filter_mode,
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
self.logger.
|
|
426
|
+
self.logger.trace(
|
|
427
427
|
f"🤖 Extracted llm_filter for {func_name}: {len(normalized_filter)} filters, mode={filter_mode}"
|
|
428
428
|
)
|
|
429
429
|
break
|
|
@@ -449,7 +449,7 @@ class RegistryClientWrapper:
|
|
|
449
449
|
namespace=provider.get("namespace", "default"),
|
|
450
450
|
)
|
|
451
451
|
|
|
452
|
-
self.logger.
|
|
452
|
+
self.logger.trace(
|
|
453
453
|
f"🔌 Extracted llm_provider for {func_name}: {llm_provider_data.model_dump()}"
|
|
454
454
|
)
|
|
455
455
|
break
|
_mcp_mesh/shared/sse_parser.py
CHANGED
|
@@ -34,11 +34,11 @@ class SSEParser:
|
|
|
34
34
|
Raises:
|
|
35
35
|
RuntimeError: If SSE response cannot be parsed
|
|
36
36
|
"""
|
|
37
|
-
logger.
|
|
38
|
-
logger.
|
|
37
|
+
logger.trace(f"🔧 SSEParser.parse_sse_response called from {context}")
|
|
38
|
+
logger.trace(
|
|
39
39
|
f"🔧 Response text length: {len(response_text)}, starts with 'event:': {response_text.startswith('event:')}"
|
|
40
40
|
)
|
|
41
|
-
logger.
|
|
41
|
+
logger.trace(f"🔧 Response preview: {repr(response_text[:100])}...")
|
|
42
42
|
|
|
43
43
|
# Check if this is SSE format (can be malformed and not start with "event:")
|
|
44
44
|
is_sse_format = (
|
|
@@ -49,13 +49,13 @@ class SSEParser:
|
|
|
49
49
|
|
|
50
50
|
if not is_sse_format:
|
|
51
51
|
# Not an SSE response, try parsing as plain JSON
|
|
52
|
-
logger.
|
|
53
|
-
logger.
|
|
52
|
+
logger.trace(f"🔧 {context}: Parsing as plain JSON (not SSE format)")
|
|
53
|
+
logger.trace(
|
|
54
54
|
f"🔧 {context}: Response preview: {repr(response_text[:200])}..."
|
|
55
55
|
)
|
|
56
56
|
try:
|
|
57
57
|
result = json.loads(response_text)
|
|
58
|
-
logger.
|
|
58
|
+
logger.trace(f"🔧 {context}: Plain JSON parsed successfully")
|
|
59
59
|
return result
|
|
60
60
|
except json.JSONDecodeError as e:
|
|
61
61
|
logger.error(f"🔧 {context}: Plain JSON parse failed: {e}")
|
|
@@ -65,7 +65,7 @@ class SSEParser:
|
|
|
65
65
|
raise RuntimeError(f"Invalid JSON response in {context}: {e}")
|
|
66
66
|
|
|
67
67
|
# Parse SSE format: find first valid JSON in data lines
|
|
68
|
-
logger.
|
|
68
|
+
logger.trace(f"🔧 {context}: Parsing SSE format - looking for first valid JSON")
|
|
69
69
|
data_line_count = 0
|
|
70
70
|
first_valid_json = None
|
|
71
71
|
|
|
@@ -79,22 +79,24 @@ class SSEParser:
|
|
|
79
79
|
parsed_json = json.loads(data_content)
|
|
80
80
|
if first_valid_json is None:
|
|
81
81
|
first_valid_json = parsed_json
|
|
82
|
-
logger.
|
|
82
|
+
logger.trace(
|
|
83
|
+
f"🔧 {context}: Found first valid JSON in data line {data_line_count}"
|
|
84
|
+
)
|
|
83
85
|
except json.JSONDecodeError:
|
|
84
86
|
# Skip invalid JSON lines - this is expected behavior
|
|
85
|
-
logger.
|
|
87
|
+
logger.trace(
|
|
88
|
+
f"🔧 {context}: Skipping invalid JSON in data line {data_line_count}: {data_content[:50]}..."
|
|
89
|
+
)
|
|
86
90
|
continue
|
|
87
91
|
|
|
88
|
-
logger.
|
|
89
|
-
f"🔧 {context}: Processed {data_line_count} data lines"
|
|
90
|
-
)
|
|
92
|
+
logger.trace(f"🔧 {context}: Processed {data_line_count} data lines")
|
|
91
93
|
|
|
92
94
|
# Return first valid JSON found
|
|
93
95
|
if first_valid_json is None:
|
|
94
96
|
logger.error(f"🔧 {context}: No valid JSON found in SSE response")
|
|
95
|
-
raise RuntimeError(
|
|
97
|
+
raise RuntimeError("Could not parse SSE response from FastMCP")
|
|
96
98
|
|
|
97
|
-
logger.
|
|
99
|
+
logger.trace(
|
|
98
100
|
f"🔧 {context}: SSE parsing successful! Result type: {type(first_valid_json)}"
|
|
99
101
|
)
|
|
100
102
|
return first_valid_json
|
|
@@ -152,14 +154,14 @@ class SSEStreamProcessor:
|
|
|
152
154
|
Returns:
|
|
153
155
|
List of complete JSON objects found in this chunk
|
|
154
156
|
"""
|
|
155
|
-
self.logger.
|
|
157
|
+
self.logger.trace(
|
|
156
158
|
f"🌊 SSEStreamProcessor.process_chunk called for {self.context}, chunk size: {len(chunk_bytes)}"
|
|
157
159
|
)
|
|
158
160
|
|
|
159
161
|
try:
|
|
160
162
|
chunk_text = chunk_bytes.decode("utf-8")
|
|
161
163
|
self.buffer += chunk_text
|
|
162
|
-
self.logger.
|
|
164
|
+
self.logger.trace(
|
|
163
165
|
f"🌊 {self.context}: Buffer size after chunk: {len(self.buffer)}"
|
|
164
166
|
)
|
|
165
167
|
except UnicodeDecodeError:
|
|
@@ -190,7 +192,7 @@ class SSEStreamProcessor:
|
|
|
190
192
|
if parsed:
|
|
191
193
|
results.append(parsed)
|
|
192
194
|
|
|
193
|
-
self.logger.
|
|
195
|
+
self.logger.trace(
|
|
194
196
|
f"🌊 {self.context}: Processed {events_processed} complete SSE events, yielding {len(results)} JSON objects"
|
|
195
197
|
)
|
|
196
198
|
return results
|
|
@@ -27,7 +27,9 @@ class ExecutionTracer:
|
|
|
27
27
|
self.function_name = function_name
|
|
28
28
|
self.logger = logger_instance
|
|
29
29
|
self.start_time: float | None = None
|
|
30
|
-
self.trace_context: Any | None =
|
|
30
|
+
self.trace_context: Any | None = (
|
|
31
|
+
None # Parent's trace context (to restore after execution)
|
|
32
|
+
)
|
|
31
33
|
self.execution_metadata: dict = {}
|
|
32
34
|
|
|
33
35
|
def start_execution(
|
|
@@ -138,11 +140,34 @@ class ExecutionTracer:
|
|
|
138
140
|
# Save execution trace to Redis for distributed tracing storage
|
|
139
141
|
publish_trace_with_fallback(self.execution_metadata, self.logger)
|
|
140
142
|
|
|
143
|
+
# CRITICAL: Restore parent's trace context so sibling calls have correct parent
|
|
144
|
+
# Without this, subsequent calls become children of this span instead of siblings
|
|
145
|
+
self._restore_parent_context()
|
|
146
|
+
|
|
141
147
|
except Exception as e:
|
|
142
148
|
self.logger.warning(
|
|
143
149
|
f"Failed to complete execution logging for {self.function_name}: {e}"
|
|
144
150
|
)
|
|
145
151
|
|
|
152
|
+
def _restore_parent_context(self) -> None:
|
|
153
|
+
"""Restore the parent's trace context after this span completes.
|
|
154
|
+
|
|
155
|
+
This ensures sibling function calls share the same parent instead of
|
|
156
|
+
becoming nested children of each other.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
from .context import TraceContext
|
|
160
|
+
|
|
161
|
+
if self.trace_context:
|
|
162
|
+
# Restore to parent's context (the context that existed before this span)
|
|
163
|
+
TraceContext.set_current(
|
|
164
|
+
trace_id=self.trace_context.trace_id,
|
|
165
|
+
span_id=self.trace_context.span_id,
|
|
166
|
+
parent_span=self.trace_context.parent_span,
|
|
167
|
+
)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
self.logger.debug(f"Failed to restore parent trace context: {e}")
|
|
170
|
+
|
|
146
171
|
@staticmethod
|
|
147
172
|
def trace_function_execution(
|
|
148
173
|
func: Callable,
|
|
@@ -40,6 +40,9 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
|
|
|
40
40
|
if not is_tracing_enabled():
|
|
41
41
|
return await call_next(request)
|
|
42
42
|
|
|
43
|
+
self.logger.debug(
|
|
44
|
+
f"[TRACE] Processing request {request.method} {request.url.path}"
|
|
45
|
+
)
|
|
43
46
|
|
|
44
47
|
# Setup distributed tracing context from request headers
|
|
45
48
|
try:
|
|
@@ -50,7 +53,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
|
|
|
50
53
|
request
|
|
51
54
|
)
|
|
52
55
|
|
|
53
|
-
|
|
54
56
|
# Setup trace context for this request lifecycle
|
|
55
57
|
TraceContextHelper.setup_request_trace_context(trace_context, self.logger)
|
|
56
58
|
|
|
@@ -62,7 +64,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
|
|
|
62
64
|
route_name = self._extract_route_name(request)
|
|
63
65
|
start_time = time.time()
|
|
64
66
|
|
|
65
|
-
|
|
66
67
|
try:
|
|
67
68
|
# Process the request
|
|
68
69
|
response = await call_next(request)
|
|
@@ -83,7 +84,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
|
|
|
83
84
|
status_code=response.status_code,
|
|
84
85
|
)
|
|
85
86
|
|
|
86
|
-
|
|
87
87
|
return response
|
|
88
88
|
|
|
89
89
|
except Exception as e:
|
|
@@ -103,7 +103,6 @@ class FastAPITracingMiddleware(BaseHTTPMiddleware):
|
|
|
103
103
|
error=str(e),
|
|
104
104
|
)
|
|
105
105
|
|
|
106
|
-
|
|
107
106
|
raise # Re-raise the original exception
|
|
108
107
|
|
|
109
108
|
def _extract_route_name(self, request: Request) -> str:
|
|
@@ -11,6 +11,23 @@ from typing import Any, Optional
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def get_header_case_insensitive(headers_list: list, name: str) -> str:
|
|
15
|
+
"""Get header value case-insensitively from ASGI headers list.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
headers_list: List of (key, value) tuples with bytes
|
|
19
|
+
name: Header name to find (case-insensitive)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Header value as string, or empty string if not found
|
|
23
|
+
"""
|
|
24
|
+
name_lower = name.lower()
|
|
25
|
+
for key, value in headers_list:
|
|
26
|
+
if key.decode().lower() == name_lower:
|
|
27
|
+
return value.decode()
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
14
31
|
class TraceContextHelper:
|
|
15
32
|
"""Helper class to handle HTTP request trace context setup and distributed tracing."""
|
|
16
33
|
|
|
@@ -37,16 +54,18 @@ class TraceContextHelper:
|
|
|
37
54
|
extracted_trace_id = trace_context.get("trace_id")
|
|
38
55
|
if extracted_trace_id and extracted_trace_id.strip():
|
|
39
56
|
# EXISTING TRACE: This service is being called by another service
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
# Pass through the incoming parent_span directly - ExecutionTracer will create
|
|
58
|
+
# the actual function span as a child of this parent.
|
|
59
|
+
# This avoids creating an unpublished intermediate middleware span.
|
|
43
60
|
parent_span_id = trace_context.get("parent_span")
|
|
44
61
|
|
|
45
|
-
# Set context
|
|
62
|
+
# Set context with incoming parent_span as the current span
|
|
63
|
+
# When ExecutionTracer runs, it will create a child span with this as parent
|
|
46
64
|
TraceContext.set_current(
|
|
47
65
|
trace_id=extracted_trace_id,
|
|
48
|
-
span_id=
|
|
49
|
-
|
|
66
|
+
span_id=parent_span_id
|
|
67
|
+
or "", # Use parent as current (will be overwritten by ExecutionTracer)
|
|
68
|
+
parent_span=None, # No grandparent needed at this level
|
|
50
69
|
)
|
|
51
70
|
else:
|
|
52
71
|
# NEW ROOT TRACE: This service is the entry point (no incoming trace headers)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.14
|
|
4
4
|
Summary: Kubernetes-native platform for distributed MCP applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
|
|
6
6
|
Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
|