mcp-mesh 0.5.1__py3-none-any.whl → 0.5.3__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/dependency_injector.py +53 -99
- _mcp_mesh/engine/http_wrapper.py +21 -9
- _mcp_mesh/engine/unified_mcp_proxy.py +135 -83
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +8 -5
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +153 -0
- _mcp_mesh/shared/fastapi_middleware_manager.py +371 -0
- _mcp_mesh/tracing/agent_context_helper.py +13 -8
- _mcp_mesh/tracing/context.py +10 -4
- _mcp_mesh/tracing/execution_tracer.py +26 -43
- _mcp_mesh/tracing/fastapi_tracing_middleware.py +204 -0
- _mcp_mesh/tracing/redis_metadata_publisher.py +14 -34
- _mcp_mesh/tracing/trace_context_helper.py +14 -19
- _mcp_mesh/tracing/utils.py +137 -0
- {mcp_mesh-0.5.1.dist-info → mcp_mesh-0.5.3.dist-info}/METADATA +1 -1
- {mcp_mesh-0.5.1.dist-info → mcp_mesh-0.5.3.dist-info}/RECORD +19 -15
- mesh/decorators.py +35 -0
- {mcp_mesh-0.5.1.dist-info → mcp_mesh-0.5.3.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.5.1.dist-info → mcp_mesh-0.5.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,22 +5,19 @@ This class encapsulates all the execution logging logic to keep the dependency i
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
-
import os
|
|
9
8
|
import time
|
|
10
9
|
from collections.abc import Callable
|
|
11
10
|
from typing import Any, Optional
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Import shared utilities at module level to avoid circular imports during execution
|
|
13
|
+
from .utils import (
|
|
14
|
+
generate_span_id,
|
|
15
|
+
get_agent_metadata_with_fallback,
|
|
16
|
+
is_tracing_enabled,
|
|
17
|
+
publish_trace_with_fallback,
|
|
18
|
+
)
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
"""Check if distributed tracing is enabled via environment variable."""
|
|
18
|
-
return os.getenv("MCP_MESH_DISTRIBUTED_TRACING_ENABLED", "false").lower() in (
|
|
19
|
-
"true",
|
|
20
|
-
"1",
|
|
21
|
-
"yes",
|
|
22
|
-
"on",
|
|
23
|
-
)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
24
21
|
|
|
25
22
|
|
|
26
23
|
class ExecutionTracer:
|
|
@@ -60,30 +57,24 @@ class ExecutionTracer:
|
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
# Add agent context metadata for distributed tracing
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
agent_metadata = get_trace_metadata()
|
|
67
|
-
self.execution_metadata.update(agent_metadata)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
# Never fail execution due to agent metadata collection
|
|
70
|
-
self.logger.debug(f"Failed to get agent metadata: {e}")
|
|
71
|
-
# Add minimal fallback metadata
|
|
72
|
-
self.execution_metadata.update(
|
|
73
|
-
{
|
|
74
|
-
"agent_id": "unknown",
|
|
75
|
-
"agent_name": "unknown",
|
|
76
|
-
"agent_hostname": "unknown",
|
|
77
|
-
"agent_ip": "unknown",
|
|
78
|
-
}
|
|
79
|
-
)
|
|
60
|
+
agent_metadata = get_agent_metadata_with_fallback(self.logger)
|
|
61
|
+
self.execution_metadata.update(agent_metadata)
|
|
80
62
|
|
|
81
63
|
if self.trace_context:
|
|
64
|
+
# Generate a new child span ID for this function execution
|
|
65
|
+
# Keep the same trace_id but create unique span_id per function call
|
|
66
|
+
function_span_id = generate_span_id()
|
|
67
|
+
|
|
68
|
+
|
|
82
69
|
self.execution_metadata.update(
|
|
83
70
|
{
|
|
84
71
|
"trace_id": self.trace_context.trace_id,
|
|
85
|
-
"span_id":
|
|
86
|
-
"parent_span":
|
|
72
|
+
"span_id": function_span_id, # New child span for this function
|
|
73
|
+
"parent_span": (
|
|
74
|
+
self.trace_context.span_id
|
|
75
|
+
if self.trace_context.parent_span is not None
|
|
76
|
+
else None
|
|
77
|
+
), # HTTP middleware span becomes parent only if not root
|
|
87
78
|
}
|
|
88
79
|
)
|
|
89
80
|
|
|
@@ -117,15 +108,7 @@ class ExecutionTracer:
|
|
|
117
108
|
)
|
|
118
109
|
|
|
119
110
|
# Save execution trace to Redis for distributed tracing storage
|
|
120
|
-
|
|
121
|
-
from .redis_metadata_publisher import get_trace_publisher
|
|
122
|
-
|
|
123
|
-
publisher = get_trace_publisher()
|
|
124
|
-
if publisher.is_available:
|
|
125
|
-
publisher.publish_execution_trace(self.execution_metadata)
|
|
126
|
-
except Exception as e:
|
|
127
|
-
# Never fail agent operations due to trace publishing
|
|
128
|
-
pass
|
|
111
|
+
publish_trace_with_fallback(self.execution_metadata, self.logger)
|
|
129
112
|
|
|
130
113
|
except Exception as e:
|
|
131
114
|
self.logger.warning(
|
|
@@ -149,7 +132,7 @@ class ExecutionTracer:
|
|
|
149
132
|
exception handling and cleanup. If tracing is disabled, calls function directly.
|
|
150
133
|
"""
|
|
151
134
|
# If tracing is disabled, call function directly without any overhead
|
|
152
|
-
if not
|
|
135
|
+
if not is_tracing_enabled():
|
|
153
136
|
return func(*args, **kwargs)
|
|
154
137
|
|
|
155
138
|
tracer = ExecutionTracer(func.__name__, logger_instance)
|
|
@@ -176,7 +159,7 @@ class ExecutionTracer:
|
|
|
176
159
|
If tracing is disabled, calls function directly.
|
|
177
160
|
"""
|
|
178
161
|
# If tracing is disabled, call function directly without any overhead
|
|
179
|
-
if not
|
|
162
|
+
if not is_tracing_enabled():
|
|
180
163
|
return func(*args, **kwargs)
|
|
181
164
|
|
|
182
165
|
tracer = ExecutionTracer(func.__name__, logger_instance)
|
|
@@ -209,9 +192,9 @@ class ExecutionTracer:
|
|
|
209
192
|
exception handling and cleanup. If tracing is disabled, calls function directly.
|
|
210
193
|
"""
|
|
211
194
|
import inspect
|
|
212
|
-
|
|
195
|
+
|
|
213
196
|
# If tracing is disabled, call function directly without any overhead
|
|
214
|
-
if not
|
|
197
|
+
if not is_tracing_enabled():
|
|
215
198
|
if inspect.iscoroutinefunction(func):
|
|
216
199
|
return await func(*args, **kwargs)
|
|
217
200
|
else:
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Tracing Middleware - Dedicated route-level tracing for FastAPI applications.
|
|
3
|
+
|
|
4
|
+
This middleware provides clean separation from MCP agent tracing while ensuring
|
|
5
|
+
FastAPI routes get comprehensive execution tracking and distributed tracing support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
|
+
from starlette.requests import Request
|
|
14
|
+
from starlette.responses import Response
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FastAPITracingMiddleware(BaseHTTPMiddleware):
|
|
20
|
+
"""Dedicated tracing middleware for FastAPI routes.
|
|
21
|
+
|
|
22
|
+
Provides route-level execution tracing with:
|
|
23
|
+
- Performance monitoring (request duration)
|
|
24
|
+
- Distributed tracing context setup
|
|
25
|
+
- Redis trace publishing
|
|
26
|
+
- Route name extraction
|
|
27
|
+
- Zero overhead when tracing disabled
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, app, logger_instance: logging.Logger = None):
|
|
31
|
+
super().__init__(app)
|
|
32
|
+
self.logger = logger_instance or logger
|
|
33
|
+
|
|
34
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
35
|
+
"""Process FastAPI request with comprehensive tracing."""
|
|
36
|
+
|
|
37
|
+
# If tracing is disabled, process request directly with zero overhead
|
|
38
|
+
from .utils import is_tracing_enabled
|
|
39
|
+
|
|
40
|
+
if not is_tracing_enabled():
|
|
41
|
+
return await call_next(request)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Setup distributed tracing context from request headers
|
|
45
|
+
try:
|
|
46
|
+
from .trace_context_helper import TraceContextHelper
|
|
47
|
+
|
|
48
|
+
# Extract trace context from request headers
|
|
49
|
+
trace_context = await TraceContextHelper.extract_trace_context_from_request(
|
|
50
|
+
request
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Setup trace context for this request lifecycle
|
|
55
|
+
TraceContextHelper.setup_request_trace_context(trace_context, self.logger)
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
# Never fail request due to tracing issues
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Extract route information for tracing
|
|
62
|
+
route_name = self._extract_route_name(request)
|
|
63
|
+
start_time = time.time()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Process the request
|
|
68
|
+
response = await call_next(request)
|
|
69
|
+
|
|
70
|
+
# Calculate performance metrics
|
|
71
|
+
end_time = time.time()
|
|
72
|
+
duration_ms = round((end_time - start_time) * 1000, 2)
|
|
73
|
+
|
|
74
|
+
# Publish route execution trace
|
|
75
|
+
self._publish_route_trace(
|
|
76
|
+
route_name=route_name,
|
|
77
|
+
request_method=request.method,
|
|
78
|
+
request_path=str(request.url.path),
|
|
79
|
+
start_time=start_time,
|
|
80
|
+
end_time=end_time,
|
|
81
|
+
duration_ms=duration_ms,
|
|
82
|
+
success=True,
|
|
83
|
+
status_code=response.status_code,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
return response
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
# Calculate performance metrics for failed request
|
|
91
|
+
end_time = time.time()
|
|
92
|
+
duration_ms = round((end_time - start_time) * 1000, 2)
|
|
93
|
+
|
|
94
|
+
# Publish failed route execution trace
|
|
95
|
+
self._publish_route_trace(
|
|
96
|
+
route_name=route_name,
|
|
97
|
+
request_method=request.method,
|
|
98
|
+
request_path=str(request.url.path),
|
|
99
|
+
start_time=start_time,
|
|
100
|
+
end_time=end_time,
|
|
101
|
+
duration_ms=duration_ms,
|
|
102
|
+
success=False,
|
|
103
|
+
error=str(e),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
raise # Re-raise the original exception
|
|
108
|
+
|
|
109
|
+
def _extract_route_name(self, request: Request) -> str:
|
|
110
|
+
"""Extract meaningful route name for tracing."""
|
|
111
|
+
try:
|
|
112
|
+
# Try to get route from FastAPI request state
|
|
113
|
+
if hasattr(request, "scope") and "route" in request.scope:
|
|
114
|
+
route = request.scope["route"]
|
|
115
|
+
if hasattr(route, "path"):
|
|
116
|
+
return f"{request.method} {route.path}"
|
|
117
|
+
|
|
118
|
+
# Fallback to URL path
|
|
119
|
+
path = str(request.url.path)
|
|
120
|
+
return f"{request.method} {path}"
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.logger.debug(f"Failed to extract route name: {e}")
|
|
124
|
+
return f"{request.method} {request.url.path}"
|
|
125
|
+
|
|
126
|
+
def _publish_route_trace(
|
|
127
|
+
self,
|
|
128
|
+
route_name: str,
|
|
129
|
+
request_method: str,
|
|
130
|
+
request_path: str,
|
|
131
|
+
start_time: float,
|
|
132
|
+
end_time: float,
|
|
133
|
+
duration_ms: float,
|
|
134
|
+
success: bool,
|
|
135
|
+
status_code: int = None,
|
|
136
|
+
error: str = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Publish route execution trace to Redis."""
|
|
139
|
+
try:
|
|
140
|
+
from .context import TraceContext
|
|
141
|
+
from .redis_metadata_publisher import get_trace_publisher
|
|
142
|
+
|
|
143
|
+
# Get current trace context
|
|
144
|
+
current_trace = TraceContext.get_current()
|
|
145
|
+
|
|
146
|
+
if not current_trace:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# Generate a unique span ID for this route execution
|
|
150
|
+
from .utils import generate_span_id
|
|
151
|
+
|
|
152
|
+
route_span_id = generate_span_id()
|
|
153
|
+
|
|
154
|
+
# Build route execution metadata
|
|
155
|
+
execution_metadata = {
|
|
156
|
+
"function_name": route_name,
|
|
157
|
+
"route_name": route_name,
|
|
158
|
+
"request_method": request_method,
|
|
159
|
+
"request_path": request_path,
|
|
160
|
+
"start_time": start_time,
|
|
161
|
+
"end_time": end_time,
|
|
162
|
+
"duration_ms": duration_ms,
|
|
163
|
+
"success": success,
|
|
164
|
+
"error": error,
|
|
165
|
+
"status_code": status_code,
|
|
166
|
+
"trace_id": current_trace.trace_id,
|
|
167
|
+
"span_id": route_span_id,
|
|
168
|
+
"parent_span": (
|
|
169
|
+
current_trace.span_id
|
|
170
|
+
if current_trace.parent_span is not None
|
|
171
|
+
else None
|
|
172
|
+
),
|
|
173
|
+
"call_context": "fastapi_route_execution",
|
|
174
|
+
"agent_type": "fastapi_app",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Add agent context metadata
|
|
178
|
+
from .utils import get_agent_metadata_with_fallback
|
|
179
|
+
|
|
180
|
+
agent_metadata = get_agent_metadata_with_fallback(self.logger)
|
|
181
|
+
# Override with FastAPI-specific values if fallback was used
|
|
182
|
+
if agent_metadata.get("agent_id") == "unknown":
|
|
183
|
+
agent_metadata.update(
|
|
184
|
+
{"agent_id": "fastapi_app", "agent_name": "fastapi_app"}
|
|
185
|
+
)
|
|
186
|
+
execution_metadata.update(agent_metadata)
|
|
187
|
+
|
|
188
|
+
# Publish to Redis
|
|
189
|
+
from .utils import publish_trace_with_fallback
|
|
190
|
+
|
|
191
|
+
publish_trace_with_fallback(execution_metadata, self.logger)
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
# Never fail requests due to trace publishing
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_fastapi_tracing_middleware() -> FastAPITracingMiddleware:
|
|
199
|
+
"""Get FastAPI tracing middleware instance.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Configured FastAPITracingMiddleware instance
|
|
203
|
+
"""
|
|
204
|
+
return FastAPITracingMiddleware
|
|
@@ -39,18 +39,14 @@ class RedisTracePublisher:
|
|
|
39
39
|
|
|
40
40
|
def _is_tracing_enabled(self) -> bool:
|
|
41
41
|
"""Check if distributed tracing is enabled via environment variable."""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"yes",
|
|
46
|
-
"on",
|
|
47
|
-
)
|
|
42
|
+
from .utils import is_tracing_enabled
|
|
43
|
+
|
|
44
|
+
return is_tracing_enabled()
|
|
48
45
|
|
|
49
46
|
def _init_redis(self):
|
|
50
47
|
"""Initialize Redis connection with graceful fallback (following session storage pattern)."""
|
|
51
48
|
if not self._tracing_enabled:
|
|
52
49
|
self._available = False
|
|
53
|
-
logger.info("Distributed tracing: disabled")
|
|
54
50
|
return
|
|
55
51
|
|
|
56
52
|
logger.info("Distributed tracing: enabled")
|
|
@@ -76,33 +72,23 @@ class RedisTracePublisher:
|
|
|
76
72
|
return # Silent no-op when Redis unavailable
|
|
77
73
|
|
|
78
74
|
try:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
redis_trace_data =
|
|
87
|
-
for key, value in trace_data.items():
|
|
88
|
-
if isinstance(value, (list, dict)):
|
|
89
|
-
# Convert lists and dicts to JSON strings
|
|
90
|
-
import json
|
|
91
|
-
|
|
92
|
-
redis_trace_data[key] = json.dumps(value)
|
|
93
|
-
elif value is None:
|
|
94
|
-
redis_trace_data[key] = "null"
|
|
95
|
-
else:
|
|
96
|
-
# Keep simple types as-is (str, int, float, bool)
|
|
97
|
-
redis_trace_data[key] = str(value)
|
|
75
|
+
function_name = trace_data.get("function_name", "unknown")
|
|
76
|
+
trace_id = trace_data.get("trace_id", "no-trace-id")
|
|
77
|
+
|
|
78
|
+
# Add timestamp and convert for Redis storage
|
|
79
|
+
from .utils import add_timestamp_if_missing, convert_for_redis_storage
|
|
80
|
+
|
|
81
|
+
add_timestamp_if_missing(trace_data)
|
|
82
|
+
redis_trace_data = convert_for_redis_storage(trace_data)
|
|
98
83
|
|
|
99
84
|
# Publish to Redis Stream
|
|
100
85
|
if self._redis_client:
|
|
101
|
-
self._redis_client.xadd(self.stream_name, redis_trace_data)
|
|
86
|
+
message_id = self._redis_client.xadd(self.stream_name, redis_trace_data)
|
|
87
|
+
logger.debug(f"Published trace for '{function_name}' to Redis stream")
|
|
102
88
|
|
|
103
89
|
except Exception as e:
|
|
104
90
|
# Non-blocking - never fail agent operations due to trace publishing
|
|
105
|
-
|
|
91
|
+
pass
|
|
106
92
|
|
|
107
93
|
@property
|
|
108
94
|
def is_available(self) -> bool:
|
|
@@ -151,9 +137,3 @@ def get_trace_publisher() -> RedisTracePublisher:
|
|
|
151
137
|
if _trace_publisher is None:
|
|
152
138
|
_trace_publisher = RedisTracePublisher()
|
|
153
139
|
return _trace_publisher
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Legacy alias for backward compatibility
|
|
157
|
-
def get_metadata_publisher() -> RedisTracePublisher:
|
|
158
|
-
"""Legacy alias for get_trace_publisher - use get_trace_publisher instead."""
|
|
159
|
-
return get_trace_publisher()
|
|
@@ -4,24 +4,13 @@ Trace Context Helper - Helper class for HTTP request trace context setup.
|
|
|
4
4
|
This class encapsulates all the trace context setup logic to keep HTTP wrappers clean.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import logging
|
|
8
|
-
import os
|
|
9
|
-
import uuid
|
|
10
9
|
from typing import Any, Optional
|
|
11
10
|
|
|
12
11
|
logger = logging.getLogger(__name__)
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
def _is_tracing_enabled() -> bool:
|
|
16
|
-
"""Check if distributed tracing is enabled via environment variable."""
|
|
17
|
-
return os.getenv("MCP_MESH_DISTRIBUTED_TRACING_ENABLED", "false").lower() in (
|
|
18
|
-
"true",
|
|
19
|
-
"1",
|
|
20
|
-
"yes",
|
|
21
|
-
"on",
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
14
|
class TraceContextHelper:
|
|
26
15
|
"""Helper class to handle HTTP request trace context setup and distributed tracing."""
|
|
27
16
|
|
|
@@ -36,7 +25,9 @@ class TraceContextHelper:
|
|
|
36
25
|
If tracing is disabled, this function returns immediately without setting up trace context.
|
|
37
26
|
"""
|
|
38
27
|
# If tracing is disabled, skip all trace context setup
|
|
39
|
-
|
|
28
|
+
from .utils import is_tracing_enabled
|
|
29
|
+
|
|
30
|
+
if not is_tracing_enabled():
|
|
40
31
|
return
|
|
41
32
|
|
|
42
33
|
try:
|
|
@@ -46,7 +37,9 @@ class TraceContextHelper:
|
|
|
46
37
|
extracted_trace_id = trace_context.get("trace_id")
|
|
47
38
|
if extracted_trace_id and extracted_trace_id.strip():
|
|
48
39
|
# EXISTING TRACE: This service is being called by another service
|
|
49
|
-
|
|
40
|
+
from .utils import generate_span_id
|
|
41
|
+
|
|
42
|
+
current_span_id = generate_span_id()
|
|
50
43
|
parent_span_id = trace_context.get("parent_span")
|
|
51
44
|
|
|
52
45
|
# Set context that will be used throughout request lifecycle
|
|
@@ -57,8 +50,10 @@ class TraceContextHelper:
|
|
|
57
50
|
)
|
|
58
51
|
else:
|
|
59
52
|
# NEW ROOT TRACE: This service is the entry point (no incoming trace headers)
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
from .utils import generate_span_id, generate_trace_id
|
|
54
|
+
|
|
55
|
+
root_trace_id = generate_trace_id()
|
|
56
|
+
root_span_id = generate_span_id()
|
|
62
57
|
|
|
63
58
|
# Set context for root trace (no parent_span)
|
|
64
59
|
TraceContext.set_current(
|
|
@@ -101,8 +96,6 @@ class TraceContextHelper:
|
|
|
101
96
|
# Try extracting from JSON-RPC body as fallback
|
|
102
97
|
if not trace_id:
|
|
103
98
|
try:
|
|
104
|
-
import json
|
|
105
|
-
|
|
106
99
|
body = await request.body()
|
|
107
100
|
if body:
|
|
108
101
|
payload = json.loads(body.decode("utf-8"))
|
|
@@ -156,7 +149,9 @@ class TraceContextHelper:
|
|
|
156
149
|
If tracing is disabled, this function returns immediately without modifying headers.
|
|
157
150
|
"""
|
|
158
151
|
# If tracing is disabled, skip all trace header injection
|
|
159
|
-
|
|
152
|
+
from .utils import is_tracing_enabled
|
|
153
|
+
|
|
154
|
+
if not is_tracing_enabled():
|
|
160
155
|
return
|
|
161
156
|
|
|
162
157
|
try:
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared tracing utilities for MCP Mesh distributed tracing.
|
|
3
|
+
|
|
4
|
+
Provides common functions used across multiple tracing modules to reduce code duplication
|
|
5
|
+
and maintain consistency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_tracing_enabled() -> bool:
|
|
19
|
+
"""Check if distributed tracing is enabled via environment variable.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True if tracing is enabled, False otherwise
|
|
23
|
+
"""
|
|
24
|
+
return os.getenv("MCP_MESH_DISTRIBUTED_TRACING_ENABLED", "false").lower() in (
|
|
25
|
+
"true",
|
|
26
|
+
"1",
|
|
27
|
+
"yes",
|
|
28
|
+
"on",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def generate_span_id() -> str:
|
|
33
|
+
"""Generate a unique span ID for tracing.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
UUID string for span identification
|
|
37
|
+
"""
|
|
38
|
+
return str(uuid.uuid4())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_trace_id() -> str:
|
|
42
|
+
"""Generate a unique trace ID for tracing.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
UUID string for trace identification
|
|
46
|
+
"""
|
|
47
|
+
return str(uuid.uuid4())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_agent_metadata_with_fallback(logger_instance: logging.Logger) -> dict[str, Any]:
|
|
51
|
+
"""Get agent context metadata with graceful fallback.
|
|
52
|
+
|
|
53
|
+
Attempts to retrieve agent metadata from the context helper, falling back
|
|
54
|
+
to minimal defaults if unavailable. Never fails execution.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
logger_instance: Logger for debug messages
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dictionary containing agent metadata
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
from .agent_context_helper import get_trace_metadata
|
|
64
|
+
|
|
65
|
+
return get_trace_metadata()
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# Never fail execution due to agent metadata collection
|
|
68
|
+
logger_instance.debug(f"Failed to get agent metadata: {e}")
|
|
69
|
+
# Return minimal fallback metadata
|
|
70
|
+
return {
|
|
71
|
+
"agent_id": "unknown",
|
|
72
|
+
"agent_name": "unknown",
|
|
73
|
+
"agent_hostname": "unknown",
|
|
74
|
+
"agent_ip": "unknown",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def publish_trace_with_fallback(
|
|
79
|
+
trace_data: dict[str, Any], logger_instance: logging.Logger
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Publish trace data to Redis with graceful fallback.
|
|
82
|
+
|
|
83
|
+
Attempts to publish trace data to Redis, silently handling failures
|
|
84
|
+
to ensure trace publishing never breaks application execution.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
trace_data: Trace metadata to publish
|
|
88
|
+
logger_instance: Logger for debug messages
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
from .redis_metadata_publisher import get_trace_publisher
|
|
92
|
+
|
|
93
|
+
publisher = get_trace_publisher()
|
|
94
|
+
if publisher.is_available:
|
|
95
|
+
publisher.publish_execution_trace(trace_data)
|
|
96
|
+
pass
|
|
97
|
+
else:
|
|
98
|
+
pass
|
|
99
|
+
except Exception as e:
|
|
100
|
+
# Never fail agent operations due to trace publishing
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def add_timestamp_if_missing(trace_data: dict[str, Any]) -> None:
|
|
105
|
+
"""Add published_at timestamp to trace data if not present.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
trace_data: Trace data dictionary to modify in-place
|
|
109
|
+
"""
|
|
110
|
+
if "published_at" not in trace_data:
|
|
111
|
+
trace_data["published_at"] = time.time()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def convert_for_redis_storage(trace_data: dict[str, Any]) -> dict[str, str]:
|
|
115
|
+
"""Convert trace data for Redis Stream storage.
|
|
116
|
+
|
|
117
|
+
Converts complex types (lists, dicts) to JSON strings and handles None values
|
|
118
|
+
for proper Redis Stream storage.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
trace_data: Original trace data with mixed types
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary with all values converted to strings suitable for Redis
|
|
125
|
+
"""
|
|
126
|
+
redis_trace_data = {}
|
|
127
|
+
for key, value in trace_data.items():
|
|
128
|
+
if isinstance(value, (list, dict)):
|
|
129
|
+
# Convert lists and dicts to JSON strings
|
|
130
|
+
redis_trace_data[key] = json.dumps(value)
|
|
131
|
+
elif value is None:
|
|
132
|
+
redis_trace_data[key] = "null"
|
|
133
|
+
else:
|
|
134
|
+
# Keep simple types as-is (str, int, float, bool)
|
|
135
|
+
redis_trace_data[key] = str(value)
|
|
136
|
+
|
|
137
|
+
return redis_trace_data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|