golf-mcp 0.1.10__py3-none-any.whl → 0.1.12__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.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +1 -1
- golf/auth/__init__.py +38 -26
- golf/auth/api_key.py +16 -23
- golf/auth/helpers.py +68 -54
- golf/auth/oauth.py +340 -277
- golf/auth/provider.py +58 -53
- golf/cli/__init__.py +1 -1
- golf/cli/main.py +202 -82
- golf/commands/__init__.py +1 -1
- golf/commands/build.py +31 -25
- golf/commands/init.py +119 -80
- golf/commands/run.py +14 -13
- golf/core/__init__.py +1 -1
- golf/core/builder.py +478 -353
- golf/core/builder_auth.py +115 -107
- golf/core/builder_telemetry.py +12 -9
- golf/core/config.py +62 -46
- golf/core/parser.py +174 -136
- golf/core/telemetry.py +169 -69
- golf/core/transformer.py +53 -55
- golf/examples/__init__.py +0 -1
- golf/examples/api_key/pre_build.py +2 -2
- golf/examples/api_key/tools/issues/create.py +35 -36
- golf/examples/api_key/tools/issues/list.py +42 -37
- golf/examples/api_key/tools/repos/list.py +50 -29
- golf/examples/api_key/tools/search/code.py +50 -37
- golf/examples/api_key/tools/users/get.py +21 -20
- golf/examples/basic/pre_build.py +4 -4
- golf/examples/basic/prompts/welcome.py +6 -7
- golf/examples/basic/resources/current_time.py +10 -9
- golf/examples/basic/resources/info.py +6 -5
- golf/examples/basic/resources/weather/common.py +16 -10
- golf/examples/basic/resources/weather/current.py +15 -11
- golf/examples/basic/resources/weather/forecast.py +15 -11
- golf/examples/basic/tools/github_user.py +19 -21
- golf/examples/basic/tools/hello.py +10 -6
- golf/examples/basic/tools/payments/charge.py +34 -25
- golf/examples/basic/tools/payments/common.py +8 -6
- golf/examples/basic/tools/payments/refund.py +29 -25
- golf/telemetry/__init__.py +6 -6
- golf/telemetry/instrumentation.py +781 -276
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
- golf_mcp-0.1.12.dist-info/RECORD +55 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
- golf_mcp-0.1.10.dist-info/RECORD +0 -55
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -1,44 +1,46 @@
|
|
|
1
1
|
"""Component-level OpenTelemetry instrumentation for Golf-built servers."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
3
5
|
import os
|
|
4
6
|
import sys
|
|
5
|
-
import
|
|
6
|
-
from typing import Callable, Optional, TypeVar
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from contextlib import asynccontextmanager
|
|
8
|
-
import
|
|
9
|
+
from typing import TypeVar
|
|
9
10
|
|
|
10
|
-
from opentelemetry import trace
|
|
11
|
+
from opentelemetry import baggage, trace
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
13
|
+
from opentelemetry.sdk.resources import Resource
|
|
11
14
|
from opentelemetry.sdk.trace import TracerProvider
|
|
12
15
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
13
|
-
from opentelemetry.
|
|
14
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
15
|
-
from opentelemetry.trace import Status, StatusCode, Span
|
|
16
|
-
from opentelemetry import baggage
|
|
16
|
+
from opentelemetry.trace import Status, StatusCode
|
|
17
17
|
|
|
18
|
-
T = TypeVar(
|
|
18
|
+
T = TypeVar("T")
|
|
19
19
|
|
|
20
20
|
# Global tracer instance
|
|
21
|
-
_tracer:
|
|
22
|
-
_provider:
|
|
23
|
-
|
|
21
|
+
_tracer: trace.Tracer | None = None
|
|
22
|
+
_provider: TracerProvider | None = None
|
|
23
|
+
|
|
24
24
|
|
|
25
|
-
def init_telemetry(service_name: str = "golf-mcp-server") ->
|
|
25
|
+
def init_telemetry(service_name: str = "golf-mcp-server") -> TracerProvider | None:
|
|
26
26
|
"""Initialize OpenTelemetry with environment-based configuration.
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
Returns None if required environment variables are not set.
|
|
29
29
|
"""
|
|
30
30
|
global _provider
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
# Check for required environment variables based on exporter type
|
|
33
33
|
exporter_type = os.environ.get("OTEL_TRACES_EXPORTER", "console").lower()
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
# For OTLP HTTP exporter, check if endpoint is configured
|
|
36
36
|
if exporter_type == "otlp_http":
|
|
37
37
|
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
38
38
|
if not endpoint:
|
|
39
|
-
print(
|
|
39
|
+
print(
|
|
40
|
+
"[WARNING] OpenTelemetry tracing is disabled: OTEL_EXPORTER_OTLP_ENDPOINT is not set for OTLP HTTP exporter"
|
|
41
|
+
)
|
|
40
42
|
return None
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
# Create resource with service information
|
|
43
45
|
resource_attributes = {
|
|
44
46
|
"service.name": os.environ.get("OTEL_SERVICE_NAME", service_name),
|
|
@@ -46,16 +48,18 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
46
48
|
"service.instance.id": os.environ.get("SERVICE_INSTANCE_ID", "default"),
|
|
47
49
|
}
|
|
48
50
|
resource = Resource.create(resource_attributes)
|
|
49
|
-
|
|
51
|
+
|
|
50
52
|
# Create provider
|
|
51
53
|
provider = TracerProvider(resource=resource)
|
|
52
|
-
|
|
54
|
+
|
|
53
55
|
# Configure exporter based on type
|
|
54
56
|
try:
|
|
55
57
|
if exporter_type == "otlp_http":
|
|
56
|
-
endpoint = os.environ.get(
|
|
58
|
+
endpoint = os.environ.get(
|
|
59
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
|
|
60
|
+
)
|
|
57
61
|
headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
58
|
-
|
|
62
|
+
|
|
59
63
|
# Parse headers if provided
|
|
60
64
|
header_dict = {}
|
|
61
65
|
if headers:
|
|
@@ -63,19 +67,19 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
63
67
|
if "=" in header:
|
|
64
68
|
key, value = header.split("=", 1)
|
|
65
69
|
header_dict[key.strip()] = value.strip()
|
|
66
|
-
|
|
70
|
+
|
|
67
71
|
exporter = OTLPSpanExporter(
|
|
68
|
-
endpoint=endpoint,
|
|
69
|
-
headers=header_dict if header_dict else None
|
|
72
|
+
endpoint=endpoint, headers=header_dict if header_dict else None
|
|
70
73
|
)
|
|
71
74
|
else:
|
|
72
75
|
# Default to console exporter
|
|
73
76
|
exporter = ConsoleSpanExporter(out=sys.stderr)
|
|
74
|
-
except Exception
|
|
77
|
+
except Exception:
|
|
75
78
|
import traceback
|
|
79
|
+
|
|
76
80
|
traceback.print_exc()
|
|
77
81
|
raise
|
|
78
|
-
|
|
82
|
+
|
|
79
83
|
# Add batch processor for better performance
|
|
80
84
|
try:
|
|
81
85
|
processor = BatchSpanProcessor(
|
|
@@ -83,27 +87,32 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
83
87
|
max_queue_size=2048,
|
|
84
88
|
schedule_delay_millis=1000, # Export every 1 second instead of default 5 seconds
|
|
85
89
|
max_export_batch_size=512,
|
|
86
|
-
export_timeout_millis=5000
|
|
90
|
+
export_timeout_millis=5000,
|
|
87
91
|
)
|
|
88
92
|
provider.add_span_processor(processor)
|
|
89
|
-
except Exception
|
|
93
|
+
except Exception:
|
|
90
94
|
import traceback
|
|
95
|
+
|
|
91
96
|
traceback.print_exc()
|
|
92
97
|
raise
|
|
93
|
-
|
|
98
|
+
|
|
94
99
|
# Set as global provider
|
|
95
100
|
try:
|
|
96
101
|
# Check if a provider is already set to avoid the warning
|
|
97
102
|
existing_provider = trace.get_tracer_provider()
|
|
98
|
-
if
|
|
103
|
+
if (
|
|
104
|
+
existing_provider is None
|
|
105
|
+
or str(type(existing_provider).__name__) == "ProxyTracerProvider"
|
|
106
|
+
):
|
|
99
107
|
# Only set if no provider exists or it's the default proxy provider
|
|
100
108
|
trace.set_tracer_provider(provider)
|
|
101
109
|
_provider = provider
|
|
102
|
-
except Exception
|
|
110
|
+
except Exception:
|
|
103
111
|
import traceback
|
|
112
|
+
|
|
104
113
|
traceback.print_exc()
|
|
105
114
|
raise
|
|
106
|
-
|
|
115
|
+
|
|
107
116
|
# Create a test span to verify everything is working
|
|
108
117
|
try:
|
|
109
118
|
test_tracer = provider.get_tracer("golf.telemetry.test", "1.0.0")
|
|
@@ -111,430 +120,926 @@ def init_telemetry(service_name: str = "golf-mcp-server") -> Optional[TracerProv
|
|
|
111
120
|
span.set_attribute("test", True)
|
|
112
121
|
span.set_attribute("service.name", service_name)
|
|
113
122
|
span.set_attribute("exporter.type", exporter_type)
|
|
114
|
-
span.set_attribute(
|
|
115
|
-
|
|
123
|
+
span.set_attribute(
|
|
124
|
+
"endpoint", os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "not set")
|
|
125
|
+
)
|
|
126
|
+
except Exception:
|
|
116
127
|
import traceback
|
|
128
|
+
|
|
117
129
|
traceback.print_exc()
|
|
118
|
-
|
|
130
|
+
|
|
119
131
|
return provider
|
|
120
132
|
|
|
133
|
+
|
|
121
134
|
def get_tracer() -> trace.Tracer:
|
|
122
135
|
"""Get or create the global tracer instance."""
|
|
123
136
|
global _tracer, _provider
|
|
124
|
-
|
|
137
|
+
|
|
125
138
|
# If no provider is set, telemetry is disabled - return no-op tracer
|
|
126
139
|
if _provider is None:
|
|
127
140
|
return trace.get_tracer("golf.mcp.components.noop", "1.0.0")
|
|
128
|
-
|
|
141
|
+
|
|
129
142
|
if _tracer is None:
|
|
130
143
|
_tracer = trace.get_tracer("golf.mcp.components", "1.0.0")
|
|
131
144
|
return _tracer
|
|
132
145
|
|
|
133
|
-
def _add_component_attributes(span: Span, component_type: str, component_name: str, **kwargs):
|
|
134
|
-
"""Add standard component attributes to a span."""
|
|
135
|
-
span.set_attribute("mcp.component.type", component_type)
|
|
136
|
-
span.set_attribute("mcp.component.name", component_name)
|
|
137
|
-
|
|
138
|
-
# Add any additional attributes
|
|
139
|
-
for key, value in kwargs.items():
|
|
140
|
-
if value is not None:
|
|
141
|
-
span.set_attribute(f"mcp.component.{key}", str(value))
|
|
142
146
|
|
|
143
147
|
def instrument_tool(func: Callable[..., T], tool_name: str) -> Callable[..., T]:
|
|
144
148
|
"""Instrument a tool function with OpenTelemetry tracing."""
|
|
145
149
|
global _provider
|
|
146
|
-
|
|
150
|
+
|
|
147
151
|
# If telemetry is disabled, return the original function
|
|
148
152
|
if _provider is None:
|
|
149
153
|
return func
|
|
150
|
-
|
|
154
|
+
|
|
151
155
|
tracer = get_tracer()
|
|
152
|
-
|
|
156
|
+
|
|
157
|
+
# Add debug logging
|
|
158
|
+
print(
|
|
159
|
+
f"[TELEMETRY DEBUG] Instrumenting tool: {tool_name} (function: {func.__name__})"
|
|
160
|
+
)
|
|
161
|
+
|
|
153
162
|
@functools.wraps(func)
|
|
154
163
|
async def async_wrapper(*args, **kwargs):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
print(f"[TELEMETRY DEBUG] Executing async tool: {tool_name}")
|
|
165
|
+
|
|
166
|
+
# Create a more descriptive span name
|
|
167
|
+
span_name = f"mcp.tool.{tool_name}.execute"
|
|
168
|
+
|
|
169
|
+
# start_as_current_span automatically uses the current context and manages it
|
|
170
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
171
|
+
# Add comprehensive attributes
|
|
172
|
+
span.set_attribute("mcp.component.type", "tool")
|
|
173
|
+
span.set_attribute("mcp.component.name", tool_name)
|
|
174
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
175
|
+
span.set_attribute("mcp.tool.function", func.__name__)
|
|
176
|
+
span.set_attribute(
|
|
177
|
+
"mcp.tool.module",
|
|
178
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Add execution context
|
|
182
|
+
span.set_attribute("mcp.execution.args_count", len(args))
|
|
183
|
+
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
184
|
+
span.set_attribute("mcp.execution.async", True)
|
|
185
|
+
|
|
186
|
+
# Extract Context parameter if present
|
|
187
|
+
ctx = kwargs.get("ctx")
|
|
168
188
|
if ctx:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
189
|
+
# Only extract known MCP context attributes
|
|
190
|
+
ctx_attrs = [
|
|
191
|
+
"request_id",
|
|
192
|
+
"session_id",
|
|
193
|
+
"client_id",
|
|
194
|
+
"user_id",
|
|
195
|
+
"tenant_id",
|
|
196
|
+
]
|
|
197
|
+
for attr in ctx_attrs:
|
|
198
|
+
if hasattr(ctx, attr):
|
|
199
|
+
value = getattr(ctx, attr)
|
|
200
|
+
if value is not None:
|
|
178
201
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
179
|
-
|
|
202
|
+
|
|
180
203
|
# Also check baggage for session ID
|
|
181
204
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
182
205
|
if session_id_from_baggage:
|
|
183
206
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
184
|
-
|
|
207
|
+
|
|
185
208
|
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
186
209
|
for i, arg in enumerate(args):
|
|
187
|
-
if isinstance(arg,
|
|
188
|
-
span.set_attribute(f"tool.arg.{i}", str(arg))
|
|
189
|
-
elif hasattr(arg,
|
|
210
|
+
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
211
|
+
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
212
|
+
elif hasattr(arg, "__dict__"):
|
|
190
213
|
# For objects, just record the type
|
|
191
|
-
span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
|
|
192
|
-
|
|
193
|
-
# Add named arguments
|
|
214
|
+
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
215
|
+
|
|
216
|
+
# Add named arguments with better naming
|
|
194
217
|
for key, value in kwargs.items():
|
|
195
|
-
if key !=
|
|
218
|
+
if key != "ctx":
|
|
196
219
|
if value is None:
|
|
197
|
-
span.set_attribute(f"tool.
|
|
198
|
-
elif isinstance(value,
|
|
199
|
-
span.set_attribute(f"tool.
|
|
200
|
-
elif isinstance(value,
|
|
201
|
-
span.set_attribute(f"tool.
|
|
220
|
+
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
221
|
+
elif isinstance(value, str | int | float | bool):
|
|
222
|
+
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
223
|
+
elif isinstance(value, list | tuple):
|
|
224
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
225
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
202
226
|
elif isinstance(value, dict):
|
|
203
|
-
span.set_attribute(f"tool.
|
|
227
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
228
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
229
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
230
|
+
if len(value) > 0 and len(value) <= 5:
|
|
231
|
+
keys_list = list(value.keys())[:5]
|
|
232
|
+
# Limit key length and join
|
|
233
|
+
truncated_keys = [
|
|
234
|
+
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
235
|
+
for k in keys_list
|
|
236
|
+
]
|
|
237
|
+
span.set_attribute(
|
|
238
|
+
f"mcp.tool.input.{key}.sample_keys",
|
|
239
|
+
",".join(truncated_keys),
|
|
240
|
+
)
|
|
204
241
|
else:
|
|
205
242
|
# For other types, at least record the type
|
|
206
|
-
span.set_attribute(
|
|
207
|
-
|
|
243
|
+
span.set_attribute(
|
|
244
|
+
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Add event for tool execution start
|
|
248
|
+
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
249
|
+
|
|
250
|
+
print(
|
|
251
|
+
f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
|
|
252
|
+
)
|
|
253
|
+
|
|
208
254
|
try:
|
|
209
255
|
result = await func(*args, **kwargs)
|
|
210
256
|
span.set_status(Status(StatusCode.OK))
|
|
211
|
-
|
|
212
|
-
#
|
|
257
|
+
|
|
258
|
+
# Add event for successful completion
|
|
259
|
+
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
260
|
+
|
|
261
|
+
# Capture result metadata with better structure
|
|
213
262
|
if result is not None:
|
|
214
|
-
if isinstance(result,
|
|
215
|
-
span.set_attribute("tool.result", str(result))
|
|
263
|
+
if isinstance(result, str | int | float | bool):
|
|
264
|
+
span.set_attribute("mcp.tool.result.value", str(result))
|
|
265
|
+
span.set_attribute(
|
|
266
|
+
"mcp.tool.result.type", type(result).__name__
|
|
267
|
+
)
|
|
216
268
|
elif isinstance(result, list):
|
|
217
|
-
span.set_attribute("tool.result.count", len(result))
|
|
218
|
-
span.set_attribute("tool.result.type", "
|
|
269
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
270
|
+
span.set_attribute("mcp.tool.result.type", "array")
|
|
219
271
|
elif isinstance(result, dict):
|
|
220
|
-
span.set_attribute("tool.result.
|
|
221
|
-
span.set_attribute("tool.result.type", "
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
272
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
273
|
+
span.set_attribute("mcp.tool.result.type", "object")
|
|
274
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
275
|
+
if len(result) > 0 and len(result) <= 5:
|
|
276
|
+
keys_list = list(result.keys())[:5]
|
|
277
|
+
# Limit key length and join
|
|
278
|
+
truncated_keys = [
|
|
279
|
+
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
280
|
+
for k in keys_list
|
|
281
|
+
]
|
|
282
|
+
span.set_attribute(
|
|
283
|
+
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
284
|
+
)
|
|
285
|
+
elif hasattr(result, "__len__"):
|
|
286
|
+
span.set_attribute("mcp.tool.result.length", len(result))
|
|
287
|
+
|
|
225
288
|
# For any result, record its type
|
|
226
|
-
span.set_attribute("tool.result.class", type(result).__name__)
|
|
227
|
-
|
|
289
|
+
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
290
|
+
|
|
291
|
+
print(
|
|
292
|
+
f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
|
|
293
|
+
)
|
|
228
294
|
return result
|
|
229
295
|
except Exception as e:
|
|
230
296
|
span.record_exception(e)
|
|
231
297
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
298
|
+
|
|
299
|
+
# Add event for error
|
|
300
|
+
span.add_event(
|
|
301
|
+
"tool.execution.error",
|
|
302
|
+
{
|
|
303
|
+
"tool.name": tool_name,
|
|
304
|
+
"error.type": type(e).__name__,
|
|
305
|
+
"error.message": str(e),
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
|
|
232
309
|
raise
|
|
233
|
-
|
|
234
|
-
# End the span and detach context
|
|
235
|
-
span.end()
|
|
236
|
-
context.detach(token)
|
|
237
|
-
|
|
238
|
-
# Force flush the provider to ensure spans are exported
|
|
239
|
-
global _provider
|
|
240
|
-
if _provider:
|
|
241
|
-
try:
|
|
242
|
-
_provider.force_flush(timeout_millis=1000)
|
|
243
|
-
except Exception as e:
|
|
244
|
-
pass
|
|
245
|
-
|
|
310
|
+
|
|
246
311
|
@functools.wraps(func)
|
|
247
312
|
def sync_wrapper(*args, **kwargs):
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
313
|
+
print(f"[TELEMETRY DEBUG] Executing sync tool: {tool_name}")
|
|
314
|
+
|
|
315
|
+
# Create a more descriptive span name
|
|
316
|
+
span_name = f"mcp.tool.{tool_name}.execute"
|
|
317
|
+
|
|
318
|
+
# start_as_current_span automatically uses the current context and manages it
|
|
319
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
320
|
+
# Add comprehensive attributes
|
|
321
|
+
span.set_attribute("mcp.component.type", "tool")
|
|
322
|
+
span.set_attribute("mcp.component.name", tool_name)
|
|
323
|
+
span.set_attribute("mcp.tool.name", tool_name)
|
|
324
|
+
span.set_attribute("mcp.tool.function", func.__name__)
|
|
325
|
+
span.set_attribute(
|
|
326
|
+
"mcp.tool.module",
|
|
327
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Add execution context
|
|
331
|
+
span.set_attribute("mcp.execution.args_count", len(args))
|
|
332
|
+
span.set_attribute("mcp.execution.kwargs_count", len(kwargs))
|
|
333
|
+
span.set_attribute("mcp.execution.async", False)
|
|
334
|
+
|
|
335
|
+
# Extract Context parameter if present
|
|
336
|
+
ctx = kwargs.get("ctx")
|
|
261
337
|
if ctx:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
338
|
+
# Only extract known MCP context attributes
|
|
339
|
+
ctx_attrs = [
|
|
340
|
+
"request_id",
|
|
341
|
+
"session_id",
|
|
342
|
+
"client_id",
|
|
343
|
+
"user_id",
|
|
344
|
+
"tenant_id",
|
|
345
|
+
]
|
|
346
|
+
for attr in ctx_attrs:
|
|
347
|
+
if hasattr(ctx, attr):
|
|
348
|
+
value = getattr(ctx, attr)
|
|
349
|
+
if value is not None:
|
|
271
350
|
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
272
|
-
|
|
351
|
+
|
|
273
352
|
# Also check baggage for session ID
|
|
274
353
|
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
275
354
|
if session_id_from_baggage:
|
|
276
355
|
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
277
|
-
|
|
356
|
+
|
|
278
357
|
# Add tool arguments as span attributes (be careful with sensitive data)
|
|
279
358
|
for i, arg in enumerate(args):
|
|
280
|
-
if isinstance(arg,
|
|
281
|
-
span.set_attribute(f"tool.arg.{i}", str(arg))
|
|
282
|
-
elif hasattr(arg,
|
|
359
|
+
if isinstance(arg, str | int | float | bool) or arg is None:
|
|
360
|
+
span.set_attribute(f"mcp.tool.arg.{i}", str(arg))
|
|
361
|
+
elif hasattr(arg, "__dict__"):
|
|
283
362
|
# For objects, just record the type
|
|
284
|
-
span.set_attribute(f"tool.arg.{i}.type", type(arg).__name__)
|
|
285
|
-
|
|
286
|
-
# Add named arguments
|
|
363
|
+
span.set_attribute(f"mcp.tool.arg.{i}.type", type(arg).__name__)
|
|
364
|
+
|
|
365
|
+
# Add named arguments with better naming
|
|
287
366
|
for key, value in kwargs.items():
|
|
288
|
-
if key !=
|
|
367
|
+
if key != "ctx":
|
|
289
368
|
if value is None:
|
|
290
|
-
span.set_attribute(f"tool.
|
|
291
|
-
elif isinstance(value,
|
|
292
|
-
span.set_attribute(f"tool.
|
|
293
|
-
elif isinstance(value,
|
|
294
|
-
span.set_attribute(f"tool.
|
|
369
|
+
span.set_attribute(f"mcp.tool.input.{key}", "null")
|
|
370
|
+
elif isinstance(value, str | int | float | bool):
|
|
371
|
+
span.set_attribute(f"mcp.tool.input.{key}", str(value))
|
|
372
|
+
elif isinstance(value, list | tuple):
|
|
373
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
374
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "array")
|
|
295
375
|
elif isinstance(value, dict):
|
|
296
|
-
span.set_attribute(f"tool.
|
|
376
|
+
span.set_attribute(f"mcp.tool.input.{key}.count", len(value))
|
|
377
|
+
span.set_attribute(f"mcp.tool.input.{key}.type", "object")
|
|
378
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
379
|
+
if len(value) > 0 and len(value) <= 5:
|
|
380
|
+
keys_list = list(value.keys())[:5]
|
|
381
|
+
# Limit key length and join
|
|
382
|
+
truncated_keys = [
|
|
383
|
+
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
384
|
+
for k in keys_list
|
|
385
|
+
]
|
|
386
|
+
span.set_attribute(
|
|
387
|
+
f"mcp.tool.input.{key}.sample_keys",
|
|
388
|
+
",".join(truncated_keys),
|
|
389
|
+
)
|
|
297
390
|
else:
|
|
298
391
|
# For other types, at least record the type
|
|
299
|
-
span.set_attribute(
|
|
300
|
-
|
|
392
|
+
span.set_attribute(
|
|
393
|
+
f"mcp.tool.input.{key}.type", type(value).__name__
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Add event for tool execution start
|
|
397
|
+
span.add_event("tool.execution.started", {"tool.name": tool_name})
|
|
398
|
+
|
|
399
|
+
print(
|
|
400
|
+
f"[TELEMETRY DEBUG] Tool span created: {span_name} (span_id: {span.get_span_context().span_id:016x})"
|
|
401
|
+
)
|
|
402
|
+
|
|
301
403
|
try:
|
|
302
404
|
result = func(*args, **kwargs)
|
|
303
405
|
span.set_status(Status(StatusCode.OK))
|
|
304
|
-
|
|
305
|
-
#
|
|
406
|
+
|
|
407
|
+
# Add event for successful completion
|
|
408
|
+
span.add_event("tool.execution.completed", {"tool.name": tool_name})
|
|
409
|
+
|
|
410
|
+
# Capture result metadata with better structure
|
|
306
411
|
if result is not None:
|
|
307
|
-
if isinstance(result,
|
|
308
|
-
span.set_attribute("tool.result", str(result))
|
|
412
|
+
if isinstance(result, str | int | float | bool):
|
|
413
|
+
span.set_attribute("mcp.tool.result.value", str(result))
|
|
414
|
+
span.set_attribute(
|
|
415
|
+
"mcp.tool.result.type", type(result).__name__
|
|
416
|
+
)
|
|
309
417
|
elif isinstance(result, list):
|
|
310
|
-
span.set_attribute("tool.result.count", len(result))
|
|
311
|
-
span.set_attribute("tool.result.type", "
|
|
418
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
419
|
+
span.set_attribute("mcp.tool.result.type", "array")
|
|
312
420
|
elif isinstance(result, dict):
|
|
313
|
-
span.set_attribute("tool.result.
|
|
314
|
-
span.set_attribute("tool.result.type", "
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
421
|
+
span.set_attribute("mcp.tool.result.count", len(result))
|
|
422
|
+
span.set_attribute("mcp.tool.result.type", "object")
|
|
423
|
+
# Only show first few keys to avoid exceeding attribute limits
|
|
424
|
+
if len(result) > 0 and len(result) <= 5:
|
|
425
|
+
keys_list = list(result.keys())[:5]
|
|
426
|
+
# Limit key length and join
|
|
427
|
+
truncated_keys = [
|
|
428
|
+
str(k)[:20] + "..." if len(str(k)) > 20 else str(k)
|
|
429
|
+
for k in keys_list
|
|
430
|
+
]
|
|
431
|
+
span.set_attribute(
|
|
432
|
+
"mcp.tool.result.sample_keys", ",".join(truncated_keys)
|
|
433
|
+
)
|
|
434
|
+
elif hasattr(result, "__len__"):
|
|
435
|
+
span.set_attribute("mcp.tool.result.length", len(result))
|
|
436
|
+
|
|
318
437
|
# For any result, record its type
|
|
319
|
-
span.set_attribute("tool.result.class", type(result).__name__)
|
|
320
|
-
|
|
438
|
+
span.set_attribute("mcp.tool.result.class", type(result).__name__)
|
|
439
|
+
|
|
440
|
+
print(
|
|
441
|
+
f"[TELEMETRY DEBUG] Tool execution completed successfully: {tool_name}"
|
|
442
|
+
)
|
|
321
443
|
return result
|
|
322
444
|
except Exception as e:
|
|
323
445
|
span.record_exception(e)
|
|
324
446
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
447
|
+
|
|
448
|
+
# Add event for error
|
|
449
|
+
span.add_event(
|
|
450
|
+
"tool.execution.error",
|
|
451
|
+
{
|
|
452
|
+
"tool.name": tool_name,
|
|
453
|
+
"error.type": type(e).__name__,
|
|
454
|
+
"error.message": str(e),
|
|
455
|
+
},
|
|
456
|
+
)
|
|
457
|
+
print(f"[TELEMETRY DEBUG] Tool execution failed: {tool_name} - {e}")
|
|
325
458
|
raise
|
|
326
|
-
|
|
327
|
-
# End the span and detach context
|
|
328
|
-
span.end()
|
|
329
|
-
context.detach(token)
|
|
330
|
-
|
|
331
|
-
# Force flush the provider to ensure spans are exported
|
|
332
|
-
global _provider
|
|
333
|
-
if _provider:
|
|
334
|
-
try:
|
|
335
|
-
_provider.force_flush(timeout_millis=1000)
|
|
336
|
-
except Exception as e:
|
|
337
|
-
pass
|
|
338
|
-
|
|
459
|
+
|
|
339
460
|
# Return appropriate wrapper based on function type
|
|
340
461
|
if asyncio.iscoroutinefunction(func):
|
|
341
462
|
return async_wrapper
|
|
342
463
|
else:
|
|
343
464
|
return sync_wrapper
|
|
344
465
|
|
|
466
|
+
|
|
345
467
|
def instrument_resource(func: Callable[..., T], resource_uri: str) -> Callable[..., T]:
|
|
346
468
|
"""Instrument a resource function with OpenTelemetry tracing."""
|
|
347
469
|
global _provider
|
|
348
|
-
|
|
470
|
+
|
|
349
471
|
# If telemetry is disabled, return the original function
|
|
350
472
|
if _provider is None:
|
|
351
473
|
return func
|
|
352
|
-
|
|
474
|
+
|
|
353
475
|
tracer = get_tracer()
|
|
354
|
-
|
|
476
|
+
|
|
355
477
|
# Determine if this is a template based on URI pattern
|
|
356
|
-
is_template =
|
|
357
|
-
|
|
478
|
+
is_template = "{" in resource_uri
|
|
479
|
+
|
|
358
480
|
@functools.wraps(func)
|
|
359
481
|
async def async_wrapper(*args, **kwargs):
|
|
360
|
-
|
|
482
|
+
# Create a more descriptive span name
|
|
483
|
+
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
361
484
|
with tracer.start_as_current_span(span_name) as span:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
485
|
+
# Add comprehensive attributes
|
|
486
|
+
span.set_attribute("mcp.component.type", "resource")
|
|
487
|
+
span.set_attribute("mcp.component.name", resource_uri)
|
|
488
|
+
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
489
|
+
span.set_attribute("mcp.resource.is_template", is_template)
|
|
490
|
+
span.set_attribute("mcp.resource.function", func.__name__)
|
|
491
|
+
span.set_attribute(
|
|
492
|
+
"mcp.resource.module",
|
|
493
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
494
|
+
)
|
|
495
|
+
span.set_attribute("mcp.execution.async", True)
|
|
496
|
+
|
|
365
497
|
# Extract Context parameter if present
|
|
366
|
-
ctx = kwargs.get(
|
|
367
|
-
if ctx
|
|
368
|
-
|
|
369
|
-
|
|
498
|
+
ctx = kwargs.get("ctx")
|
|
499
|
+
if ctx:
|
|
500
|
+
# Only extract known MCP context attributes
|
|
501
|
+
ctx_attrs = [
|
|
502
|
+
"request_id",
|
|
503
|
+
"session_id",
|
|
504
|
+
"client_id",
|
|
505
|
+
"user_id",
|
|
506
|
+
"tenant_id",
|
|
507
|
+
]
|
|
508
|
+
for attr in ctx_attrs:
|
|
509
|
+
if hasattr(ctx, attr):
|
|
510
|
+
value = getattr(ctx, attr)
|
|
511
|
+
if value is not None:
|
|
512
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
513
|
+
|
|
514
|
+
# Also check baggage for session ID
|
|
515
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
516
|
+
if session_id_from_baggage:
|
|
517
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
518
|
+
|
|
519
|
+
# Add event for resource read start
|
|
520
|
+
span.add_event("resource.read.started", {"resource.uri": resource_uri})
|
|
521
|
+
|
|
370
522
|
try:
|
|
371
523
|
result = await func(*args, **kwargs)
|
|
372
524
|
span.set_status(Status(StatusCode.OK))
|
|
373
|
-
|
|
374
|
-
# Add
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
525
|
+
|
|
526
|
+
# Add event for successful read
|
|
527
|
+
span.add_event(
|
|
528
|
+
"resource.read.completed", {"resource.uri": resource_uri}
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Add result metadata
|
|
532
|
+
if hasattr(result, "__len__"):
|
|
533
|
+
span.set_attribute("mcp.resource.result.size", len(result))
|
|
534
|
+
|
|
535
|
+
# Determine content type if possible
|
|
536
|
+
if isinstance(result, str):
|
|
537
|
+
span.set_attribute("mcp.resource.result.type", "text")
|
|
538
|
+
span.set_attribute("mcp.resource.result.length", len(result))
|
|
539
|
+
elif isinstance(result, bytes):
|
|
540
|
+
span.set_attribute("mcp.resource.result.type", "binary")
|
|
541
|
+
span.set_attribute("mcp.resource.result.size_bytes", len(result))
|
|
542
|
+
elif isinstance(result, dict):
|
|
543
|
+
span.set_attribute("mcp.resource.result.type", "object")
|
|
544
|
+
span.set_attribute("mcp.resource.result.keys_count", len(result))
|
|
545
|
+
elif isinstance(result, list):
|
|
546
|
+
span.set_attribute("mcp.resource.result.type", "array")
|
|
547
|
+
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
548
|
+
|
|
378
549
|
return result
|
|
379
550
|
except Exception as e:
|
|
380
551
|
span.record_exception(e)
|
|
381
552
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
553
|
+
|
|
554
|
+
# Add event for error
|
|
555
|
+
span.add_event(
|
|
556
|
+
"resource.read.error",
|
|
557
|
+
{
|
|
558
|
+
"resource.uri": resource_uri,
|
|
559
|
+
"error.type": type(e).__name__,
|
|
560
|
+
"error.message": str(e),
|
|
561
|
+
},
|
|
562
|
+
)
|
|
382
563
|
raise
|
|
383
|
-
|
|
564
|
+
|
|
384
565
|
@functools.wraps(func)
|
|
385
566
|
def sync_wrapper(*args, **kwargs):
|
|
386
|
-
|
|
567
|
+
# Create a more descriptive span name
|
|
568
|
+
span_name = f"mcp.resource.{'template' if is_template else 'static'}.read"
|
|
387
569
|
with tracer.start_as_current_span(span_name) as span:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
570
|
+
# Add comprehensive attributes
|
|
571
|
+
span.set_attribute("mcp.component.type", "resource")
|
|
572
|
+
span.set_attribute("mcp.component.name", resource_uri)
|
|
573
|
+
span.set_attribute("mcp.resource.uri", resource_uri)
|
|
574
|
+
span.set_attribute("mcp.resource.is_template", is_template)
|
|
575
|
+
span.set_attribute("mcp.resource.function", func.__name__)
|
|
576
|
+
span.set_attribute(
|
|
577
|
+
"mcp.resource.module",
|
|
578
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
579
|
+
)
|
|
580
|
+
span.set_attribute("mcp.execution.async", False)
|
|
581
|
+
|
|
391
582
|
# Extract Context parameter if present
|
|
392
|
-
ctx = kwargs.get(
|
|
393
|
-
if ctx
|
|
394
|
-
|
|
395
|
-
|
|
583
|
+
ctx = kwargs.get("ctx")
|
|
584
|
+
if ctx:
|
|
585
|
+
# Only extract known MCP context attributes
|
|
586
|
+
ctx_attrs = [
|
|
587
|
+
"request_id",
|
|
588
|
+
"session_id",
|
|
589
|
+
"client_id",
|
|
590
|
+
"user_id",
|
|
591
|
+
"tenant_id",
|
|
592
|
+
]
|
|
593
|
+
for attr in ctx_attrs:
|
|
594
|
+
if hasattr(ctx, attr):
|
|
595
|
+
value = getattr(ctx, attr)
|
|
596
|
+
if value is not None:
|
|
597
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
598
|
+
|
|
599
|
+
# Also check baggage for session ID
|
|
600
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
601
|
+
if session_id_from_baggage:
|
|
602
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
603
|
+
|
|
604
|
+
# Add event for resource read start
|
|
605
|
+
span.add_event("resource.read.started", {"resource.uri": resource_uri})
|
|
606
|
+
|
|
396
607
|
try:
|
|
397
608
|
result = func(*args, **kwargs)
|
|
398
609
|
span.set_status(Status(StatusCode.OK))
|
|
399
|
-
|
|
400
|
-
# Add
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
610
|
+
|
|
611
|
+
# Add event for successful read
|
|
612
|
+
span.add_event(
|
|
613
|
+
"resource.read.completed", {"resource.uri": resource_uri}
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Add result metadata
|
|
617
|
+
if hasattr(result, "__len__"):
|
|
618
|
+
span.set_attribute("mcp.resource.result.size", len(result))
|
|
619
|
+
|
|
620
|
+
# Determine content type if possible
|
|
621
|
+
if isinstance(result, str):
|
|
622
|
+
span.set_attribute("mcp.resource.result.type", "text")
|
|
623
|
+
span.set_attribute("mcp.resource.result.length", len(result))
|
|
624
|
+
elif isinstance(result, bytes):
|
|
625
|
+
span.set_attribute("mcp.resource.result.type", "binary")
|
|
626
|
+
span.set_attribute("mcp.resource.result.size_bytes", len(result))
|
|
627
|
+
elif isinstance(result, dict):
|
|
628
|
+
span.set_attribute("mcp.resource.result.type", "object")
|
|
629
|
+
span.set_attribute("mcp.resource.result.keys_count", len(result))
|
|
630
|
+
elif isinstance(result, list):
|
|
631
|
+
span.set_attribute("mcp.resource.result.type", "array")
|
|
632
|
+
span.set_attribute("mcp.resource.result.items_count", len(result))
|
|
633
|
+
|
|
404
634
|
return result
|
|
405
635
|
except Exception as e:
|
|
406
636
|
span.record_exception(e)
|
|
407
637
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
638
|
+
|
|
639
|
+
# Add event for error
|
|
640
|
+
span.add_event(
|
|
641
|
+
"resource.read.error",
|
|
642
|
+
{
|
|
643
|
+
"resource.uri": resource_uri,
|
|
644
|
+
"error.type": type(e).__name__,
|
|
645
|
+
"error.message": str(e),
|
|
646
|
+
},
|
|
647
|
+
)
|
|
408
648
|
raise
|
|
409
|
-
|
|
649
|
+
|
|
410
650
|
if asyncio.iscoroutinefunction(func):
|
|
411
651
|
return async_wrapper
|
|
412
652
|
else:
|
|
413
653
|
return sync_wrapper
|
|
414
654
|
|
|
655
|
+
|
|
415
656
|
def instrument_prompt(func: Callable[..., T], prompt_name: str) -> Callable[..., T]:
|
|
416
657
|
"""Instrument a prompt function with OpenTelemetry tracing."""
|
|
417
658
|
global _provider
|
|
418
|
-
|
|
659
|
+
|
|
419
660
|
# If telemetry is disabled, return the original function
|
|
420
661
|
if _provider is None:
|
|
421
662
|
return func
|
|
422
|
-
|
|
663
|
+
|
|
423
664
|
tracer = get_tracer()
|
|
424
|
-
|
|
665
|
+
|
|
425
666
|
@functools.wraps(func)
|
|
426
667
|
async def async_wrapper(*args, **kwargs):
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
668
|
+
# Create a more descriptive span name
|
|
669
|
+
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
670
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
671
|
+
# Add comprehensive attributes
|
|
672
|
+
span.set_attribute("mcp.component.type", "prompt")
|
|
673
|
+
span.set_attribute("mcp.component.name", prompt_name)
|
|
674
|
+
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
675
|
+
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
676
|
+
span.set_attribute(
|
|
677
|
+
"mcp.prompt.module",
|
|
678
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
679
|
+
)
|
|
680
|
+
span.set_attribute("mcp.execution.async", True)
|
|
681
|
+
|
|
430
682
|
# Extract Context parameter if present
|
|
431
|
-
ctx = kwargs.get(
|
|
432
|
-
if ctx
|
|
433
|
-
|
|
434
|
-
|
|
683
|
+
ctx = kwargs.get("ctx")
|
|
684
|
+
if ctx:
|
|
685
|
+
# Only extract known MCP context attributes
|
|
686
|
+
ctx_attrs = [
|
|
687
|
+
"request_id",
|
|
688
|
+
"session_id",
|
|
689
|
+
"client_id",
|
|
690
|
+
"user_id",
|
|
691
|
+
"tenant_id",
|
|
692
|
+
]
|
|
693
|
+
for attr in ctx_attrs:
|
|
694
|
+
if hasattr(ctx, attr):
|
|
695
|
+
value = getattr(ctx, attr)
|
|
696
|
+
if value is not None:
|
|
697
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
698
|
+
|
|
699
|
+
# Also check baggage for session ID
|
|
700
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
701
|
+
if session_id_from_baggage:
|
|
702
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
703
|
+
|
|
704
|
+
# Add prompt arguments
|
|
705
|
+
for key, value in kwargs.items():
|
|
706
|
+
if key != "ctx":
|
|
707
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
708
|
+
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
709
|
+
else:
|
|
710
|
+
span.set_attribute(
|
|
711
|
+
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
# Add event for prompt generation start
|
|
715
|
+
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
716
|
+
|
|
435
717
|
try:
|
|
436
718
|
result = await func(*args, **kwargs)
|
|
437
719
|
span.set_status(Status(StatusCode.OK))
|
|
438
|
-
|
|
439
|
-
# Add
|
|
720
|
+
|
|
721
|
+
# Add event for successful generation
|
|
722
|
+
span.add_event(
|
|
723
|
+
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Add message count and type information
|
|
440
727
|
if isinstance(result, list):
|
|
441
|
-
span.set_attribute("mcp.prompt.message_count", len(result))
|
|
442
|
-
|
|
728
|
+
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
729
|
+
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
730
|
+
|
|
731
|
+
# Analyze message types if they have role attributes
|
|
732
|
+
roles = []
|
|
733
|
+
for msg in result:
|
|
734
|
+
if hasattr(msg, "role"):
|
|
735
|
+
roles.append(msg.role)
|
|
736
|
+
elif isinstance(msg, dict) and "role" in msg:
|
|
737
|
+
roles.append(msg["role"])
|
|
738
|
+
|
|
739
|
+
if roles:
|
|
740
|
+
unique_roles = list(set(roles))
|
|
741
|
+
span.set_attribute(
|
|
742
|
+
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
743
|
+
)
|
|
744
|
+
span.set_attribute(
|
|
745
|
+
"mcp.prompt.result.role_counts",
|
|
746
|
+
str({role: roles.count(role) for role in unique_roles}),
|
|
747
|
+
)
|
|
748
|
+
elif isinstance(result, str):
|
|
749
|
+
span.set_attribute("mcp.prompt.result.type", "string")
|
|
750
|
+
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
751
|
+
else:
|
|
752
|
+
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
753
|
+
|
|
443
754
|
return result
|
|
444
755
|
except Exception as e:
|
|
445
756
|
span.record_exception(e)
|
|
446
757
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
758
|
+
|
|
759
|
+
# Add event for error
|
|
760
|
+
span.add_event(
|
|
761
|
+
"prompt.generation.error",
|
|
762
|
+
{
|
|
763
|
+
"prompt.name": prompt_name,
|
|
764
|
+
"error.type": type(e).__name__,
|
|
765
|
+
"error.message": str(e),
|
|
766
|
+
},
|
|
767
|
+
)
|
|
447
768
|
raise
|
|
448
|
-
|
|
769
|
+
|
|
449
770
|
@functools.wraps(func)
|
|
450
771
|
def sync_wrapper(*args, **kwargs):
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
772
|
+
# Create a more descriptive span name
|
|
773
|
+
span_name = f"mcp.prompt.{prompt_name}.generate"
|
|
774
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
775
|
+
# Add comprehensive attributes
|
|
776
|
+
span.set_attribute("mcp.component.type", "prompt")
|
|
777
|
+
span.set_attribute("mcp.component.name", prompt_name)
|
|
778
|
+
span.set_attribute("mcp.prompt.name", prompt_name)
|
|
779
|
+
span.set_attribute("mcp.prompt.function", func.__name__)
|
|
780
|
+
span.set_attribute(
|
|
781
|
+
"mcp.prompt.module",
|
|
782
|
+
func.__module__ if hasattr(func, "__module__") else "unknown",
|
|
783
|
+
)
|
|
784
|
+
span.set_attribute("mcp.execution.async", False)
|
|
785
|
+
|
|
454
786
|
# Extract Context parameter if present
|
|
455
|
-
ctx = kwargs.get(
|
|
456
|
-
if ctx
|
|
457
|
-
|
|
458
|
-
|
|
787
|
+
ctx = kwargs.get("ctx")
|
|
788
|
+
if ctx:
|
|
789
|
+
# Only extract known MCP context attributes
|
|
790
|
+
ctx_attrs = [
|
|
791
|
+
"request_id",
|
|
792
|
+
"session_id",
|
|
793
|
+
"client_id",
|
|
794
|
+
"user_id",
|
|
795
|
+
"tenant_id",
|
|
796
|
+
]
|
|
797
|
+
for attr in ctx_attrs:
|
|
798
|
+
if hasattr(ctx, attr):
|
|
799
|
+
value = getattr(ctx, attr)
|
|
800
|
+
if value is not None:
|
|
801
|
+
span.set_attribute(f"mcp.context.{attr}", str(value))
|
|
802
|
+
|
|
803
|
+
# Also check baggage for session ID
|
|
804
|
+
session_id_from_baggage = baggage.get_baggage("mcp.session.id")
|
|
805
|
+
if session_id_from_baggage:
|
|
806
|
+
span.set_attribute("mcp.session.id", session_id_from_baggage)
|
|
807
|
+
|
|
808
|
+
# Add prompt arguments
|
|
809
|
+
for key, value in kwargs.items():
|
|
810
|
+
if key != "ctx":
|
|
811
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
812
|
+
span.set_attribute(f"mcp.prompt.arg.{key}", str(value))
|
|
813
|
+
else:
|
|
814
|
+
span.set_attribute(
|
|
815
|
+
f"mcp.prompt.arg.{key}.type", type(value).__name__
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# Add event for prompt generation start
|
|
819
|
+
span.add_event("prompt.generation.started", {"prompt.name": prompt_name})
|
|
820
|
+
|
|
459
821
|
try:
|
|
460
822
|
result = func(*args, **kwargs)
|
|
461
823
|
span.set_status(Status(StatusCode.OK))
|
|
462
|
-
|
|
463
|
-
# Add
|
|
824
|
+
|
|
825
|
+
# Add event for successful generation
|
|
826
|
+
span.add_event(
|
|
827
|
+
"prompt.generation.completed", {"prompt.name": prompt_name}
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Add message count and type information
|
|
464
831
|
if isinstance(result, list):
|
|
465
|
-
span.set_attribute("mcp.prompt.message_count", len(result))
|
|
466
|
-
|
|
832
|
+
span.set_attribute("mcp.prompt.result.message_count", len(result))
|
|
833
|
+
span.set_attribute("mcp.prompt.result.type", "message_list")
|
|
834
|
+
|
|
835
|
+
# Analyze message types if they have role attributes
|
|
836
|
+
roles = []
|
|
837
|
+
for msg in result:
|
|
838
|
+
if hasattr(msg, "role"):
|
|
839
|
+
roles.append(msg.role)
|
|
840
|
+
elif isinstance(msg, dict) and "role" in msg:
|
|
841
|
+
roles.append(msg["role"])
|
|
842
|
+
|
|
843
|
+
if roles:
|
|
844
|
+
unique_roles = list(set(roles))
|
|
845
|
+
span.set_attribute(
|
|
846
|
+
"mcp.prompt.result.roles", ",".join(unique_roles)
|
|
847
|
+
)
|
|
848
|
+
span.set_attribute(
|
|
849
|
+
"mcp.prompt.result.role_counts",
|
|
850
|
+
str({role: roles.count(role) for role in unique_roles}),
|
|
851
|
+
)
|
|
852
|
+
elif isinstance(result, str):
|
|
853
|
+
span.set_attribute("mcp.prompt.result.type", "string")
|
|
854
|
+
span.set_attribute("mcp.prompt.result.length", len(result))
|
|
855
|
+
else:
|
|
856
|
+
span.set_attribute("mcp.prompt.result.type", type(result).__name__)
|
|
857
|
+
|
|
467
858
|
return result
|
|
468
859
|
except Exception as e:
|
|
469
860
|
span.record_exception(e)
|
|
470
861
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
862
|
+
|
|
863
|
+
# Add event for error
|
|
864
|
+
span.add_event(
|
|
865
|
+
"prompt.generation.error",
|
|
866
|
+
{
|
|
867
|
+
"prompt.name": prompt_name,
|
|
868
|
+
"error.type": type(e).__name__,
|
|
869
|
+
"error.message": str(e),
|
|
870
|
+
},
|
|
871
|
+
)
|
|
471
872
|
raise
|
|
472
|
-
|
|
873
|
+
|
|
473
874
|
if asyncio.iscoroutinefunction(func):
|
|
474
875
|
return async_wrapper
|
|
475
876
|
else:
|
|
476
877
|
return sync_wrapper
|
|
477
878
|
|
|
879
|
+
|
|
478
880
|
@asynccontextmanager
|
|
479
881
|
async def telemetry_lifespan(mcp_instance):
|
|
480
882
|
"""Simplified lifespan for telemetry initialization and cleanup."""
|
|
481
|
-
global _provider
|
|
482
|
-
|
|
883
|
+
global _provider
|
|
884
|
+
|
|
483
885
|
# Initialize telemetry with the server name
|
|
484
886
|
provider = init_telemetry(service_name=mcp_instance.name)
|
|
485
|
-
|
|
887
|
+
|
|
486
888
|
# If provider is None, telemetry is disabled
|
|
487
889
|
if provider is None:
|
|
488
890
|
# Just yield without any telemetry setup
|
|
489
891
|
yield
|
|
490
892
|
return
|
|
491
|
-
|
|
893
|
+
|
|
492
894
|
# Try to add session tracking middleware if possible
|
|
493
895
|
try:
|
|
494
896
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
495
897
|
from starlette.requests import Request
|
|
496
|
-
|
|
898
|
+
|
|
497
899
|
class SessionTracingMiddleware(BaseHTTPMiddleware):
|
|
498
900
|
async def dispatch(self, request: Request, call_next):
|
|
499
|
-
# Extract session ID from query params
|
|
500
|
-
session_id = request.query_params.get(
|
|
501
|
-
if session_id:
|
|
502
|
-
#
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
901
|
+
# Extract session ID from query params or headers
|
|
902
|
+
session_id = request.query_params.get("session_id")
|
|
903
|
+
if not session_id:
|
|
904
|
+
# Check headers as fallback
|
|
905
|
+
session_id = request.headers.get("x-session-id")
|
|
906
|
+
|
|
907
|
+
# Create a descriptive span name based on the request
|
|
908
|
+
method = request.method
|
|
909
|
+
path = request.url.path
|
|
910
|
+
|
|
911
|
+
# Determine the operation type from the path
|
|
912
|
+
operation_type = "unknown"
|
|
913
|
+
if "/mcp" in path:
|
|
914
|
+
operation_type = "mcp.request"
|
|
915
|
+
elif "/sse" in path:
|
|
916
|
+
operation_type = "sse.stream"
|
|
917
|
+
elif "/auth" in path:
|
|
918
|
+
operation_type = "auth"
|
|
919
|
+
|
|
920
|
+
span_name = f"{operation_type}.{method.lower()}"
|
|
921
|
+
|
|
922
|
+
tracer = get_tracer()
|
|
923
|
+
with tracer.start_as_current_span(span_name) as span:
|
|
924
|
+
# Add comprehensive HTTP attributes
|
|
925
|
+
span.set_attribute("http.method", method)
|
|
926
|
+
span.set_attribute("http.url", str(request.url))
|
|
927
|
+
span.set_attribute("http.scheme", request.url.scheme)
|
|
928
|
+
span.set_attribute("http.host", request.url.hostname or "unknown")
|
|
929
|
+
span.set_attribute("http.target", path)
|
|
930
|
+
span.set_attribute(
|
|
931
|
+
"http.user_agent", request.headers.get("user-agent", "unknown")
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Add session tracking
|
|
935
|
+
if session_id:
|
|
513
936
|
span.set_attribute("mcp.session.id", session_id)
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
937
|
+
# Add to baggage for propagation
|
|
938
|
+
ctx = baggage.set_baggage("mcp.session.id", session_id)
|
|
939
|
+
from opentelemetry import context
|
|
940
|
+
|
|
941
|
+
token = context.attach(ctx)
|
|
942
|
+
else:
|
|
943
|
+
token = None
|
|
944
|
+
|
|
945
|
+
# Add request size if available
|
|
946
|
+
content_length = request.headers.get("content-length")
|
|
947
|
+
if content_length:
|
|
948
|
+
span.set_attribute("http.request.size", int(content_length))
|
|
949
|
+
|
|
950
|
+
# Add event for request start
|
|
951
|
+
span.add_event(
|
|
952
|
+
"http.request.started", {"method": method, "path": path}
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
try:
|
|
956
|
+
response = await call_next(request)
|
|
957
|
+
|
|
958
|
+
# Add response attributes
|
|
959
|
+
span.set_attribute("http.status_code", response.status_code)
|
|
960
|
+
span.set_attribute(
|
|
961
|
+
"http.status_class", f"{response.status_code // 100}xx"
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
# Set span status based on HTTP status
|
|
965
|
+
if response.status_code >= 400:
|
|
966
|
+
span.set_status(
|
|
967
|
+
Status(StatusCode.ERROR, f"HTTP {response.status_code}")
|
|
968
|
+
)
|
|
969
|
+
else:
|
|
970
|
+
span.set_status(Status(StatusCode.OK))
|
|
971
|
+
|
|
972
|
+
# Add event for request completion
|
|
973
|
+
span.add_event(
|
|
974
|
+
"http.request.completed",
|
|
975
|
+
{
|
|
976
|
+
"method": method,
|
|
977
|
+
"path": path,
|
|
978
|
+
"status_code": response.status_code,
|
|
979
|
+
},
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
return response
|
|
983
|
+
except Exception as e:
|
|
984
|
+
span.record_exception(e)
|
|
985
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
986
|
+
|
|
987
|
+
# Add event for error
|
|
988
|
+
span.add_event(
|
|
989
|
+
"http.request.error",
|
|
990
|
+
{
|
|
991
|
+
"method": method,
|
|
992
|
+
"path": path,
|
|
993
|
+
"error.type": type(e).__name__,
|
|
994
|
+
"error.message": str(e),
|
|
995
|
+
},
|
|
996
|
+
)
|
|
997
|
+
raise
|
|
998
|
+
finally:
|
|
999
|
+
if token:
|
|
520
1000
|
context.detach(token)
|
|
521
|
-
|
|
522
|
-
return await call_next(request)
|
|
523
|
-
|
|
1001
|
+
|
|
524
1002
|
# Try to add middleware to FastMCP app if it has Starlette app
|
|
525
|
-
if hasattr(mcp_instance,
|
|
526
|
-
app = getattr(mcp_instance,
|
|
527
|
-
if app and hasattr(app,
|
|
1003
|
+
if hasattr(mcp_instance, "app") or hasattr(mcp_instance, "_app"):
|
|
1004
|
+
app = getattr(mcp_instance, "app", getattr(mcp_instance, "_app", None))
|
|
1005
|
+
if app and hasattr(app, "add_middleware"):
|
|
528
1006
|
app.add_middleware(SessionTracingMiddleware)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1007
|
+
print("[TELEMETRY DEBUG] Added SessionTracingMiddleware to FastMCP app")
|
|
1008
|
+
|
|
1009
|
+
# Also try to instrument FastMCP's internal handlers
|
|
1010
|
+
if hasattr(mcp_instance, "_tool_manager") and hasattr(
|
|
1011
|
+
mcp_instance._tool_manager, "tools"
|
|
1012
|
+
):
|
|
1013
|
+
print(
|
|
1014
|
+
f"[TELEMETRY DEBUG] Found {len(mcp_instance._tool_manager.tools)} tools in FastMCP"
|
|
1015
|
+
)
|
|
1016
|
+
# The tools should already be instrumented when they were registered
|
|
1017
|
+
|
|
1018
|
+
# Try to patch FastMCP's request handling to ensure context propagation
|
|
1019
|
+
if hasattr(mcp_instance, "handle_request"):
|
|
1020
|
+
original_handle_request = mcp_instance.handle_request
|
|
1021
|
+
|
|
1022
|
+
async def traced_handle_request(*args, **kwargs):
|
|
1023
|
+
tracer = get_tracer()
|
|
1024
|
+
with tracer.start_as_current_span("mcp.handle_request") as span:
|
|
1025
|
+
span.set_attribute("mcp.request.handler", "handle_request")
|
|
1026
|
+
return await original_handle_request(*args, **kwargs)
|
|
1027
|
+
|
|
1028
|
+
mcp_instance.handle_request = traced_handle_request
|
|
1029
|
+
print("[TELEMETRY DEBUG] Patched FastMCP handle_request method")
|
|
1030
|
+
|
|
1031
|
+
except Exception as e:
|
|
1032
|
+
print(f"[TELEMETRY DEBUG] Error setting up telemetry middleware: {e}")
|
|
1033
|
+
import traceback
|
|
1034
|
+
|
|
1035
|
+
traceback.print_exc()
|
|
1036
|
+
|
|
532
1037
|
try:
|
|
533
1038
|
# Yield control back to FastMCP
|
|
534
1039
|
yield
|
|
535
1040
|
finally:
|
|
536
1041
|
# Cleanup - shutdown the provider
|
|
537
|
-
if _provider and hasattr(_provider,
|
|
1042
|
+
if _provider and hasattr(_provider, "shutdown"):
|
|
538
1043
|
_provider.force_flush()
|
|
539
1044
|
_provider.shutdown()
|
|
540
|
-
_provider = None
|
|
1045
|
+
_provider = None
|