golf-mcp 0.2.16__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.
- golf/__init__.py +1 -0
- golf/auth/__init__.py +277 -0
- golf/auth/api_key.py +73 -0
- golf/auth/factory.py +360 -0
- golf/auth/helpers.py +175 -0
- golf/auth/providers.py +586 -0
- golf/auth/registry.py +256 -0
- golf/cli/__init__.py +1 -0
- golf/cli/branding.py +191 -0
- golf/cli/main.py +377 -0
- golf/commands/__init__.py +5 -0
- golf/commands/build.py +81 -0
- golf/commands/init.py +290 -0
- golf/commands/run.py +137 -0
- golf/core/__init__.py +1 -0
- golf/core/builder.py +1884 -0
- golf/core/builder_auth.py +209 -0
- golf/core/builder_metrics.py +221 -0
- golf/core/builder_telemetry.py +99 -0
- golf/core/config.py +199 -0
- golf/core/parser.py +1085 -0
- golf/core/telemetry.py +492 -0
- golf/core/transformer.py +231 -0
- golf/examples/__init__.py +0 -0
- golf/examples/basic/.env.example +4 -0
- golf/examples/basic/README.md +133 -0
- golf/examples/basic/auth.py +76 -0
- golf/examples/basic/golf.json +5 -0
- golf/examples/basic/prompts/welcome.py +27 -0
- golf/examples/basic/resources/current_time.py +34 -0
- golf/examples/basic/resources/info.py +28 -0
- golf/examples/basic/resources/weather/city.py +46 -0
- golf/examples/basic/resources/weather/client.py +48 -0
- golf/examples/basic/resources/weather/current.py +36 -0
- golf/examples/basic/resources/weather/forecast.py +36 -0
- golf/examples/basic/tools/calculator.py +94 -0
- golf/examples/basic/tools/say/hello.py +65 -0
- golf/metrics/__init__.py +10 -0
- golf/metrics/collector.py +320 -0
- golf/metrics/registry.py +12 -0
- golf/telemetry/__init__.py +23 -0
- golf/telemetry/instrumentation.py +1402 -0
- golf/utilities/__init__.py +12 -0
- golf/utilities/context.py +53 -0
- golf/utilities/elicitation.py +170 -0
- golf/utilities/sampling.py +221 -0
- golf_mcp-0.2.16.dist-info/METADATA +262 -0
- golf_mcp-0.2.16.dist-info/RECORD +52 -0
- golf_mcp-0.2.16.dist-info/WHEEL +5 -0
- golf_mcp-0.2.16.dist-info/entry_points.txt +2 -0
- golf_mcp-0.2.16.dist-info/licenses/LICENSE +201 -0
- golf_mcp-0.2.16.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1402 @@
|
|
|
1
|
+
"""Component-level OpenTelemetry instrumentation for Golf-built servers."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import json
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
from collections.abc import AsyncGenerator
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
|
|
15
|
+
from opentelemetry import baggage, trace
|
|
16
|
+
|
|
17
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
18
|
+
from opentelemetry.sdk.resources import Resource
|
|
19
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
20
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
21
|
+
from opentelemetry.trace import Status, StatusCode
|
|
22
|
+
|
|
23
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
# Global tracer instance
|
|
28
|
+
_tracer: trace.Tracer | None = None
|
|
29
|
+
_provider: TracerProvider | None = None
|
|
30
|
+
_detailed_tracing_enabled: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _safe_serialize(data: Any, max_length: int = 1000) -> str | None:
|
|
34
|
+
"""Safely serialize data to string with length limit."""
|
|
35
|
+
try:
|
|
36
|
+
if isinstance(data, str):
|
|
37
|
+
serialized = data
|
|
38
|
+
else:
|
|
39
|
+
serialized = json.dumps(data, default=str, ensure_ascii=False)
|
|
40
|
+
|
|
41
|
+
if len(serialized) > max_length:
|
|
42
|
+
return serialized[:max_length] + "..." + f" (truncated from {len(serialized)} chars)"
|
|
43
|
+
return serialized
|
|
44
|
+
except (TypeError, ValueError):
|
|
45
|
+
# Fallback for non-serializable objects
|
|
46
|
+
try:
|
|
47
|
+
return str(data)[:max_length] + "..." if len(str(data)) > max_length else str(data)
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def set_detailed_tracing(enabled: bool) -> None:
|
|
53
|
+
"""Enable or disable detailed tracing with input/output capture."""
|
|
54
|
+
global _detailed_tracing_enabled
|
|
55
|
+
_detailed_tracing_enabled = enabled
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | None:
|
|
59
|
+
"""Initialize OpenTelemetry with environment-based configuration.
|
|
60
|
+
|
|
61
|
+
Returns None if required environment variables are not set.
|
|
62
|
+
"""
|
|
63
|
+
global _provider
|
|
64
|
+
|
|
65
|
+
# Check for required environment variables based on exporter type
|
|
66
|
+
exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
|
|
67
|
+
|
|
68
|
+
# For OTLP HTTP exporter, check if endpoint is configured
|
|
69
|
+
if exporter_type == "otlp_http":
|
|
70
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
71
|
+
if not endpoint:
|
|
72
|
+
print(
|
|
73
|
+
"[WARNING] OpenTelemetry tracing is disabled: "
|
|
74
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter"
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Create resource with service information
|
|
79
|
+
resource_attributes = {
|
|
80
|
+
"service.name": os.environ.get("OTEL_SERVICE_NAME", service_name),
|
|
81
|
+
"service.version": os.environ.get("SERVICE_VERSION", "1.0.0"),
|
|
82
|
+
"service.instance.id": os.environ.get("SERVICE_INSTANCE_ID", "default"),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resource = Resource.create(resource_attributes)
|
|
86
|
+
|
|
87
|
+
# Create provider
|
|
88
|
+
provider = TracerProvider(resource=resource)
|
|
89
|
+
|
|
90
|
+
# Configure exporter based on type
|
|
91
|
+
try:
|
|
92
|
+
if exporter_type == "otlp_http":
|
|
93
|
+
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
|
|
94
|
+
headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
95
|
+
|
|
96
|
+
# Parse headers if provided
|
|
97
|
+
header_dict = {}
|
|
98
|
+
if headers:
|
|
99
|
+
for header in headers.split(","):
|
|
100
|
+
if "=" in header:
|
|
101
|
+
key, value = header.split("=", 1)
|
|
102
|
+
header_dict[key.strip()] = value.strip()
|
|
103
|
+
|
|
104
|
+
exporter = OTLPSpanExporter(endpoint=endpoint, headers=header_dict if header_dict else None)
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
# Default to console exporter
|
|
108
|
+
exporter = ConsoleSpanExporter(out=sys.stderr)
|
|
109
|
+
except Exception:
|
|
110
|
+
import traceback
|
|
111
|
+
|
|
112
|
+
traceback.print_exc()
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
# Add batch processor for better performance
|
|
116
|
+
try:
|
|
117
|
+
processor = BatchSpanProcessor(
|
|
118
|
+
exporter,
|
|
119
|
+
max_queue_size=2048,
|
|
120
|
+
schedule_delay_millis=1000, # Export every 1 second instead of
|
|
121
|
+
# default 5 seconds
|
|
122
|
+
max_export_batch_size=512,
|
|
123
|
+
export_timeout_millis=5000,
|
|
124
|
+
)
|
|
125
|
+
provider.add_span_processor(processor)
|
|
126
|
+
except Exception:
|
|
127
|
+
import traceback
|
|
128
|
+
|
|
129
|
+
traceback.print_exc()
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
# Set as global provider
|
|
133
|
+
try:
|
|
134
|
+
# Check if a provider is already set to avoid the warning
|
|
135
|
+
existing_provider = trace.get_tracer_provider()
|
|
136
|
+
if existing_provider is None or str(type(existing_provider).__name__) == "ProxyTracerProvider":
|
|
137
|
+
# Only set if no provider exists or it's the default proxy provider
|
|
138
|
+
trace.set_tracer_provider(provider)
|
|
139
|
+
_provider = provider
|
|
140
|
+
except Exception:
|
|
141
|
+
import traceback
|
|
142
|
+
|
|
143
|
+
traceback.print_exc()
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
return provider
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_tracer() -> trace.Tracer:
|
|
150
|
+
"""Get or create the global tracer instance."""
|
|
151
|
+
global _tracer, _provider
|
|
152
|
+
|
|
153
|
+
# If no provider is set, telemetry is disabled - return no-op tracer
|
|
154
|
+
if _provider is None:
|
|
155
|
+
return trace.get_tracer("golf.mcp.components.noop", "1.0.0")
|
|
156
|
+
|
|
157
|
+
if _tracer is None:
|
|
158
|
+
_tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
|
|
159
|
+
return _tracer
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
163
|
+
"""Instrument a tool function with OpenTelemetry tracing."""
|
|
164
|
+
global _provider
|
|
165
|
+
|
|
166
|
+
# If telemetry is disabled, return the original function
|
|
167
|
+
if _provider is None:
|
|
168
|
+
return func
|
|
169
|
+
|
|
170
|
+
tracer = get_tracer()
|
|
171
|
+
|
|
172
|
+
@functools.wraps(func)
|
|
173
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
174
|
+
# Record metrics timing
|
|
175
|
+
import time
|
|
176
|
+
|
|
177
|
+
start_time = time.time()
|
|
178
|
+
|
|
179
|
+
# Create a more descriptive span name
|
|
180
|
+
span_name = f"mcp.tool.{tool_name}.execute"
|
|
181
|
+
|
|
182
|
+
# start_as_current_span automatically uses the current context and manages it
|
|
183
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
184
|
+
# Add essential attributes only
|
|
185
|
+
span.set_attribute("mcp.component.type", "tool")
|
|
186
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
187
|
+
span.set_attribute(
|
|
188
|
+
"mcp.tool.module",
|
|
189
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Add minimal execution context
|
|
193
|
+
if args or kwargs:
|
|
194
|
+
span.set_attribute("mcp.execution.has_params", True)
|
|
195
|
+
|
|
196
|
+
# Capture inputs if detailed tracing is enabled
|
|
197
|
+
if _detailed_tracing_enabled and (args or kwargs):
|
|
198
|
+
input_data = {"args": args, "kwargs": kwargs} if args or kwargs else None
|
|
199
|
+
if input_data:
|
|
200
|
+
input_str = _safe_serialize(input_data)
|
|
201
|
+
if input_str:
|
|
202
|
+
span.set_attribute("mcp.tool.input", input_str)
|
|
203
|
+
|
|
204
|
+
# Extract Context parameter if present
|
|
205
|
+
ctx = kwargs.get("ctx")
|
|
206
|
+
if ctx:
|
|
207
|
+
# Only extract known MCP context attributes
|
|
208
|
+
ctx_attrs = [
|
|
209
|
+
"request_id",
|
|
210
|
+
"session_id",
|
|
211
|
+
"client_id",
|
|
212
|
+
"user_id",
|
|
213
|
+
"tenant_id",
|
|
214
|
+
]
|
|
215
|
+
for attr in ctx_attrs:
|
|
216
|
+
if hasattr(ctx, attr):
|
|
217
|
+
value = getattr(ctx, attr)
|
|
218
|
+
if value is not None:
|
|
219
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
220
|
+
|
|
221
|
+
# Also check baggage for session ID
|
|
222
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
223
|
+
if session_id_from_baggage:
|
|
224
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
225
|
+
|
|
226
|
+
# Add event for tool execution start
|
|
227
|
+
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
result = await func(*args, **kwargs)
|
|
231
|
+
span.set_status(Status(StatusCode.OK))
|
|
232
|
+
|
|
233
|
+
# Add event for successful completion
|
|
234
|
+
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
235
|
+
|
|
236
|
+
# Record metrics for successful execution
|
|
237
|
+
try:
|
|
238
|
+
from golf.metrics import get_metrics_collector
|
|
239
|
+
|
|
240
|
+
metrics_collector = get_metrics_collector()
|
|
241
|
+
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
242
|
+
metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
|
|
243
|
+
except ImportError:
|
|
244
|
+
# Metrics not available, continue without metrics
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
# Capture result metadata
|
|
248
|
+
if result is not None:
|
|
249
|
+
span.set_attribute("mcp.tool.result.type", type(result).__name__)
|
|
250
|
+
|
|
251
|
+
if isinstance(result, list | dict) and hasattr(result, "__len__"):
|
|
252
|
+
span.set_attribute("mcp.tool.result.size", len(result))
|
|
253
|
+
elif isinstance(result, str):
|
|
254
|
+
span.set_attribute("mcp.tool.result.length", len(result))
|
|
255
|
+
|
|
256
|
+
# Capture full output if detailed tracing is enabled
|
|
257
|
+
if _detailed_tracing_enabled:
|
|
258
|
+
output_str = _safe_serialize(result)
|
|
259
|
+
if output_str:
|
|
260
|
+
span.set_attribute("mcp.tool.output", output_str)
|
|
261
|
+
|
|
262
|
+
return result
|
|
263
|
+
except Exception as e:
|
|
264
|
+
span.record_exception(e)
|
|
265
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
266
|
+
|
|
267
|
+
# Add event for error
|
|
268
|
+
span.add_event(
|
|
269
|
+
"tool.execution.error",
|
|
270
|
+
{
|
|
271
|
+
"tool.name": tool_name,
|
|
272
|
+
"error.type": type(e).__name__,
|
|
273
|
+
"error.message": str(e),
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Record metrics for failed execution
|
|
278
|
+
try:
|
|
279
|
+
from golf.metrics import get_metrics_collector
|
|
280
|
+
|
|
281
|
+
metrics_collector = get_metrics_collector()
|
|
282
|
+
metrics_collector.increment_tool_execution(tool_name, "error")
|
|
283
|
+
metrics_collector.increment_error("tool", type(e).__name__)
|
|
284
|
+
except ImportError:
|
|
285
|
+
# Metrics not available, continue without metrics
|
|
286
|
+
pass
|
|
287
|
+
|
|
288
|
+
raise
|
|
289
|
+
|
|
290
|
+
@functools.wraps(func)
|
|
291
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
292
|
+
# Record metrics timing
|
|
293
|
+
import time
|
|
294
|
+
|
|
295
|
+
start_time = time.time()
|
|
296
|
+
|
|
297
|
+
# Create a more descriptive span name
|
|
298
|
+
span_name = f"mcp.tool.{tool_name}.execute"
|
|
299
|
+
|
|
300
|
+
# start_as_current_span automatically uses the current context and manages it
|
|
301
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
302
|
+
# Add essential attributes only
|
|
303
|
+
span.set_attribute("mcp.component.type", "tool")
|
|
304
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
305
|
+
span.set_attribute(
|
|
306
|
+
"mcp.tool.module",
|
|
307
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Add execution context
|
|
311
|
+
span.set_attribute("mcp.execution.args_count", len(args))
|
|
312
|
+
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
313
|
+
|
|
314
|
+
# Extract Context parameter if present
|
|
315
|
+
ctx = kwargs.get("ctx")
|
|
316
|
+
if ctx:
|
|
317
|
+
# Only extract known MCP context attributes
|
|
318
|
+
ctx_attrs = [
|
|
319
|
+
"request_id",
|
|
320
|
+
"session_id",
|
|
321
|
+
"client_id",
|
|
322
|
+
"user_id",
|
|
323
|
+
"tenant_id",
|
|
324
|
+
]
|
|
325
|
+
for attr in ctx_attrs:
|
|
326
|
+
if hasattr(ctx, attr):
|
|
327
|
+
value = getattr(ctx, attr)
|
|
328
|
+
if value is not None:
|
|
329
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
330
|
+
|
|
331
|
+
# Also check baggage for session ID
|
|
332
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
333
|
+
if session_id_from_baggage:
|
|
334
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
335
|
+
|
|
336
|
+
# Add event for tool execution start
|
|
337
|
+
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
result = func(*args, **kwargs)
|
|
341
|
+
span.set_status(Status(StatusCode.OK))
|
|
342
|
+
|
|
343
|
+
# Add event for successful completion
|
|
344
|
+
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
345
|
+
|
|
346
|
+
# Record metrics for successful execution
|
|
347
|
+
try:
|
|
348
|
+
from golf.metrics import get_metrics_collector
|
|
349
|
+
|
|
350
|
+
metrics_collector = get_metrics_collector()
|
|
351
|
+
metrics_collector.increment_tool_execution(tool_name, "success")
|
|
352
|
+
metrics_collector.record_tool_duration(tool_name, time.time() - start_time)
|
|
353
|
+
except ImportError:
|
|
354
|
+
# Metrics not available, continue without metrics
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
# Capture result metadata
|
|
358
|
+
if result is not None:
|
|
359
|
+
span.set_attribute("mcp.tool.result.type", type(result).__name__)
|
|
360
|
+
|
|
361
|
+
if isinstance(result, list | dict) and hasattr(result, "__len__"):
|
|
362
|
+
span.set_attribute("mcp.tool.result.size", len(result))
|
|
363
|
+
elif isinstance(result, str):
|
|
364
|
+
span.set_attribute("mcp.tool.result.length", len(result))
|
|
365
|
+
|
|
366
|
+
# Capture full output if detailed tracing is enabled
|
|
367
|
+
if _detailed_tracing_enabled:
|
|
368
|
+
output_str = _safe_serialize(result)
|
|
369
|
+
if output_str:
|
|
370
|
+
span.set_attribute("mcp.tool.output", output_str)
|
|
371
|
+
|
|
372
|
+
return result
|
|
373
|
+
except Exception as e:
|
|
374
|
+
span.record_exception(e)
|
|
375
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
376
|
+
|
|
377
|
+
# Add event for error
|
|
378
|
+
span.add_event(
|
|
379
|
+
"tool.execution.error",
|
|
380
|
+
{
|
|
381
|
+
"tool.name": tool_name,
|
|
382
|
+
"error.type": type(e).__name__,
|
|
383
|
+
"error.message": str(e),
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Record metrics for failed execution
|
|
388
|
+
try:
|
|
389
|
+
from golf.metrics import get_metrics_collector
|
|
390
|
+
|
|
391
|
+
metrics_collector = get_metrics_collector()
|
|
392
|
+
metrics_collector.increment_tool_execution(tool_name, "error")
|
|
393
|
+
metrics_collector.increment_error("tool", type(e).__name__)
|
|
394
|
+
except ImportError:
|
|
395
|
+
# Metrics not available, continue without metrics
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
raise
|
|
399
|
+
|
|
400
|
+
# Return appropriate wrapper based on function type
|
|
401
|
+
if asyncio.iscoroutinefunction(func):
|
|
402
|
+
return async_wrapper
|
|
403
|
+
else:
|
|
404
|
+
return sync_wrapper
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[..., T]:
|
|
408
|
+
"""Instrument a resource function with OpenTelemetry tracing."""
|
|
409
|
+
global _provider
|
|
410
|
+
|
|
411
|
+
# If telemetry is disabled, return the original function
|
|
412
|
+
if _provider is None:
|
|
413
|
+
return func
|
|
414
|
+
|
|
415
|
+
tracer = get_tracer()
|
|
416
|
+
|
|
417
|
+
# Determine if this is a template based on URI pattern
|
|
418
|
+
is_template = "{" in resource_uri
|
|
419
|
+
|
|
420
|
+
@functools.wraps(func)
|
|
421
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
422
|
+
# Create a more descriptive span name
|
|
423
|
+
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
424
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
425
|
+
# Add essential attributes only
|
|
426
|
+
span.set_attribute("mcp.component.type", "resource")
|
|
427
|
+
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
428
|
+
span.set_attribute("mcp.resource.is_template", is_template)
|
|
429
|
+
span.set_attribute(
|
|
430
|
+
"mcp.resource.module",
|
|
431
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Extract Context parameter if present
|
|
435
|
+
ctx = kwargs.get("ctx")
|
|
436
|
+
if ctx:
|
|
437
|
+
# Only extract known MCP context attributes
|
|
438
|
+
ctx_attrs = [
|
|
439
|
+
"request_id",
|
|
440
|
+
"session_id",
|
|
441
|
+
"client_id",
|
|
442
|
+
"user_id",
|
|
443
|
+
"tenant_id",
|
|
444
|
+
]
|
|
445
|
+
for attr in ctx_attrs:
|
|
446
|
+
if hasattr(ctx, attr):
|
|
447
|
+
value = getattr(ctx, attr)
|
|
448
|
+
if value is not None:
|
|
449
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
450
|
+
|
|
451
|
+
# Also check baggage for session ID
|
|
452
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
453
|
+
if session_id_from_baggage:
|
|
454
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
455
|
+
|
|
456
|
+
# Add event for resource read start
|
|
457
|
+
span.add_event("resource.read.started", {"resource.uri": resource_uri})
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
result = await func(*args, **kwargs)
|
|
461
|
+
span.set_status(Status(StatusCode.OK))
|
|
462
|
+
|
|
463
|
+
# Add event for successful read
|
|
464
|
+
span.add_event("resource.read.completed", {"resource.uri": resource_uri})
|
|
465
|
+
|
|
466
|
+
# Add result metadata
|
|
467
|
+
if hasattr(result, "__len__"):
|
|
468
|
+
span.set_attribute("mcp.resource.result.size", len(result))
|
|
469
|
+
|
|
470
|
+
# Determine content type if possible
|
|
471
|
+
if isinstance(result, str):
|
|
472
|
+
span.set_attribute("mcp.resource.result.type", "text")
|
|
473
|
+
span.set_attribute("mcp.resource.result.length", len(result))
|
|
474
|
+
elif isinstance(result, bytes):
|
|
475
|
+
span.set_attribute("mcp.resource.result.type", "binary")
|
|
476
|
+
span.set_attribute("mcp.resource.result.size_bytes", len(result))
|
|
477
|
+
elif isinstance(result, dict):
|
|
478
|
+
span.set_attribute("mcp.resource.result.type", "object")
|
|
479
|
+
span.set_attribute("mcp.resource.result.keys_count", len(result))
|
|
480
|
+
elif isinstance(result, list):
|
|
481
|
+
span.set_attribute("mcp.resource.result.type", "array")
|
|
482
|
+
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
483
|
+
|
|
484
|
+
return result
|
|
485
|
+
except Exception as e:
|
|
486
|
+
span.record_exception(e)
|
|
487
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
488
|
+
|
|
489
|
+
# Add event for error
|
|
490
|
+
span.add_event(
|
|
491
|
+
"resource.read.error",
|
|
492
|
+
{
|
|
493
|
+
"resource.uri": resource_uri,
|
|
494
|
+
"error.type": type(e).__name__,
|
|
495
|
+
"error.message": str(e),
|
|
496
|
+
},
|
|
497
|
+
)
|
|
498
|
+
raise
|
|
499
|
+
|
|
500
|
+
@functools.wraps(func)
|
|
501
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
502
|
+
# Create a more descriptive span name
|
|
503
|
+
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
504
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
505
|
+
# Add essential attributes only
|
|
506
|
+
span.set_attribute("mcp.component.type", "resource")
|
|
507
|
+
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
508
|
+
span.set_attribute("mcp.resource.is_template", is_template)
|
|
509
|
+
span.set_attribute(
|
|
510
|
+
"mcp.resource.module",
|
|
511
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Extract Context parameter if present
|
|
515
|
+
ctx = kwargs.get("ctx")
|
|
516
|
+
if ctx:
|
|
517
|
+
# Only extract known MCP context attributes
|
|
518
|
+
ctx_attrs = [
|
|
519
|
+
"request_id",
|
|
520
|
+
"session_id",
|
|
521
|
+
"client_id",
|
|
522
|
+
"user_id",
|
|
523
|
+
"tenant_id",
|
|
524
|
+
]
|
|
525
|
+
for attr in ctx_attrs:
|
|
526
|
+
if hasattr(ctx, attr):
|
|
527
|
+
value = getattr(ctx, attr)
|
|
528
|
+
if value is not None:
|
|
529
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
530
|
+
|
|
531
|
+
# Also check baggage for session ID
|
|
532
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
533
|
+
if session_id_from_baggage:
|
|
534
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
535
|
+
|
|
536
|
+
# Add event for resource read start
|
|
537
|
+
span.add_event("resource.read.started", {"resource.uri": resource_uri})
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
result = func(*args, **kwargs)
|
|
541
|
+
span.set_status(Status(StatusCode.OK))
|
|
542
|
+
|
|
543
|
+
# Add event for successful read
|
|
544
|
+
span.add_event("resource.read.completed", {"resource.uri": resource_uri})
|
|
545
|
+
|
|
546
|
+
# Add result metadata
|
|
547
|
+
if hasattr(result, "__len__"):
|
|
548
|
+
span.set_attribute("mcp.resource.result.size", len(result))
|
|
549
|
+
|
|
550
|
+
# Determine content type if possible
|
|
551
|
+
if isinstance(result, str):
|
|
552
|
+
span.set_attribute("mcp.resource.result.type", "text")
|
|
553
|
+
span.set_attribute("mcp.resource.result.length", len(result))
|
|
554
|
+
elif isinstance(result, bytes):
|
|
555
|
+
span.set_attribute("mcp.resource.result.type", "binary")
|
|
556
|
+
span.set_attribute("mcp.resource.result.size_bytes", len(result))
|
|
557
|
+
elif isinstance(result, dict):
|
|
558
|
+
span.set_attribute("mcp.resource.result.type", "object")
|
|
559
|
+
span.set_attribute("mcp.resource.result.keys_count", len(result))
|
|
560
|
+
elif isinstance(result, list):
|
|
561
|
+
span.set_attribute("mcp.resource.result.type", "array")
|
|
562
|
+
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
563
|
+
|
|
564
|
+
return result
|
|
565
|
+
except Exception as e:
|
|
566
|
+
span.record_exception(e)
|
|
567
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
568
|
+
|
|
569
|
+
# Add event for error
|
|
570
|
+
span.add_event(
|
|
571
|
+
"resource.read.error",
|
|
572
|
+
{
|
|
573
|
+
"resource.uri": resource_uri,
|
|
574
|
+
"error.type": type(e).__name__,
|
|
575
|
+
"error.message": str(e),
|
|
576
|
+
},
|
|
577
|
+
)
|
|
578
|
+
raise
|
|
579
|
+
|
|
580
|
+
if asyncio.iscoroutinefunction(func):
|
|
581
|
+
return async_wrapper
|
|
582
|
+
else:
|
|
583
|
+
return sync_wrapper
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def instrument_elicitation(func: Callable[..., T], elicitation_type: str = "elicit") -> Callable[..., T]:
|
|
587
|
+
"""Instrument an elicitation function with OpenTelemetry tracing."""
|
|
588
|
+
global _provider
|
|
589
|
+
|
|
590
|
+
# If telemetry is disabled, return the original function
|
|
591
|
+
if _provider is None:
|
|
592
|
+
return func
|
|
593
|
+
|
|
594
|
+
tracer = get_tracer()
|
|
595
|
+
|
|
596
|
+
@functools.wraps(func)
|
|
597
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
598
|
+
# If telemetry is disabled at runtime, call original function
|
|
599
|
+
global _provider
|
|
600
|
+
if _provider is None:
|
|
601
|
+
return await func(*args, **kwargs)
|
|
602
|
+
|
|
603
|
+
# Record metrics timing
|
|
604
|
+
start_time = time.time()
|
|
605
|
+
|
|
606
|
+
# Create a more descriptive span name
|
|
607
|
+
span_name = f"mcp.elicitation.{elicitation_type}.request"
|
|
608
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
609
|
+
# Add essential attributes
|
|
610
|
+
span.set_attribute("mcp.component.type", "elicitation")
|
|
611
|
+
span.set_attribute("mcp.elicitation.type", elicitation_type)
|
|
612
|
+
|
|
613
|
+
# Capture elicitation parameters if detailed tracing is enabled
|
|
614
|
+
if _detailed_tracing_enabled:
|
|
615
|
+
# Extract message from first argument (common pattern)
|
|
616
|
+
if args:
|
|
617
|
+
message = args[0] if isinstance(args[0], str) else None
|
|
618
|
+
if message:
|
|
619
|
+
span.set_attribute("mcp.elicitation.message", _safe_serialize(message, 500))
|
|
620
|
+
|
|
621
|
+
# Extract response_type from kwargs/args
|
|
622
|
+
response_type = kwargs.get("response_type") or (args[1] if len(args) > 1 else None)
|
|
623
|
+
if response_type is not None:
|
|
624
|
+
if isinstance(response_type, list):
|
|
625
|
+
span.set_attribute("mcp.elicitation.response_type", "choice")
|
|
626
|
+
span.set_attribute("mcp.elicitation.choices", str(response_type))
|
|
627
|
+
elif hasattr(response_type, "__name__"):
|
|
628
|
+
span.set_attribute("mcp.elicitation.response_type", response_type.__name__)
|
|
629
|
+
else:
|
|
630
|
+
span.set_attribute("mcp.elicitation.response_type", str(type(response_type).__name__))
|
|
631
|
+
|
|
632
|
+
# Extract Context parameter if present
|
|
633
|
+
ctx = kwargs.get("ctx")
|
|
634
|
+
if ctx:
|
|
635
|
+
ctx_attrs = ["request_id", "session_id", "client_id", "user_id", "tenant_id"]
|
|
636
|
+
for attr in ctx_attrs:
|
|
637
|
+
if hasattr(ctx, attr):
|
|
638
|
+
value = getattr(ctx, attr)
|
|
639
|
+
if value is not None:
|
|
640
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
641
|
+
|
|
642
|
+
# Add event for elicitation start
|
|
643
|
+
span.add_event("elicitation.request.started")
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
result = await func(*args, **kwargs)
|
|
647
|
+
span.set_status(Status(StatusCode.OK))
|
|
648
|
+
|
|
649
|
+
# Add event for successful completion
|
|
650
|
+
span.add_event("elicitation.request.completed")
|
|
651
|
+
|
|
652
|
+
# Capture result metadata
|
|
653
|
+
if result is not None and _detailed_tracing_enabled:
|
|
654
|
+
if isinstance(result, str):
|
|
655
|
+
span.set_attribute("mcp.elicitation.result.content", _safe_serialize(result, 500))
|
|
656
|
+
elif isinstance(result, (list, dict)) and hasattr(result, "__len__"):
|
|
657
|
+
span.set_attribute("mcp.elicitation.result.size", len(result))
|
|
658
|
+
span.set_attribute("mcp.elicitation.result.content", _safe_serialize(result, 1000))
|
|
659
|
+
|
|
660
|
+
# Record metrics for successful elicitation
|
|
661
|
+
try:
|
|
662
|
+
from golf.metrics import get_metrics_collector
|
|
663
|
+
|
|
664
|
+
metrics_collector = get_metrics_collector()
|
|
665
|
+
metrics_collector.increment_elicitation(elicitation_type, "success")
|
|
666
|
+
metrics_collector.record_elicitation_duration(elicitation_type, time.time() - start_time)
|
|
667
|
+
except ImportError:
|
|
668
|
+
pass
|
|
669
|
+
|
|
670
|
+
return result
|
|
671
|
+
except Exception as e:
|
|
672
|
+
span.record_exception(e)
|
|
673
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
674
|
+
|
|
675
|
+
# Add event for error
|
|
676
|
+
span.add_event(
|
|
677
|
+
"elicitation.request.error",
|
|
678
|
+
{
|
|
679
|
+
"error.type": type(e).__name__,
|
|
680
|
+
"error.message": str(e),
|
|
681
|
+
},
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# Record metrics for failed elicitation
|
|
685
|
+
try:
|
|
686
|
+
from golf.metrics import get_metrics_collector
|
|
687
|
+
|
|
688
|
+
metrics_collector = get_metrics_collector()
|
|
689
|
+
metrics_collector.increment_elicitation(elicitation_type, "error")
|
|
690
|
+
metrics_collector.increment_error("elicitation", type(e).__name__)
|
|
691
|
+
except ImportError:
|
|
692
|
+
pass
|
|
693
|
+
|
|
694
|
+
raise
|
|
695
|
+
|
|
696
|
+
@functools.wraps(func)
|
|
697
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
698
|
+
# If telemetry is disabled at runtime, call original function
|
|
699
|
+
global _provider
|
|
700
|
+
if _provider is None:
|
|
701
|
+
return func(*args, **kwargs)
|
|
702
|
+
|
|
703
|
+
# Record metrics timing
|
|
704
|
+
start_time = time.time()
|
|
705
|
+
|
|
706
|
+
# Create a more descriptive span name
|
|
707
|
+
span_name = f"mcp.elicitation.{elicitation_type}.request"
|
|
708
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
709
|
+
# Add essential attributes
|
|
710
|
+
span.set_attribute("mcp.component.type", "elicitation")
|
|
711
|
+
span.set_attribute("mcp.elicitation.type", elicitation_type)
|
|
712
|
+
|
|
713
|
+
# Capture elicitation parameters if detailed tracing is enabled
|
|
714
|
+
if _detailed_tracing_enabled:
|
|
715
|
+
if args:
|
|
716
|
+
message = args[0] if isinstance(args[0], str) else None
|
|
717
|
+
if message:
|
|
718
|
+
span.set_attribute("mcp.elicitation.message", _safe_serialize(message, 500))
|
|
719
|
+
|
|
720
|
+
# Add event for elicitation start
|
|
721
|
+
span.add_event("elicitation.request.started")
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
result = func(*args, **kwargs)
|
|
725
|
+
span.set_status(Status(StatusCode.OK))
|
|
726
|
+
|
|
727
|
+
# Add event for successful completion
|
|
728
|
+
span.add_event("elicitation.request.completed")
|
|
729
|
+
|
|
730
|
+
# Record metrics for successful elicitation
|
|
731
|
+
try:
|
|
732
|
+
from golf.metrics import get_metrics_collector
|
|
733
|
+
|
|
734
|
+
metrics_collector = get_metrics_collector()
|
|
735
|
+
metrics_collector.increment_elicitation(elicitation_type, "success")
|
|
736
|
+
metrics_collector.record_elicitation_duration(elicitation_type, time.time() - start_time)
|
|
737
|
+
except ImportError:
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
return result
|
|
741
|
+
except Exception as e:
|
|
742
|
+
span.record_exception(e)
|
|
743
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
744
|
+
|
|
745
|
+
# Add event for error
|
|
746
|
+
span.add_event(
|
|
747
|
+
"elicitation.request.error",
|
|
748
|
+
{
|
|
749
|
+
"error.type": type(e).__name__,
|
|
750
|
+
"error.message": str(e),
|
|
751
|
+
},
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Record metrics for failed elicitation
|
|
755
|
+
try:
|
|
756
|
+
from golf.metrics import get_metrics_collector
|
|
757
|
+
|
|
758
|
+
metrics_collector = get_metrics_collector()
|
|
759
|
+
metrics_collector.increment_elicitation(elicitation_type, "error")
|
|
760
|
+
metrics_collector.increment_error("elicitation", type(e).__name__)
|
|
761
|
+
except ImportError:
|
|
762
|
+
pass
|
|
763
|
+
|
|
764
|
+
raise
|
|
765
|
+
|
|
766
|
+
if asyncio.iscoroutinefunction(func):
|
|
767
|
+
return async_wrapper
|
|
768
|
+
else:
|
|
769
|
+
return sync_wrapper
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def instrument_sampling(func: Callable[..., T], sampling_type: str = "sample") -> Callable[..., T]:
|
|
773
|
+
"""Instrument a sampling function with OpenTelemetry tracing."""
|
|
774
|
+
global _provider
|
|
775
|
+
|
|
776
|
+
# If telemetry is disabled, return the original function
|
|
777
|
+
if _provider is None:
|
|
778
|
+
return func
|
|
779
|
+
|
|
780
|
+
tracer = get_tracer()
|
|
781
|
+
|
|
782
|
+
@functools.wraps(func)
|
|
783
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
784
|
+
# If telemetry is disabled at runtime, call original function
|
|
785
|
+
global _provider
|
|
786
|
+
if _provider is None:
|
|
787
|
+
return await func(*args, **kwargs)
|
|
788
|
+
|
|
789
|
+
# Record metrics timing
|
|
790
|
+
start_time = time.time()
|
|
791
|
+
|
|
792
|
+
# Create a more descriptive span name
|
|
793
|
+
span_name = f"mcp.sampling.{sampling_type}.request"
|
|
794
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
795
|
+
# Add essential attributes
|
|
796
|
+
span.set_attribute("mcp.component.type", "sampling")
|
|
797
|
+
span.set_attribute("mcp.sampling.type", sampling_type)
|
|
798
|
+
|
|
799
|
+
# Capture sampling parameters
|
|
800
|
+
messages = kwargs.get("messages") or (args[0] if args else None)
|
|
801
|
+
if messages and _detailed_tracing_enabled:
|
|
802
|
+
if isinstance(messages, str):
|
|
803
|
+
span.set_attribute("mcp.sampling.messages.content", _safe_serialize(messages, 1000))
|
|
804
|
+
elif isinstance(messages, list):
|
|
805
|
+
span.set_attribute("mcp.sampling.messages.type", "list")
|
|
806
|
+
span.set_attribute("mcp.sampling.messages.count", len(messages))
|
|
807
|
+
span.set_attribute("mcp.sampling.messages.content", _safe_serialize(messages, 1000))
|
|
808
|
+
|
|
809
|
+
# Capture other sampling parameters
|
|
810
|
+
system_prompt = kwargs.get("system_prompt")
|
|
811
|
+
if system_prompt and _detailed_tracing_enabled:
|
|
812
|
+
span.set_attribute("mcp.sampling.system_prompt.length", len(str(system_prompt)))
|
|
813
|
+
span.set_attribute("mcp.sampling.system_prompt.content", _safe_serialize(system_prompt, 500))
|
|
814
|
+
|
|
815
|
+
temperature = kwargs.get("temperature")
|
|
816
|
+
if temperature is not None:
|
|
817
|
+
span.set_attribute("mcp.sampling.temperature", temperature)
|
|
818
|
+
|
|
819
|
+
max_tokens = kwargs.get("max_tokens")
|
|
820
|
+
if max_tokens is not None:
|
|
821
|
+
span.set_attribute("mcp.sampling.max_tokens", max_tokens)
|
|
822
|
+
|
|
823
|
+
model_preferences = kwargs.get("model_preferences")
|
|
824
|
+
if model_preferences:
|
|
825
|
+
if isinstance(model_preferences, str):
|
|
826
|
+
span.set_attribute("mcp.sampling.model_preferences", model_preferences)
|
|
827
|
+
elif isinstance(model_preferences, list):
|
|
828
|
+
span.set_attribute("mcp.sampling.model_preferences", ",".join(model_preferences))
|
|
829
|
+
|
|
830
|
+
# Extract Context parameter if present
|
|
831
|
+
ctx = kwargs.get("ctx")
|
|
832
|
+
if ctx:
|
|
833
|
+
ctx_attrs = ["request_id", "session_id", "client_id", "user_id", "tenant_id"]
|
|
834
|
+
for attr in ctx_attrs:
|
|
835
|
+
if hasattr(ctx, attr):
|
|
836
|
+
value = getattr(ctx, attr)
|
|
837
|
+
if value is not None:
|
|
838
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
839
|
+
|
|
840
|
+
# Add event for sampling start
|
|
841
|
+
span.add_event("sampling.request.started")
|
|
842
|
+
|
|
843
|
+
try:
|
|
844
|
+
result = await func(*args, **kwargs)
|
|
845
|
+
span.set_status(Status(StatusCode.OK))
|
|
846
|
+
|
|
847
|
+
# Add event for successful completion
|
|
848
|
+
span.add_event("sampling.request.completed")
|
|
849
|
+
|
|
850
|
+
# Capture result metadata
|
|
851
|
+
if result is not None and _detailed_tracing_enabled and isinstance(result, str):
|
|
852
|
+
span.set_attribute("mcp.sampling.result.content", _safe_serialize(result, 1000))
|
|
853
|
+
|
|
854
|
+
# Record metrics for successful sampling
|
|
855
|
+
try:
|
|
856
|
+
from golf.metrics import get_metrics_collector
|
|
857
|
+
|
|
858
|
+
metrics_collector = get_metrics_collector()
|
|
859
|
+
metrics_collector.increment_sampling(sampling_type, "success")
|
|
860
|
+
metrics_collector.record_sampling_duration(sampling_type, time.time() - start_time)
|
|
861
|
+
if isinstance(result, str):
|
|
862
|
+
metrics_collector.record_sampling_tokens(sampling_type, len(result.split()))
|
|
863
|
+
except ImportError:
|
|
864
|
+
pass
|
|
865
|
+
|
|
866
|
+
return result
|
|
867
|
+
except Exception as e:
|
|
868
|
+
span.record_exception(e)
|
|
869
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
870
|
+
|
|
871
|
+
# Add event for error
|
|
872
|
+
span.add_event(
|
|
873
|
+
"sampling.request.error",
|
|
874
|
+
{
|
|
875
|
+
"error.type": type(e).__name__,
|
|
876
|
+
"error.message": str(e),
|
|
877
|
+
},
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
# Record metrics for failed sampling
|
|
881
|
+
try:
|
|
882
|
+
from golf.metrics import get_metrics_collector
|
|
883
|
+
|
|
884
|
+
metrics_collector = get_metrics_collector()
|
|
885
|
+
metrics_collector.increment_sampling(sampling_type, "error")
|
|
886
|
+
metrics_collector.increment_error("sampling", type(e).__name__)
|
|
887
|
+
except ImportError:
|
|
888
|
+
pass
|
|
889
|
+
|
|
890
|
+
raise
|
|
891
|
+
|
|
892
|
+
@functools.wraps(func)
|
|
893
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
894
|
+
# If telemetry is disabled at runtime, call original function
|
|
895
|
+
global _provider
|
|
896
|
+
if _provider is None:
|
|
897
|
+
return func(*args, **kwargs)
|
|
898
|
+
|
|
899
|
+
# Record metrics timing
|
|
900
|
+
start_time = time.time()
|
|
901
|
+
|
|
902
|
+
# Create a more descriptive span name
|
|
903
|
+
span_name = f"mcp.sampling.{sampling_type}.request"
|
|
904
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
905
|
+
# Add essential attributes
|
|
906
|
+
span.set_attribute("mcp.component.type", "sampling")
|
|
907
|
+
span.set_attribute("mcp.sampling.type", sampling_type)
|
|
908
|
+
|
|
909
|
+
# Add event for sampling start
|
|
910
|
+
span.add_event("sampling.request.started")
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
result = func(*args, **kwargs)
|
|
914
|
+
span.set_status(Status(StatusCode.OK))
|
|
915
|
+
|
|
916
|
+
# Add event for successful completion
|
|
917
|
+
span.add_event("sampling.request.completed")
|
|
918
|
+
|
|
919
|
+
# Record metrics for successful sampling
|
|
920
|
+
try:
|
|
921
|
+
from golf.metrics import get_metrics_collector
|
|
922
|
+
|
|
923
|
+
metrics_collector = get_metrics_collector()
|
|
924
|
+
metrics_collector.increment_sampling(sampling_type, "success")
|
|
925
|
+
metrics_collector.record_sampling_duration(sampling_type, time.time() - start_time)
|
|
926
|
+
except ImportError:
|
|
927
|
+
pass
|
|
928
|
+
|
|
929
|
+
return result
|
|
930
|
+
except Exception as e:
|
|
931
|
+
span.record_exception(e)
|
|
932
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
933
|
+
|
|
934
|
+
# Add event for error
|
|
935
|
+
span.add_event(
|
|
936
|
+
"sampling.request.error",
|
|
937
|
+
{
|
|
938
|
+
"error.type": type(e).__name__,
|
|
939
|
+
"error.message": str(e),
|
|
940
|
+
},
|
|
941
|
+
)
|
|
942
|
+
raise
|
|
943
|
+
|
|
944
|
+
if asyncio.iscoroutinefunction(func):
|
|
945
|
+
return async_wrapper
|
|
946
|
+
else:
|
|
947
|
+
return sync_wrapper
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
|
|
951
|
+
"""Instrument a prompt function with OpenTelemetry tracing."""
|
|
952
|
+
global _provider
|
|
953
|
+
|
|
954
|
+
# If telemetry is disabled, return the original function
|
|
955
|
+
if _provider is None:
|
|
956
|
+
return func
|
|
957
|
+
|
|
958
|
+
tracer = get_tracer()
|
|
959
|
+
|
|
960
|
+
@functools.wraps(func)
|
|
961
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
962
|
+
# Create a more descriptive span name
|
|
963
|
+
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
964
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
965
|
+
# Add essential attributes only
|
|
966
|
+
span.set_attribute("mcp.component.type", "prompt")
|
|
967
|
+
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
968
|
+
span.set_attribute(
|
|
969
|
+
"mcp.prompt.module",
|
|
970
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
# Extract Context parameter if present
|
|
974
|
+
ctx = kwargs.get("ctx")
|
|
975
|
+
if ctx:
|
|
976
|
+
# Only extract known MCP context attributes
|
|
977
|
+
ctx_attrs = [
|
|
978
|
+
"request_id",
|
|
979
|
+
"session_id",
|
|
980
|
+
"client_id",
|
|
981
|
+
"user_id",
|
|
982
|
+
"tenant_id",
|
|
983
|
+
]
|
|
984
|
+
for attr in ctx_attrs:
|
|
985
|
+
if hasattr(ctx, attr):
|
|
986
|
+
value = getattr(ctx, attr)
|
|
987
|
+
if value is not None:
|
|
988
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
989
|
+
|
|
990
|
+
# Also check baggage for session ID
|
|
991
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
992
|
+
if session_id_from_baggage:
|
|
993
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
994
|
+
|
|
995
|
+
# Add event for prompt generation start
|
|
996
|
+
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
997
|
+
|
|
998
|
+
try:
|
|
999
|
+
result = await func(*args, **kwargs)
|
|
1000
|
+
span.set_status(Status(StatusCode.OK))
|
|
1001
|
+
|
|
1002
|
+
# Add event for successful generation
|
|
1003
|
+
span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
|
|
1004
|
+
|
|
1005
|
+
# Add message count and type information
|
|
1006
|
+
if isinstance(result, list):
|
|
1007
|
+
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
1008
|
+
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
1009
|
+
|
|
1010
|
+
# Analyze message types if they have role attributes
|
|
1011
|
+
roles = []
|
|
1012
|
+
for msg in result:
|
|
1013
|
+
if hasattr(msg, "role"):
|
|
1014
|
+
roles.append(msg.role)
|
|
1015
|
+
elif isinstance(msg, dict) and "role" in msg:
|
|
1016
|
+
roles.append(msg["role"])
|
|
1017
|
+
|
|
1018
|
+
if roles:
|
|
1019
|
+
unique_roles = list(set(roles))
|
|
1020
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
1021
|
+
span.set_attribute(
|
|
1022
|
+
"mcp.prompt.result.role_counts",
|
|
1023
|
+
str({role: roles.count(role) for role in unique_roles}),
|
|
1024
|
+
)
|
|
1025
|
+
elif isinstance(result, str):
|
|
1026
|
+
span.set_attribute("mcp.prompt.result.type", "string")
|
|
1027
|
+
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
1028
|
+
else:
|
|
1029
|
+
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
1030
|
+
|
|
1031
|
+
return result
|
|
1032
|
+
except Exception as e:
|
|
1033
|
+
span.record_exception(e)
|
|
1034
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
1035
|
+
|
|
1036
|
+
# Add event for error
|
|
1037
|
+
span.add_event(
|
|
1038
|
+
"prompt.generation.error",
|
|
1039
|
+
{
|
|
1040
|
+
"prompt.name": prompt_name,
|
|
1041
|
+
"error.type": type(e).__name__,
|
|
1042
|
+
"error.message": str(e),
|
|
1043
|
+
},
|
|
1044
|
+
)
|
|
1045
|
+
raise
|
|
1046
|
+
|
|
1047
|
+
@functools.wraps(func)
|
|
1048
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1049
|
+
# Create a more descriptive span name
|
|
1050
|
+
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
1051
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
1052
|
+
# Add essential attributes only
|
|
1053
|
+
span.set_attribute("mcp.component.type", "prompt")
|
|
1054
|
+
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
1055
|
+
span.set_attribute(
|
|
1056
|
+
"mcp.prompt.module",
|
|
1057
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
# Extract Context parameter if present
|
|
1061
|
+
ctx = kwargs.get("ctx")
|
|
1062
|
+
if ctx:
|
|
1063
|
+
# Only extract known MCP context attributes
|
|
1064
|
+
ctx_attrs = [
|
|
1065
|
+
"request_id",
|
|
1066
|
+
"session_id",
|
|
1067
|
+
"client_id",
|
|
1068
|
+
"user_id",
|
|
1069
|
+
"tenant_id",
|
|
1070
|
+
]
|
|
1071
|
+
for attr in ctx_attrs:
|
|
1072
|
+
if hasattr(ctx, attr):
|
|
1073
|
+
value = getattr(ctx, attr)
|
|
1074
|
+
if value is not None:
|
|
1075
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
1076
|
+
|
|
1077
|
+
# Also check baggage for session ID
|
|
1078
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
1079
|
+
if session_id_from_baggage:
|
|
1080
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
1081
|
+
|
|
1082
|
+
# Add event for prompt generation start
|
|
1083
|
+
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
1084
|
+
|
|
1085
|
+
try:
|
|
1086
|
+
result = func(*args, **kwargs)
|
|
1087
|
+
span.set_status(Status(StatusCode.OK))
|
|
1088
|
+
|
|
1089
|
+
# Add event for successful generation
|
|
1090
|
+
span.add_event("prompt.generation.completed", {"prompt.name": prompt_name})
|
|
1091
|
+
|
|
1092
|
+
# Add message count and type information
|
|
1093
|
+
if isinstance(result, list):
|
|
1094
|
+
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
1095
|
+
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
1096
|
+
|
|
1097
|
+
# Analyze message types if they have role attributes
|
|
1098
|
+
roles = []
|
|
1099
|
+
for msg in result:
|
|
1100
|
+
if hasattr(msg, "role"):
|
|
1101
|
+
roles.append(msg.role)
|
|
1102
|
+
elif isinstance(msg, dict) and "role" in msg:
|
|
1103
|
+
roles.append(msg["role"])
|
|
1104
|
+
|
|
1105
|
+
if roles:
|
|
1106
|
+
unique_roles = list(set(roles))
|
|
1107
|
+
span.set_attribute("mcp.prompt.result.roles", ",".join(unique_roles))
|
|
1108
|
+
span.set_attribute(
|
|
1109
|
+
"mcp.prompt.result.role_counts",
|
|
1110
|
+
str({role: roles.count(role) for role in unique_roles}),
|
|
1111
|
+
)
|
|
1112
|
+
elif isinstance(result, str):
|
|
1113
|
+
span.set_attribute("mcp.prompt.result.type", "string")
|
|
1114
|
+
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
1115
|
+
else:
|
|
1116
|
+
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
1117
|
+
|
|
1118
|
+
return result
|
|
1119
|
+
except Exception as e:
|
|
1120
|
+
span.record_exception(e)
|
|
1121
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
1122
|
+
|
|
1123
|
+
# Add event for error
|
|
1124
|
+
span.add_event(
|
|
1125
|
+
"prompt.generation.error",
|
|
1126
|
+
{
|
|
1127
|
+
"prompt.name": prompt_name,
|
|
1128
|
+
"error.type": type(e).__name__,
|
|
1129
|
+
"error.message": str(e),
|
|
1130
|
+
},
|
|
1131
|
+
)
|
|
1132
|
+
raise
|
|
1133
|
+
|
|
1134
|
+
if asyncio.iscoroutinefunction(func):
|
|
1135
|
+
return async_wrapper
|
|
1136
|
+
else:
|
|
1137
|
+
return sync_wrapper
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
# Add the BoundedSessionTracker class before SessionTracingMiddleware
|
|
1141
|
+
class BoundedSessionTracker:
|
|
1142
|
+
"""Memory-safe session tracker with automatic expiration."""
|
|
1143
|
+
|
|
1144
|
+
def __init__(self, max_sessions: int = 1000, session_ttl: int = 3600) -> None:
|
|
1145
|
+
self.max_sessions = max_sessions
|
|
1146
|
+
self.session_ttl = session_ttl
|
|
1147
|
+
self.sessions: OrderedDict[str, float] = OrderedDict()
|
|
1148
|
+
self.last_cleanup = time.time()
|
|
1149
|
+
|
|
1150
|
+
def track_session(self, session_id: str) -> bool:
|
|
1151
|
+
"""Track a session, returns True if it's new."""
|
|
1152
|
+
current_time = time.time()
|
|
1153
|
+
|
|
1154
|
+
# Periodic cleanup (every 5 minutes)
|
|
1155
|
+
if current_time - self.last_cleanup > 300:
|
|
1156
|
+
self._cleanup_expired(current_time)
|
|
1157
|
+
self.last_cleanup = current_time
|
|
1158
|
+
|
|
1159
|
+
# Check if session exists and is still valid
|
|
1160
|
+
if session_id in self.sessions:
|
|
1161
|
+
# Move to end (mark as recently used)
|
|
1162
|
+
self.sessions.move_to_end(session_id)
|
|
1163
|
+
return False
|
|
1164
|
+
|
|
1165
|
+
# New session
|
|
1166
|
+
self.sessions[session_id] = current_time
|
|
1167
|
+
|
|
1168
|
+
# Enforce max size
|
|
1169
|
+
while len(self.sessions) > self.max_sessions:
|
|
1170
|
+
self.sessions.popitem(last=False) # Remove oldest
|
|
1171
|
+
|
|
1172
|
+
return True
|
|
1173
|
+
|
|
1174
|
+
def _cleanup_expired(self, current_time: float) -> None:
|
|
1175
|
+
"""Remove expired sessions."""
|
|
1176
|
+
expired = [sid for sid, timestamp in self.sessions.items() if current_time - timestamp > self.session_ttl]
|
|
1177
|
+
for sid in expired:
|
|
1178
|
+
del self.sessions[sid]
|
|
1179
|
+
|
|
1180
|
+
def get_active_session_count(self) -> int:
|
|
1181
|
+
return len(self.sessions)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
1185
|
+
def __init__(self, app: Any) -> None:
|
|
1186
|
+
super().__init__(app)
|
|
1187
|
+
# Use memory-safe session tracker instead of unbounded collections
|
|
1188
|
+
self.session_tracker = BoundedSessionTracker(max_sessions=1000, session_ttl=3600)
|
|
1189
|
+
|
|
1190
|
+
async def dispatch(self, request: Any, call_next: Callable[..., Any]) -> Any:
|
|
1191
|
+
# Record HTTP request timing
|
|
1192
|
+
import time
|
|
1193
|
+
|
|
1194
|
+
start_time = time.time()
|
|
1195
|
+
|
|
1196
|
+
# Extract session ID from query params or headers
|
|
1197
|
+
session_id = request.query_params.get("session_id")
|
|
1198
|
+
if not session_id:
|
|
1199
|
+
# Check headers as fallback
|
|
1200
|
+
session_id = request.headers.get("x-session-id")
|
|
1201
|
+
|
|
1202
|
+
# Track session metrics using memory-safe tracker
|
|
1203
|
+
if session_id:
|
|
1204
|
+
is_new_session = self.session_tracker.track_session(session_id)
|
|
1205
|
+
|
|
1206
|
+
if is_new_session:
|
|
1207
|
+
try:
|
|
1208
|
+
from golf.metrics import get_metrics_collector
|
|
1209
|
+
|
|
1210
|
+
metrics_collector = get_metrics_collector()
|
|
1211
|
+
metrics_collector.increment_session()
|
|
1212
|
+
except ImportError:
|
|
1213
|
+
pass
|
|
1214
|
+
else:
|
|
1215
|
+
# Record session duration for existing sessions
|
|
1216
|
+
try:
|
|
1217
|
+
from golf.metrics import get_metrics_collector
|
|
1218
|
+
|
|
1219
|
+
metrics_collector = get_metrics_collector()
|
|
1220
|
+
# Use a default duration since we don't track exact start
|
|
1221
|
+
# times anymore
|
|
1222
|
+
# This is less precise but memory-safe
|
|
1223
|
+
metrics_collector.record_session_duration(300.0) # 5 min default
|
|
1224
|
+
except ImportError:
|
|
1225
|
+
pass
|
|
1226
|
+
|
|
1227
|
+
# Create a descriptive span name based on the request
|
|
1228
|
+
method = request.method
|
|
1229
|
+
path = request.url.path
|
|
1230
|
+
|
|
1231
|
+
# Determine the operation type from the path
|
|
1232
|
+
operation_type = "unknown"
|
|
1233
|
+
if "/mcp" in path:
|
|
1234
|
+
operation_type = "mcp.request"
|
|
1235
|
+
elif "/sse" in path:
|
|
1236
|
+
operation_type = "sse.stream"
|
|
1237
|
+
elif "/auth" in path:
|
|
1238
|
+
operation_type = "auth"
|
|
1239
|
+
|
|
1240
|
+
span_name = f"{operation_type}.{method.lower()}"
|
|
1241
|
+
|
|
1242
|
+
tracer = get_tracer()
|
|
1243
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
1244
|
+
# Add essential HTTP attributes
|
|
1245
|
+
span.set_attribute("http.method", method)
|
|
1246
|
+
span.set_attribute("http.target", path)
|
|
1247
|
+
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
1248
|
+
|
|
1249
|
+
# Add session tracking
|
|
1250
|
+
if session_id:
|
|
1251
|
+
span.set_attribute("mcp.session.id", session_id)
|
|
1252
|
+
span.set_attribute(
|
|
1253
|
+
"mcp.session.active_count",
|
|
1254
|
+
self.session_tracker.get_active_session_count(),
|
|
1255
|
+
)
|
|
1256
|
+
# Add to baggage for propagation
|
|
1257
|
+
ctx = baggage.set_baggage("mcp.session.id", session_id)
|
|
1258
|
+
from opentelemetry import context
|
|
1259
|
+
|
|
1260
|
+
token = context.attach(ctx)
|
|
1261
|
+
else:
|
|
1262
|
+
token = None
|
|
1263
|
+
|
|
1264
|
+
# Add request size if available
|
|
1265
|
+
content_length = request.headers.get("content-length")
|
|
1266
|
+
if content_length:
|
|
1267
|
+
span.set_attribute("http.request.size", int(content_length))
|
|
1268
|
+
|
|
1269
|
+
# Add event for request start
|
|
1270
|
+
span.add_event("http.request.started", {"method": method, "path": path})
|
|
1271
|
+
|
|
1272
|
+
try:
|
|
1273
|
+
response = await call_next(request)
|
|
1274
|
+
|
|
1275
|
+
# Add response attributes
|
|
1276
|
+
span.set_attribute("http.status_code", response.status_code)
|
|
1277
|
+
|
|
1278
|
+
# Set span status based on HTTP status
|
|
1279
|
+
if response.status_code >= 400:
|
|
1280
|
+
span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
1281
|
+
else:
|
|
1282
|
+
span.set_status(Status(StatusCode.OK))
|
|
1283
|
+
|
|
1284
|
+
# Add event for request completion
|
|
1285
|
+
span.add_event(
|
|
1286
|
+
"http.request.completed",
|
|
1287
|
+
{
|
|
1288
|
+
"method": method,
|
|
1289
|
+
"path": path,
|
|
1290
|
+
"status_code": response.status_code,
|
|
1291
|
+
},
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
# Record HTTP request metrics
|
|
1295
|
+
try:
|
|
1296
|
+
from golf.metrics import get_metrics_collector
|
|
1297
|
+
|
|
1298
|
+
metrics_collector = get_metrics_collector()
|
|
1299
|
+
|
|
1300
|
+
# Clean up path for metrics (remove query params, normalize)
|
|
1301
|
+
clean_path = path.split("?")[0] # Remove query parameters
|
|
1302
|
+
if clean_path.startswith("/"):
|
|
1303
|
+
clean_path = clean_path[1:] or "root" # Remove leading slash, handle root
|
|
1304
|
+
|
|
1305
|
+
metrics_collector.increment_http_request(method, response.status_code, clean_path)
|
|
1306
|
+
metrics_collector.record_http_duration(method, clean_path, time.time() - start_time)
|
|
1307
|
+
except ImportError:
|
|
1308
|
+
# Metrics not available, continue without metrics
|
|
1309
|
+
pass
|
|
1310
|
+
|
|
1311
|
+
return response
|
|
1312
|
+
except Exception as e:
|
|
1313
|
+
span.record_exception(e)
|
|
1314
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
1315
|
+
|
|
1316
|
+
# Add event for error
|
|
1317
|
+
span.add_event(
|
|
1318
|
+
"http.request.error",
|
|
1319
|
+
{
|
|
1320
|
+
"method": method,
|
|
1321
|
+
"path": path,
|
|
1322
|
+
"error.type": type(e).__name__,
|
|
1323
|
+
"error.message": str(e),
|
|
1324
|
+
},
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
# Record HTTP error metrics
|
|
1328
|
+
try:
|
|
1329
|
+
from golf.metrics import get_metrics_collector
|
|
1330
|
+
|
|
1331
|
+
metrics_collector = get_metrics_collector()
|
|
1332
|
+
|
|
1333
|
+
# Clean up path for metrics
|
|
1334
|
+
clean_path = path.split("?")[0]
|
|
1335
|
+
if clean_path.startswith("/"):
|
|
1336
|
+
clean_path = clean_path[1:] or "root"
|
|
1337
|
+
|
|
1338
|
+
metrics_collector.increment_http_request(method, 500, clean_path) # Assume 500 for exceptions
|
|
1339
|
+
metrics_collector.increment_error("http", type(e).__name__)
|
|
1340
|
+
except ImportError:
|
|
1341
|
+
pass
|
|
1342
|
+
|
|
1343
|
+
raise
|
|
1344
|
+
finally:
|
|
1345
|
+
if token:
|
|
1346
|
+
context.detach(token)
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
@asynccontextmanager
|
|
1350
|
+
async def telemetry_lifespan(mcp_instance: Any) -> AsyncGenerator[None, None]:
|
|
1351
|
+
"""Simplified lifespan for telemetry initialization and cleanup."""
|
|
1352
|
+
global _provider
|
|
1353
|
+
|
|
1354
|
+
# Initialize telemetry with the server name
|
|
1355
|
+
provider = init_telemetry(service_name=mcp_instance.name)
|
|
1356
|
+
|
|
1357
|
+
# If provider is None, telemetry is disabled
|
|
1358
|
+
if provider is None:
|
|
1359
|
+
# Just yield without any telemetry setup
|
|
1360
|
+
yield
|
|
1361
|
+
return
|
|
1362
|
+
|
|
1363
|
+
# Try to add session tracking middleware if possible
|
|
1364
|
+
try:
|
|
1365
|
+
# Try to add middleware to FastMCP app if it has Starlette app
|
|
1366
|
+
if hasattr(mcp_instance, "app") or hasattr(mcp_instance, "_app"):
|
|
1367
|
+
app = getattr(mcp_instance, "app", getattr(mcp_instance, "_app", None))
|
|
1368
|
+
if app and hasattr(app, "add_middleware"):
|
|
1369
|
+
app.add_middleware(SessionTracingMiddleware)
|
|
1370
|
+
|
|
1371
|
+
# Also try to instrument FastMCP's internal handlers
|
|
1372
|
+
if hasattr(mcp_instance, "_tool_manager") and hasattr(mcp_instance._tool_manager, "tools"):
|
|
1373
|
+
# The tools should already be instrumented when they were registered
|
|
1374
|
+
pass
|
|
1375
|
+
|
|
1376
|
+
# Try to patch FastMCP's request handling to ensure context propagation
|
|
1377
|
+
if hasattr(mcp_instance, "handle_request"):
|
|
1378
|
+
original_handle_request = mcp_instance.handle_request
|
|
1379
|
+
|
|
1380
|
+
async def traced_handle_request(*args: Any, **kwargs: Any) -> Any:
|
|
1381
|
+
tracer = get_tracer()
|
|
1382
|
+
with tracer.start_as_current_span("mcp.handle_request") as span:
|
|
1383
|
+
span.set_attribute("mcp.request.handler", "handle_request")
|
|
1384
|
+
return await original_handle_request(*args, **kwargs)
|
|
1385
|
+
|
|
1386
|
+
mcp_instance.handle_request = traced_handle_request
|
|
1387
|
+
|
|
1388
|
+
except Exception:
|
|
1389
|
+
# Silently continue if middleware setup fails
|
|
1390
|
+
import traceback
|
|
1391
|
+
|
|
1392
|
+
traceback.print_exc()
|
|
1393
|
+
|
|
1394
|
+
try:
|
|
1395
|
+
# Yield control back to FastMCP
|
|
1396
|
+
yield
|
|
1397
|
+
finally:
|
|
1398
|
+
# Cleanup - shutdown the provider
|
|
1399
|
+
if _provider and hasattr(_provider, "shutdown"):
|
|
1400
|
+
_provider.force_flush()
|
|
1401
|
+
_provider.shutdown()
|
|
1402
|
+
_provider = None
|