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.
@@ -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
- logger = logging.getLogger(__name__)
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
- def _is_tracing_enabled() -> bool:
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
- try:
64
- from .agent_context_helper import get_trace_metadata
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": self.trace_context.span_id,
86
- "parent_span": self.trace_context.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
- try:
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 _is_tracing_enabled():
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 _is_tracing_enabled():
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 _is_tracing_enabled():
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
- return os.getenv("MCP_MESH_DISTRIBUTED_TRACING_ENABLED", "false").lower() in (
43
- "true",
44
- "1",
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
- # Add timestamp to trace data if not present
80
- if "published_at" not in trace_data:
81
- import time
82
-
83
- trace_data["published_at"] = time.time()
84
-
85
- # Convert complex types to JSON strings for Redis Stream storage
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
- logger.warning(f"Failed to publish execution trace to Redis: {e}")
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
- if not _is_tracing_enabled():
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
- current_span_id = str(uuid.uuid4())
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
- root_trace_id = str(uuid.uuid4())
61
- root_span_id = str(uuid.uuid4())
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
- if not _is_tracing_enabled():
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.1
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